1 | /* An experimental state machine, for tracking bad calls from within |
2 | signal handlers. |
3 | |
4 | Copyright (C) 2019-2024 Free Software Foundation, Inc. |
5 | Contributed by David Malcolm <dmalcolm@redhat.com>. |
6 | |
7 | This file is part of GCC. |
8 | |
9 | GCC is free software; you can redistribute it and/or modify it |
10 | under the terms of the GNU General Public License as published by |
11 | the Free Software Foundation; either version 3, or (at your option) |
12 | any later version. |
13 | |
14 | GCC is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
17 | General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU General Public License |
20 | along with GCC; see the file COPYING3. If not see |
21 | <http://www.gnu.org/licenses/>. */ |
22 | |
23 | #include "config.h" |
24 | #define INCLUDE_MEMORY |
25 | #include "system.h" |
26 | #include "coretypes.h" |
27 | #include "make-unique.h" |
28 | #include "tree.h" |
29 | #include "function.h" |
30 | #include "basic-block.h" |
31 | #include "gimple.h" |
32 | #include "options.h" |
33 | #include "bitmap.h" |
34 | #include "diagnostic-path.h" |
35 | #include "analyzer/analyzer.h" |
36 | #include "diagnostic-event-id.h" |
37 | #include "analyzer/analyzer-logging.h" |
38 | #include "analyzer/sm.h" |
39 | #include "analyzer/pending-diagnostic.h" |
40 | #include "sbitmap.h" |
41 | #include "ordered-hash-map.h" |
42 | #include "selftest.h" |
43 | #include "analyzer/call-string.h" |
44 | #include "analyzer/program-point.h" |
45 | #include "analyzer/store.h" |
46 | #include "analyzer/region-model.h" |
47 | #include "analyzer/program-state.h" |
48 | #include "analyzer/checker-path.h" |
49 | #include "cfg.h" |
50 | #include "gimple-iterator.h" |
51 | #include "cgraph.h" |
52 | #include "analyzer/supergraph.h" |
53 | #include "analyzer/diagnostic-manager.h" |
54 | #include "shortest-paths.h" |
55 | #include "analyzer/exploded-graph.h" |
56 | #include "analyzer/function-set.h" |
57 | #include "analyzer/analyzer-selftests.h" |
58 | |
59 | #if ENABLE_ANALYZER |
60 | |
61 | namespace ana { |
62 | |
63 | namespace { |
64 | |
65 | /* An experimental state machine, for tracking calls to async-signal-unsafe |
66 | functions from within signal handlers. */ |
67 | |
68 | class signal_state_machine : public state_machine |
69 | { |
70 | public: |
71 | signal_state_machine (logger *logger); |
72 | |
73 | bool inherited_state_p () const final override { return false; } |
74 | |
75 | bool on_stmt (sm_context *sm_ctxt, |
76 | const supernode *node, |
77 | const gimple *stmt) const final override; |
78 | |
79 | bool can_purge_p (state_t s) const final override; |
80 | |
81 | /* These states are "global", rather than per-expression. */ |
82 | |
83 | /* State for when we're in a signal handler. */ |
84 | state_t m_in_signal_handler; |
85 | |
86 | /* Stop state. */ |
87 | state_t m_stop; |
88 | }; |
89 | |
90 | /* Concrete subclass for describing call to an async-signal-unsafe function |
91 | from a signal handler. */ |
92 | |
93 | class signal_unsafe_call |
94 | : public pending_diagnostic_subclass<signal_unsafe_call> |
95 | { |
96 | public: |
97 | signal_unsafe_call (const signal_state_machine &sm, const gcall *unsafe_call, |
98 | tree unsafe_fndecl) |
99 | : m_sm (sm), m_unsafe_call (unsafe_call), m_unsafe_fndecl (unsafe_fndecl) |
100 | { |
101 | gcc_assert (m_unsafe_fndecl); |
102 | } |
103 | |
104 | const char *get_kind () const final override { return "signal_unsafe_call" ; } |
105 | |
106 | bool operator== (const signal_unsafe_call &other) const |
107 | { |
108 | return m_unsafe_call == other.m_unsafe_call; |
109 | } |
110 | |
111 | int get_controlling_option () const final override |
112 | { |
113 | return OPT_Wanalyzer_unsafe_call_within_signal_handler; |
114 | } |
115 | |
116 | bool emit (diagnostic_emission_context &ctxt) final override |
117 | { |
118 | auto_diagnostic_group d; |
119 | /* CWE-479: Signal Handler Use of a Non-reentrant Function. */ |
120 | ctxt.add_cwe (cwe: 479); |
121 | if (ctxt.warn ("call to %qD from within signal handler" , |
122 | m_unsafe_fndecl)) |
123 | { |
124 | /* If we know a possible alternative function, add a note |
125 | suggesting the replacement. */ |
126 | if (const char *replacement = get_replacement_fn ()) |
127 | { |
128 | location_t note_loc = gimple_location (g: m_unsafe_call); |
129 | /* It would be nice to add a fixit, but the gimple call |
130 | location covers the whole call expression. It isn't |
131 | currently possible to cut this down to just the call |
132 | symbol. So the fixit would replace too much. |
133 | note_rich_loc.add_fixit_replace (replacement); */ |
134 | inform (note_loc, |
135 | "%qs is a possible signal-safe alternative for %qD" , |
136 | replacement, m_unsafe_fndecl); |
137 | } |
138 | return true; |
139 | } |
140 | return false; |
141 | } |
142 | |
143 | label_text describe_state_change (const evdesc::state_change &change) |
144 | final override |
145 | { |
146 | if (change.is_global_p () |
147 | && change.m_new_state == m_sm.m_in_signal_handler) |
148 | { |
149 | const function *handler = change.m_event.get_dest_function (); |
150 | gcc_assert (handler); |
151 | return change.formatted_print (fmt: "registering %qD as signal handler" , |
152 | handler->decl); |
153 | } |
154 | return label_text (); |
155 | } |
156 | |
157 | label_text describe_final_event (const evdesc::final_event &ev) final override |
158 | { |
159 | return ev.formatted_print (fmt: "call to %qD from within signal handler" , |
160 | m_unsafe_fndecl); |
161 | } |
162 | |
163 | private: |
164 | const signal_state_machine &m_sm; |
165 | const gcall *m_unsafe_call; |
166 | tree m_unsafe_fndecl; |
167 | |
168 | /* Returns a replacement function as text if it exists. Currently |
169 | only "exit" has a signal-safe replacement "_exit", which does |
170 | slightly less, but can be used in a signal handler. */ |
171 | const char * |
172 | get_replacement_fn () |
173 | { |
174 | gcc_assert (m_unsafe_fndecl && DECL_P (m_unsafe_fndecl)); |
175 | |
176 | if (id_equal (str: "exit" , DECL_NAME (m_unsafe_fndecl))) |
177 | return "_exit" ; |
178 | |
179 | return NULL; |
180 | } |
181 | }; |
182 | |
183 | /* signal_state_machine's ctor. */ |
184 | |
185 | signal_state_machine::signal_state_machine (logger *logger) |
186 | : state_machine ("signal" , logger), |
187 | m_in_signal_handler (add_state (name: "in_signal_handler" )), |
188 | m_stop (add_state (name: "stop" )) |
189 | { |
190 | } |
191 | |
192 | /* Update MODEL for edges that simulate HANDLER_FUN being called as |
193 | an signal-handler in response to a signal. */ |
194 | |
195 | static void |
196 | update_model_for_signal_handler (region_model *model, |
197 | const function &handler_fun) |
198 | { |
199 | gcc_assert (model); |
200 | /* Purge all state within MODEL. */ |
201 | *model = region_model (model->get_manager ()); |
202 | model->push_frame (fun: handler_fun, NULL, NULL); |
203 | } |
204 | |
205 | /* Custom exploded_edge info: entry into a signal-handler. */ |
206 | |
207 | class signal_delivery_edge_info_t : public custom_edge_info |
208 | { |
209 | public: |
210 | void print (pretty_printer *pp) const final override |
211 | { |
212 | pp_string (pp, "signal delivered" ); |
213 | } |
214 | |
215 | json::object *to_json () const |
216 | { |
217 | json::object *custom_obj = new json::object (); |
218 | return custom_obj; |
219 | } |
220 | |
221 | bool update_model (region_model *model, |
222 | const exploded_edge *eedge, |
223 | region_model_context *) const final override |
224 | { |
225 | gcc_assert (eedge); |
226 | gcc_assert (eedge->m_dest->get_function ()); |
227 | update_model_for_signal_handler (model, |
228 | handler_fun: *eedge->m_dest->get_function ()); |
229 | return true; |
230 | } |
231 | |
232 | void add_events_to_path (checker_path *emission_path, |
233 | const exploded_edge &eedge ATTRIBUTE_UNUSED) |
234 | const final override |
235 | { |
236 | emission_path->add_event |
237 | (event: make_unique<precanned_custom_event> |
238 | (args: event_loc_info (UNKNOWN_LOCATION, NULL_TREE, 0), |
239 | args: "later on," |
240 | " when the signal is delivered to the process" )); |
241 | } |
242 | }; |
243 | |
244 | /* Concrete subclass of custom_transition for modeling registration of a |
245 | signal handler and the signal handler later being called. */ |
246 | |
247 | class register_signal_handler : public custom_transition |
248 | { |
249 | public: |
250 | register_signal_handler (const signal_state_machine &sm, |
251 | tree fndecl) |
252 | : m_sm (sm), m_fndecl (fndecl) {} |
253 | |
254 | /* Model a signal-handler FNDECL being called at some later point |
255 | by injecting an edge to a new function-entry node with an empty |
256 | callstring, setting the 'in-signal-handler' global state |
257 | on the node. */ |
258 | void impl_transition (exploded_graph *eg, |
259 | exploded_node *src_enode, |
260 | int sm_idx) final override |
261 | { |
262 | function *handler_fun = DECL_STRUCT_FUNCTION (m_fndecl); |
263 | if (!handler_fun) |
264 | return; |
265 | const extrinsic_state &ext_state = eg->get_ext_state (); |
266 | program_point entering_handler |
267 | = program_point::from_function_entry (mgr: *ext_state.get_model_manager (), |
268 | sg: eg->get_supergraph (), |
269 | fun: *handler_fun); |
270 | |
271 | program_state state_entering_handler (ext_state); |
272 | update_model_for_signal_handler (model: state_entering_handler.m_region_model, |
273 | handler_fun: *handler_fun); |
274 | state_entering_handler.m_checker_states[sm_idx]->set_global_state |
275 | (m_sm.m_in_signal_handler); |
276 | |
277 | exploded_node *dst_enode = eg->get_or_create_node (point: entering_handler, |
278 | state: state_entering_handler, |
279 | enode_for_diag: src_enode); |
280 | if (dst_enode) |
281 | eg->add_edge (src: src_enode, dest: dst_enode, NULL, /*state_change (),*/ |
282 | could_do_work: true, /* assume does work */ |
283 | custom: make_unique<signal_delivery_edge_info_t> ()); |
284 | } |
285 | |
286 | const signal_state_machine &m_sm; |
287 | tree m_fndecl; |
288 | }; |
289 | |
290 | /* Get a set of functions that are known to be unsafe to call from an |
291 | async signal handler. */ |
292 | |
293 | static function_set |
294 | get_async_signal_unsafe_fns () |
295 | { |
296 | // TODO: populate this list more fully |
297 | static const char * const async_signal_unsafe_fns[] = { |
298 | /* This array must be kept sorted. */ |
299 | "exit" , |
300 | "fprintf" , |
301 | "free" , |
302 | "malloc" , |
303 | "printf" , |
304 | "snprintf" , |
305 | "sprintf" , |
306 | "vfprintf" , |
307 | "vprintf" , |
308 | "vsnprintf" , |
309 | "vsprintf" |
310 | }; |
311 | const size_t count = ARRAY_SIZE (async_signal_unsafe_fns); |
312 | function_set fs (async_signal_unsafe_fns, count); |
313 | return fs; |
314 | } |
315 | |
316 | /* Return true if FNDECL is known to be unsafe to call from a signal |
317 | handler. */ |
318 | |
319 | static bool |
320 | signal_unsafe_p (tree fndecl) |
321 | { |
322 | function_set fs = get_async_signal_unsafe_fns (); |
323 | return fs.contains_decl_p (fndecl); |
324 | } |
325 | |
326 | /* Implementation of state_machine::on_stmt vfunc for signal_state_machine. */ |
327 | |
328 | bool |
329 | signal_state_machine::on_stmt (sm_context *sm_ctxt, |
330 | const supernode *node, |
331 | const gimple *stmt) const |
332 | { |
333 | const state_t global_state = sm_ctxt->get_global_state (); |
334 | if (global_state == m_start) |
335 | { |
336 | if (const gcall *call = dyn_cast <const gcall *> (p: stmt)) |
337 | if (tree callee_fndecl = sm_ctxt->get_fndecl_for_call (call)) |
338 | if (is_named_call_p (fndecl: callee_fndecl, funcname: "signal" , call, num_args: 2)) |
339 | { |
340 | tree handler = gimple_call_arg (gs: call, index: 1); |
341 | if (TREE_CODE (handler) == ADDR_EXPR |
342 | && TREE_CODE (TREE_OPERAND (handler, 0)) == FUNCTION_DECL) |
343 | { |
344 | tree fndecl = TREE_OPERAND (handler, 0); |
345 | register_signal_handler rsh (*this, fndecl); |
346 | sm_ctxt->on_custom_transition (transition: &rsh); |
347 | } |
348 | } |
349 | } |
350 | else if (global_state == m_in_signal_handler) |
351 | { |
352 | if (const gcall *call = dyn_cast <const gcall *> (p: stmt)) |
353 | if (tree callee_fndecl = sm_ctxt->get_fndecl_for_call (call)) |
354 | if (signal_unsafe_p (fndecl: callee_fndecl)) |
355 | if (sm_ctxt->get_global_state () == m_in_signal_handler) |
356 | sm_ctxt->warn (node, stmt, NULL_TREE, |
357 | d: make_unique<signal_unsafe_call> |
358 | (args: *this, args&: call, args&: callee_fndecl)); |
359 | } |
360 | |
361 | return false; |
362 | } |
363 | |
364 | bool |
365 | signal_state_machine::can_purge_p (state_t s ATTRIBUTE_UNUSED) const |
366 | { |
367 | return true; |
368 | } |
369 | |
370 | } // anonymous namespace |
371 | |
372 | /* Internal interface to this file. */ |
373 | |
374 | state_machine * |
375 | make_signal_state_machine (logger *logger) |
376 | { |
377 | return new signal_state_machine (logger); |
378 | } |
379 | |
380 | #if CHECKING_P |
381 | |
382 | namespace selftest { |
383 | |
384 | /* Run all of the selftests within this file. */ |
385 | |
386 | void |
387 | analyzer_sm_signal_cc_tests () |
388 | { |
389 | function_set fs = get_async_signal_unsafe_fns (); |
390 | fs.assert_sorted (); |
391 | fs.assert_sane (); |
392 | } |
393 | |
394 | } // namespace selftest |
395 | |
396 | #endif /* CHECKING_P */ |
397 | |
398 | } // namespace ana |
399 | |
400 | #endif /* #if ENABLE_ANALYZER */ |
401 | |