1/* A state machine for detecting misuses of <stdio.h>'s FILE * API. 2 Copyright (C) 2019-2020 Free Software Foundation, Inc. 3 Contributed by David Malcolm <dmalcolm@redhat.com>. 4 5This file is part of GCC. 6 7GCC is free software; you can redistribute it and/or modify it 8under the terms of the GNU General Public License as published by 9the Free Software Foundation; either version 3, or (at your option) 10any later version. 11 12GCC is distributed in the hope that it will be useful, but 13WITHOUT ANY WARRANTY; without even the implied warranty of 14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15General Public License for more details. 16 17You should have received a copy of the GNU General Public License 18along with GCC; see the file COPYING3. If not see 19<http://www.gnu.org/licenses/>. */ 20 21#include "config.h" 22#include "system.h" 23#include "coretypes.h" 24#include "tree.h" 25#include "function.h" 26#include "basic-block.h" 27#include "gimple.h" 28#include "options.h" 29#include "diagnostic-path.h" 30#include "diagnostic-metadata.h" 31#include "function.h" 32#include "analyzer/analyzer.h" 33#include "diagnostic-event-id.h" 34#include "analyzer/analyzer-logging.h" 35#include "analyzer/sm.h" 36#include "analyzer/pending-diagnostic.h" 37#include "analyzer/function-set.h" 38#include "analyzer/analyzer-selftests.h" 39 40#if ENABLE_ANALYZER 41 42namespace ana { 43 44namespace { 45 46/* A state machine for detecting misuses of <stdio.h>'s FILE * API. */ 47 48class fileptr_state_machine : public state_machine 49{ 50public: 51 fileptr_state_machine (logger *logger); 52 53 bool inherited_state_p () const FINAL OVERRIDE { return false; } 54 55 bool on_stmt (sm_context *sm_ctxt, 56 const supernode *node, 57 const gimple *stmt) const FINAL OVERRIDE; 58 59 void on_condition (sm_context *sm_ctxt, 60 const supernode *node, 61 const gimple *stmt, 62 tree lhs, 63 enum tree_code op, 64 tree rhs) const FINAL OVERRIDE; 65 66 bool can_purge_p (state_t s) const FINAL OVERRIDE; 67 pending_diagnostic *on_leak (tree var) const FINAL OVERRIDE; 68 69 /* Start state. */ 70 state_t m_start; 71 72 /* State for a FILE * returned from fopen that hasn't been checked for 73 NULL. 74 It could be an open stream, or could be NULL. */ 75 state_t m_unchecked; 76 77 /* State for a FILE * that's known to be NULL. */ 78 state_t m_null; 79 80 /* State for a FILE * that's known to be a non-NULL open stream. */ 81 state_t m_nonnull; 82 83 /* State for a FILE * that's had fclose called on it. */ 84 state_t m_closed; 85 86 /* Stop state, for a FILE * we don't want to track any more. */ 87 state_t m_stop; 88}; 89 90/* Base class for diagnostics relative to fileptr_state_machine. */ 91 92class file_diagnostic : public pending_diagnostic 93{ 94public: 95 file_diagnostic (const fileptr_state_machine &sm, tree arg) 96 : m_sm (sm), m_arg (arg) 97 {} 98 99 bool subclass_equal_p (const pending_diagnostic &base_other) const OVERRIDE 100 { 101 return same_tree_p (m_arg, ((const file_diagnostic &)base_other).m_arg); 102 } 103 104 label_text describe_state_change (const evdesc::state_change &change) 105 OVERRIDE 106 { 107 if (change.m_old_state == m_sm.m_start 108 && change.m_new_state == m_sm.m_unchecked) 109 // TODO: verify that it's the fopen stmt, not a copy 110 return label_text::borrow ("opened here"); 111 if (change.m_old_state == m_sm.m_unchecked 112 && change.m_new_state == m_sm.m_nonnull) 113 return change.formatted_print ("assuming %qE is non-NULL", 114 change.m_expr); 115 if (change.m_new_state == m_sm.m_null) 116 return change.formatted_print ("assuming %qE is NULL", 117 change.m_expr); 118 return label_text (); 119 } 120 121protected: 122 const fileptr_state_machine &m_sm; 123 tree m_arg; 124}; 125 126class double_fclose : public file_diagnostic 127{ 128public: 129 double_fclose (const fileptr_state_machine &sm, tree arg) 130 : file_diagnostic (sm, arg) 131 {} 132 133 const char *get_kind () const FINAL OVERRIDE { return "double_fclose"; } 134 135 bool emit (rich_location *rich_loc) FINAL OVERRIDE 136 { 137 return warning_at (rich_loc, OPT_Wanalyzer_double_fclose, 138 "double %<fclose%> of FILE %qE", 139 m_arg); 140 } 141 142 label_text describe_state_change (const evdesc::state_change &change) 143 OVERRIDE 144 { 145 if (change.m_new_state == m_sm.m_closed) 146 { 147 m_first_fclose_event = change.m_event_id; 148 return change.formatted_print ("first %qs here", "fclose"); 149 } 150 return file_diagnostic::describe_state_change (change); 151 } 152 153 label_text describe_final_event (const evdesc::final_event &ev) FINAL OVERRIDE 154 { 155 if (m_first_fclose_event.known_p ()) 156 return ev.formatted_print ("second %qs here; first %qs was at %@", 157 "fclose", "fclose", 158 &m_first_fclose_event); 159 return ev.formatted_print ("second %qs here", "fclose"); 160 } 161 162private: 163 diagnostic_event_id_t m_first_fclose_event; 164}; 165 166class file_leak : public file_diagnostic 167{ 168public: 169 file_leak (const fileptr_state_machine &sm, tree arg) 170 : file_diagnostic (sm, arg) 171 {} 172 173 const char *get_kind () const FINAL OVERRIDE { return "file_leak"; } 174 175 bool emit (rich_location *rich_loc) FINAL OVERRIDE 176 { 177 diagnostic_metadata m; 178 /* CWE-775: "Missing Release of File Descriptor or Handle after 179 Effective Lifetime". */ 180 m.add_cwe (775); 181 return warning_meta (rich_loc, m, OPT_Wanalyzer_file_leak, 182 "leak of FILE %qE", 183 m_arg); 184 } 185 186 label_text describe_state_change (const evdesc::state_change &change) 187 FINAL OVERRIDE 188 { 189 if (change.m_new_state == m_sm.m_unchecked) 190 { 191 m_fopen_event = change.m_event_id; 192 return label_text::borrow ("opened here"); 193 } 194 return file_diagnostic::describe_state_change (change); 195 } 196 197 label_text describe_final_event (const evdesc::final_event &ev) FINAL OVERRIDE 198 { 199 if (m_fopen_event.known_p ()) 200 return ev.formatted_print ("%qE leaks here; was opened at %@", 201 ev.m_expr, &m_fopen_event); 202 else 203 return ev.formatted_print ("%qE leaks here", ev.m_expr); 204 } 205 206private: 207 diagnostic_event_id_t m_fopen_event; 208}; 209 210/* fileptr_state_machine's ctor. */ 211 212fileptr_state_machine::fileptr_state_machine (logger *logger) 213: state_machine ("file", logger) 214{ 215 m_start = add_state ("start"); 216 m_unchecked = add_state ("unchecked"); 217 m_null = add_state ("null"); 218 m_nonnull = add_state ("nonnull"); 219 m_closed = add_state ("closed"); 220 m_stop = add_state ("stop"); 221} 222 223/* Get a set of functions that are known to take a FILE * that must be open, 224 and are known to not close it. */ 225 226static function_set 227get_file_using_fns () 228{ 229 // TODO: populate this list more fully 230 static const char * const funcnames[] = { 231 /* This array must be kept sorted. */ 232 "__fbufsize", 233 "__flbf", 234 "__fpending", 235 "__fpurge" 236 "__freadable", 237 "__freading", 238 "__fsetlocking", 239 "__fwritable", 240 "__fwriting", 241 "clearerr", 242 "clearerr_unlocked", 243 "feof", 244 "feof_unlocked", 245 "ferror", 246 "ferror_unlocked", 247 "fflush", // safe to call with NULL 248 "fflush_unlocked", // safe to call with NULL 249 "fgetc", 250 "fgetc_unlocked", 251 "fgetpos", 252 "fgets", 253 "fgets_unlocked", 254 "fgetwc_unlocked", 255 "fgetws_unlocked", 256 "fileno", 257 "fileno_unlocked", 258 "fprintf", 259 "fputc", 260 "fputc_unlocked", 261 "fputs", 262 "fputs_unlocked", 263 "fputwc_unlocked", 264 "fputws_unlocked", 265 "fread_unlocked", 266 "fseek", 267 "fsetpos", 268 "ftell", 269 "fwrite_unlocked", 270 "getc", 271 "getc_unlocked", 272 "getwc_unlocked", 273 "putc", 274 "putc_unlocked", 275 "rewind", 276 "setbuf", 277 "setbuffer", 278 "setlinebuf", 279 "setvbuf", 280 "ungetc", 281 "vfprintf" 282 }; 283 const size_t count 284 = sizeof(funcnames) / sizeof (funcnames[0]); 285 function_set fs (funcnames, count); 286 return fs; 287} 288 289/* Return true if FNDECL is known to require an open FILE *, and is known 290 to not close it. */ 291 292static bool 293is_file_using_fn_p (tree fndecl) 294{ 295 function_set fs = get_file_using_fns (); 296 return fs.contains_decl_p (fndecl); 297} 298 299/* Implementation of state_machine::on_stmt vfunc for fileptr_state_machine. */ 300 301bool 302fileptr_state_machine::on_stmt (sm_context *sm_ctxt, 303 const supernode *node, 304 const gimple *stmt) const 305{ 306 if (const gcall *call = dyn_cast <const gcall *> (stmt)) 307 if (tree callee_fndecl = sm_ctxt->get_fndecl_for_call (call)) 308 { 309 if (is_named_call_p (callee_fndecl, "fopen", call, 2)) 310 { 311 tree lhs = gimple_call_lhs (call); 312 if (lhs) 313 { 314 lhs = sm_ctxt->get_readable_tree (lhs); 315 sm_ctxt->on_transition (node, stmt, lhs, m_start, m_unchecked); 316 } 317 else 318 { 319 /* TODO: report leak. */ 320 } 321 return true; 322 } 323 324 if (is_named_call_p (callee_fndecl, "fclose", call, 1)) 325 { 326 tree arg = gimple_call_arg (call, 0); 327 arg = sm_ctxt->get_readable_tree (arg); 328 329 sm_ctxt->on_transition (node, stmt, arg, m_start, m_closed); 330 331 // TODO: is it safe to call fclose (NULL) ? 332 sm_ctxt->on_transition (node, stmt, arg, m_unchecked, m_closed); 333 sm_ctxt->on_transition (node, stmt, arg, m_null, m_closed); 334 335 sm_ctxt->on_transition (node, stmt , arg, m_nonnull, m_closed); 336 337 sm_ctxt->warn_for_state (node, stmt, arg, m_closed, 338 new double_fclose (*this, arg)); 339 sm_ctxt->on_transition (node, stmt, arg, m_closed, m_stop); 340 return true; 341 } 342 343 if (is_file_using_fn_p (callee_fndecl)) 344 { 345 // TODO: operations on unchecked file 346 return true; 347 } 348 // etc 349 } 350 351 return false; 352} 353 354/* Implementation of state_machine::on_condition vfunc for 355 fileptr_state_machine. 356 Potentially transition state 'unchecked' to 'nonnull' or to 'null'. */ 357 358void 359fileptr_state_machine::on_condition (sm_context *sm_ctxt, 360 const supernode *node, 361 const gimple *stmt, 362 tree lhs, 363 enum tree_code op, 364 tree rhs) const 365{ 366 if (!zerop (rhs)) 367 return; 368 369 // TODO: has to be a FILE *, specifically 370 if (TREE_CODE (TREE_TYPE (lhs)) != POINTER_TYPE) 371 return; 372 373 // TODO: has to be a FILE *, specifically 374 if (TREE_CODE (TREE_TYPE (rhs)) != POINTER_TYPE) 375 return; 376 377 if (op == NE_EXPR) 378 { 379 log ("got 'ARG != 0' match"); 380 sm_ctxt->on_transition (node, stmt, 381 lhs, m_unchecked, m_nonnull); 382 } 383 else if (op == EQ_EXPR) 384 { 385 log ("got 'ARG == 0' match"); 386 sm_ctxt->on_transition (node, stmt, 387 lhs, m_unchecked, m_null); 388 } 389} 390 391/* Implementation of state_machine::can_purge_p vfunc for fileptr_state_machine. 392 Don't allow purging of pointers in state 'unchecked' or 'nonnull' 393 (to avoid false leak reports). */ 394 395bool 396fileptr_state_machine::can_purge_p (state_t s) const 397{ 398 return s != m_unchecked && s != m_nonnull; 399} 400 401/* Implementation of state_machine::on_leak vfunc for 402 fileptr_state_machine, for complaining about leaks of FILE * in 403 state 'unchecked' and 'nonnull'. */ 404 405pending_diagnostic * 406fileptr_state_machine::on_leak (tree var) const 407{ 408 return new file_leak (*this, var); 409} 410 411} // anonymous namespace 412 413/* Internal interface to this file. */ 414 415state_machine * 416make_fileptr_state_machine (logger *logger) 417{ 418 return new fileptr_state_machine (logger); 419} 420 421#if CHECKING_P 422 423namespace selftest { 424 425/* Run all of the selftests within this file. */ 426 427void 428analyzer_sm_file_cc_tests () 429{ 430 function_set fs = get_file_using_fns (); 431 fs.assert_sorted (); 432 fs.assert_sane (); 433} 434 435} // namespace selftest 436 437#endif /* CHECKING_P */ 438 439} // namespace ana 440 441#endif /* #if ENABLE_ANALYZER */ 442