blob: a2b442a4ef28e2a92150606f24b366ca49a8d71f [file] [log] [blame]
/* An experimental state machine, for tracking "taint": unsanitized uses
of data potentially under an attacker's control.
Copyright (C) 2019-2022 Free Software Foundation, Inc.
Contributed by David Malcolm <dmalcolm@redhat.com>.
This file is part of GCC.
GCC is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
GCC is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with GCC; see the file COPYING3. If not see
<http://www.gnu.org/licenses/>. */
#include "config.h"
#define INCLUDE_MEMORY
#include "system.h"
#include "coretypes.h"
#include "make-unique.h"
#include "tree.h"
#include "function.h"
#include "basic-block.h"
#include "gimple.h"
#include "options.h"
#include "diagnostic-path.h"
#include "diagnostic-metadata.h"
#include "analyzer/analyzer.h"
#include "analyzer/analyzer-logging.h"
#include "gimple-iterator.h"
#include "ordered-hash-map.h"
#include "cgraph.h"
#include "cfg.h"
#include "digraph.h"
#include "stringpool.h"
#include "attribs.h"
#include "analyzer/supergraph.h"
#include "analyzer/call-string.h"
#include "analyzer/program-point.h"
#include "analyzer/store.h"
#include "analyzer/region-model.h"
#include "analyzer/sm.h"
#include "analyzer/program-state.h"
#include "analyzer/pending-diagnostic.h"
#include "analyzer/constraint-manager.h"
#if ENABLE_ANALYZER
namespace ana {
namespace {
/* An enum for describing tainted values. */
enum bounds
{
/* This tainted value has no upper or lower bound. */
BOUNDS_NONE,
/* This tainted value has an upper bound but not lower bound. */
BOUNDS_UPPER,
/* This tainted value has a lower bound but no upper bound. */
BOUNDS_LOWER
};
/* An experimental state machine, for tracking "taint": unsanitized uses
of data potentially under an attacker's control. */
class taint_state_machine : public state_machine
{
public:
taint_state_machine (logger *logger);
bool inherited_state_p () const final override { return true; }
state_t alt_get_inherited_state (const sm_state_map &map,
const svalue *sval,
const extrinsic_state &ext_state)
const final override;
bool on_stmt (sm_context *sm_ctxt,
const supernode *node,
const gimple *stmt) const final override;
void on_condition (sm_context *sm_ctxt,
const supernode *node,
const gimple *stmt,
const svalue *lhs,
enum tree_code op,
const svalue *rhs) const final override;
void on_bounded_ranges (sm_context *sm_ctxt,
const supernode *node,
const gimple *stmt,
const svalue &sval,
const bounded_ranges &ranges) const final override;
bool can_purge_p (state_t s) const final override;
bool get_taint (state_t s, tree type, enum bounds *out) const;
state_t combine_states (state_t s0, state_t s1) const;
private:
void check_control_flow_arg_for_taint (sm_context *sm_ctxt,
const gimple *stmt,
tree expr) const;
void check_for_tainted_size_arg (sm_context *sm_ctxt,
const supernode *node,
const gcall *call,
tree callee_fndecl) const;
void check_for_tainted_divisor (sm_context *sm_ctxt,
const supernode *node,
const gassign *assign) const;
public:
/* State for a "tainted" value: unsanitized data potentially under an
attacker's control. */
state_t m_tainted;
/* State for a "tainted" value that has a lower bound. */
state_t m_has_lb;
/* State for a "tainted" value that has an upper bound. */
state_t m_has_ub;
/* Stop state, for a value we don't want to track any more. */
state_t m_stop;
/* Global state, for when the last condition had tainted arguments. */
state_t m_tainted_control_flow;
};
/* Class for diagnostics relating to taint_state_machine. */
class taint_diagnostic : public pending_diagnostic
{
public:
taint_diagnostic (const taint_state_machine &sm, tree arg,
enum bounds has_bounds)
: m_sm (sm), m_arg (arg), m_has_bounds (has_bounds)
{}
bool subclass_equal_p (const pending_diagnostic &base_other) const override
{
const taint_diagnostic &other = (const taint_diagnostic &)base_other;
return (same_tree_p (m_arg, other.m_arg)
&& m_has_bounds == other.m_has_bounds);
}
label_text describe_state_change (const evdesc::state_change &change) override
{
if (change.m_new_state == m_sm.m_tainted)
{
if (change.m_origin)
return change.formatted_print ("%qE has an unchecked value here"
" (from %qE)",
change.m_expr, change.m_origin);
else
return change.formatted_print ("%qE gets an unchecked value here",
change.m_expr);
}
else if (change.m_new_state == m_sm.m_has_lb)
return change.formatted_print ("%qE has its lower bound checked here",
change.m_expr);
else if (change.m_new_state == m_sm.m_has_ub)
return change.formatted_print ("%qE has its upper bound checked here",
change.m_expr);
return label_text ();
}
diagnostic_event::meaning
get_meaning_for_state_change (const evdesc::state_change &change)
const final override
{
if (change.m_new_state == m_sm.m_tainted)
return diagnostic_event::meaning (diagnostic_event::VERB_acquire,
diagnostic_event::NOUN_taint);
return diagnostic_event::meaning ();
}
protected:
const taint_state_machine &m_sm;
tree m_arg;
enum bounds m_has_bounds;
};
/* Concrete taint_diagnostic subclass for reporting attacker-controlled
array index. */
class tainted_array_index : public taint_diagnostic
{
public:
tainted_array_index (const taint_state_machine &sm, tree arg,
enum bounds has_bounds)
: taint_diagnostic (sm, arg, has_bounds)
{}
const char *get_kind () const final override { return "tainted_array_index"; }
int get_controlling_option () const final override
{
return OPT_Wanalyzer_tainted_array_index;
}
bool emit (rich_location *rich_loc) final override
{
diagnostic_metadata m;
/* CWE-129: "Improper Validation of Array Index". */
m.add_cwe (129);
if (m_arg)
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE"
" in array lookup without bounds checking",
m_arg);
break;
case BOUNDS_UPPER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE"
" in array lookup without checking for negative",
m_arg);
break;
case BOUNDS_LOWER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE"
" in array lookup without upper-bounds checking",
m_arg);
break;
}
else
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value"
" in array lookup without bounds checking");
break;
case BOUNDS_UPPER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value"
" in array lookup without checking for"
" negative");
break;
case BOUNDS_LOWER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value"
" in array lookup without upper-bounds"
" checking");
break;
}
}
label_text describe_final_event (const evdesc::final_event &ev) final override
{
if (m_arg)
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return ev.formatted_print
("use of attacker-controlled value %qE in array lookup"
" without bounds checking",
m_arg);
case BOUNDS_UPPER:
return ev.formatted_print
("use of attacker-controlled value %qE"
" in array lookup without checking for negative",
m_arg);
case BOUNDS_LOWER:
return ev.formatted_print
("use of attacker-controlled value %qE"
" in array lookup without upper-bounds checking",
m_arg);
}
else
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return ev.formatted_print
("use of attacker-controlled value in array lookup"
" without bounds checking");
case BOUNDS_UPPER:
return ev.formatted_print
("use of attacker-controlled value"
" in array lookup without checking for negative");
case BOUNDS_LOWER:
return ev.formatted_print
("use of attacker-controlled value"
" in array lookup without upper-bounds checking");
}
}
};
/* Concrete taint_diagnostic subclass for reporting attacker-controlled
pointer offset. */
class tainted_offset : public taint_diagnostic
{
public:
tainted_offset (const taint_state_machine &sm, tree arg,
enum bounds has_bounds)
: taint_diagnostic (sm, arg, has_bounds)
{}
const char *get_kind () const final override { return "tainted_offset"; }
int get_controlling_option () const final override
{
return OPT_Wanalyzer_tainted_offset;
}
bool emit (rich_location *rich_loc) final override
{
diagnostic_metadata m;
/* CWE-823: "Use of Out-of-range Pointer Offset". */
m.add_cwe (823);
if (m_arg)
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as offset"
" without bounds checking",
m_arg);
break;
case BOUNDS_UPPER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as offset"
" without lower-bounds checking",
m_arg);
break;
case BOUNDS_LOWER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as offset"
" without upper-bounds checking",
m_arg);
break;
}
else
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as offset"
" without bounds checking");
break;
case BOUNDS_UPPER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as offset"
" without lower-bounds checking");
break;
case BOUNDS_LOWER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as offset"
" without upper-bounds checking");
break;
}
}
label_text describe_final_event (const evdesc::final_event &ev) final override
{
if (m_arg)
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return ev.formatted_print ("use of attacker-controlled value %qE"
" as offset without bounds checking",
m_arg);
case BOUNDS_UPPER:
return ev.formatted_print ("use of attacker-controlled value %qE"
" as offset without lower-bounds checking",
m_arg);
case BOUNDS_LOWER:
return ev.formatted_print ("use of attacker-controlled value %qE"
" as offset without upper-bounds checking",
m_arg);
}
else
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return ev.formatted_print ("use of attacker-controlled value"
" as offset without bounds checking");
case BOUNDS_UPPER:
return ev.formatted_print ("use of attacker-controlled value"
" as offset without lower-bounds"
" checking");
case BOUNDS_LOWER:
return ev.formatted_print ("use of attacker-controlled value"
" as offset without upper-bounds"
" checking");
}
}
};
/* Concrete taint_diagnostic subclass for reporting attacker-controlled
size. */
class tainted_size : public taint_diagnostic
{
public:
tainted_size (const taint_state_machine &sm, tree arg,
enum bounds has_bounds)
: taint_diagnostic (sm, arg, has_bounds)
{}
const char *get_kind () const override { return "tainted_size"; }
int get_controlling_option () const final override
{
return OPT_Wanalyzer_tainted_size;
}
bool emit (rich_location *rich_loc) override
{
/* "CWE-129: Improper Validation of Array Index". */
diagnostic_metadata m;
m.add_cwe (129);
if (m_arg)
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as size"
" without bounds checking",
m_arg);
break;
case BOUNDS_UPPER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as size"
" without lower-bounds checking",
m_arg);
break;
case BOUNDS_LOWER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as size"
" without upper-bounds checking",
m_arg);
break;
}
else
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as size"
" without bounds checking");
break;
case BOUNDS_UPPER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as size"
" without lower-bounds checking");
break;
case BOUNDS_LOWER:
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as size"
" without upper-bounds checking");
break;
}
}
label_text describe_final_event (const evdesc::final_event &ev) final override
{
if (m_arg)
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return ev.formatted_print ("use of attacker-controlled value %qE"
" as size without bounds checking",
m_arg);
case BOUNDS_UPPER:
return ev.formatted_print ("use of attacker-controlled value %qE"
" as size without lower-bounds checking",
m_arg);
case BOUNDS_LOWER:
return ev.formatted_print ("use of attacker-controlled value %qE"
" as size without upper-bounds checking",
m_arg);
}
else
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return ev.formatted_print ("use of attacker-controlled value"
" as size without bounds checking");
case BOUNDS_UPPER:
return ev.formatted_print ("use of attacker-controlled value"
" as size without lower-bounds checking");
case BOUNDS_LOWER:
return ev.formatted_print ("use of attacker-controlled value"
" as size without upper-bounds checking");
}
}
};
/* Subclass of tainted_size for reporting on tainted size values
passed to an external function annotated with attribute "access". */
class tainted_access_attrib_size : public tainted_size
{
public:
tainted_access_attrib_size (const taint_state_machine &sm, tree arg,
enum bounds has_bounds, tree callee_fndecl,
unsigned size_argno, const char *access_str)
: tainted_size (sm, arg, has_bounds),
m_callee_fndecl (callee_fndecl),
m_size_argno (size_argno), m_access_str (access_str)
{
}
const char *get_kind () const override
{
return "tainted_access_attrib_size";
}
bool emit (rich_location *rich_loc) final override
{
bool warned = tainted_size::emit (rich_loc);
if (warned)
{
inform (DECL_SOURCE_LOCATION (m_callee_fndecl),
"parameter %i of %qD marked as a size via attribute %qs",
m_size_argno + 1, m_callee_fndecl, m_access_str);
}
return warned;
}
private:
tree m_callee_fndecl;
unsigned m_size_argno;
const char *m_access_str;
};
/* Concrete taint_diagnostic subclass for reporting attacker-controlled
divisor (so that an attacker can trigger a divide by zero). */
class tainted_divisor : public taint_diagnostic
{
public:
tainted_divisor (const taint_state_machine &sm, tree arg,
enum bounds has_bounds)
: taint_diagnostic (sm, arg, has_bounds)
{}
const char *get_kind () const final override { return "tainted_divisor"; }
int get_controlling_option () const final override
{
return OPT_Wanalyzer_tainted_divisor;
}
bool emit (rich_location *rich_loc) final override
{
diagnostic_metadata m;
/* CWE-369: "Divide By Zero". */
m.add_cwe (369);
if (m_arg)
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as divisor"
" without checking for zero",
m_arg);
else
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as divisor"
" without checking for zero");
}
label_text describe_final_event (const evdesc::final_event &ev) final override
{
if (m_arg)
return ev.formatted_print
("use of attacker-controlled value %qE as divisor"
" without checking for zero",
m_arg);
else
return ev.formatted_print
("use of attacker-controlled value as divisor"
" without checking for zero");
}
};
/* Concrete taint_diagnostic subclass for reporting attacker-controlled
size of a dynamic allocation. */
class tainted_allocation_size : public taint_diagnostic
{
public:
tainted_allocation_size (const taint_state_machine &sm, tree arg,
enum bounds has_bounds, enum memory_space mem_space)
: taint_diagnostic (sm, arg, has_bounds),
m_mem_space (mem_space)
{
}
const char *get_kind () const final override
{
return "tainted_allocation_size";
}
bool subclass_equal_p (const pending_diagnostic &base_other) const override
{
if (!taint_diagnostic::subclass_equal_p (base_other))
return false;
const tainted_allocation_size &other
= (const tainted_allocation_size &)base_other;
return m_mem_space == other.m_mem_space;
}
int get_controlling_option () const final override
{
return OPT_Wanalyzer_tainted_allocation_size;
}
bool emit (rich_location *rich_loc) final override
{
diagnostic_metadata m;
/* "CWE-789: Memory Allocation with Excessive Size Value". */
m.add_cwe (789);
bool warned;
if (m_arg)
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
warned = warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as"
" allocation size without bounds checking",
m_arg);
break;
case BOUNDS_UPPER:
warned = warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as"
" allocation size without"
" lower-bounds checking",
m_arg);
break;
case BOUNDS_LOWER:
warned = warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value %qE as"
" allocation size without"
" upper-bounds checking",
m_arg);
break;
}
else
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
warned = warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as"
" allocation size without bounds"
" checking");
break;
case BOUNDS_UPPER:
warned = warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as"
" allocation size without"
" lower-bounds checking");
break;
case BOUNDS_LOWER:
warned = warning_meta (rich_loc, m, get_controlling_option (),
"use of attacker-controlled value as"
" allocation size without"
" upper-bounds checking");
break;
}
if (warned)
{
location_t loc = rich_loc->get_loc ();
switch (m_mem_space)
{
default:
break;
case MEMSPACE_STACK:
inform (loc, "stack-based allocation");
break;
case MEMSPACE_HEAP:
inform (loc, "heap-based allocation");
break;
}
}
return warned;
}
label_text describe_final_event (const evdesc::final_event &ev) final override
{
if (m_arg)
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return ev.formatted_print
("use of attacker-controlled value %qE as allocation size"
" without bounds checking",
m_arg);
case BOUNDS_UPPER:
return ev.formatted_print
("use of attacker-controlled value %qE as allocation size"
" without lower-bounds checking",
m_arg);
case BOUNDS_LOWER:
return ev.formatted_print
("use of attacker-controlled value %qE as allocation size"
" without upper-bounds checking",
m_arg);
}
else
switch (m_has_bounds)
{
default:
gcc_unreachable ();
case BOUNDS_NONE:
return ev.formatted_print
("use of attacker-controlled value as allocation size"
" without bounds checking");
case BOUNDS_UPPER:
return ev.formatted_print
("use of attacker-controlled value as allocation size"
" without lower-bounds checking");
case BOUNDS_LOWER:
return ev.formatted_print
("use of attacker-controlled value as allocation size"
" without upper-bounds checking");
}
}
private:
enum memory_space m_mem_space;
};
/* Concrete taint_diagnostic subclass for reporting attacker-controlled
value being used as part of the condition of an assertion. */
class tainted_assertion : public taint_diagnostic
{
public:
tainted_assertion (const taint_state_machine &sm, tree arg,
tree assert_failure_fndecl)
: taint_diagnostic (sm, arg, BOUNDS_NONE),
m_assert_failure_fndecl (assert_failure_fndecl)
{
gcc_assert (m_assert_failure_fndecl);
}
const char *get_kind () const final override
{
return "tainted_assertion";
}
bool subclass_equal_p (const pending_diagnostic &base_other) const override
{
if (!taint_diagnostic::subclass_equal_p (base_other))
return false;
const tainted_assertion &other
= (const tainted_assertion &)base_other;
return m_assert_failure_fndecl == other.m_assert_failure_fndecl;
}
int get_controlling_option () const final override
{
return OPT_Wanalyzer_tainted_assertion;
}
bool emit (rich_location *rich_loc) final override
{
diagnostic_metadata m;
/* "CWE-617: Reachable Assertion". */
m.add_cwe (617);
return warning_meta (rich_loc, m, get_controlling_option (),
"use of attacked-controlled value in"
" condition for assertion");
}
location_t fixup_location (location_t loc,
bool primary) const final override
{
if (primary)
/* For the primary location we want to avoid being in e.g. the
<assert.h> system header, since this would suppress the
diagnostic. */
return expansion_point_location_if_in_system_header (loc);
else if (in_system_header_at (loc))
/* For events, we want to show the implemenation of the assert
macro when we're describing them. */
return linemap_resolve_location (line_table, loc,
LRK_SPELLING_LOCATION,
NULL);
else
return pending_diagnostic::fixup_location (loc, primary);
}
label_text describe_state_change (const evdesc::state_change &change) override
{
if (change.m_new_state == m_sm.m_tainted_control_flow)
return change.formatted_print
("use of attacker-controlled value for control flow");
return taint_diagnostic::describe_state_change (change);
}
label_text describe_final_event (const evdesc::final_event &ev) final override
{
if (mention_noreturn_attribute_p ())
return ev.formatted_print
("treating %qE as an assertion failure handler"
" due to %<__attribute__((__noreturn__))%>",
m_assert_failure_fndecl);
else
return ev.formatted_print
("treating %qE as an assertion failure handler",
m_assert_failure_fndecl);
}
private:
bool mention_noreturn_attribute_p () const
{
if (fndecl_built_in_p (m_assert_failure_fndecl, BUILT_IN_UNREACHABLE))
return false;
return true;
}
tree m_assert_failure_fndecl;
};
/* taint_state_machine's ctor. */
taint_state_machine::taint_state_machine (logger *logger)
: state_machine ("taint", logger)
{
m_tainted = add_state ("tainted");
m_has_lb = add_state ("has_lb");
m_has_ub = add_state ("has_ub");
m_stop = add_state ("stop");
m_tainted_control_flow = add_state ("tainted-control-flow");
}
state_machine::state_t
taint_state_machine::alt_get_inherited_state (const sm_state_map &map,
const svalue *sval,
const extrinsic_state &ext_state)
const
{
switch (sval->get_kind ())
{
default:
break;
case SK_UNARYOP:
{
const unaryop_svalue *unaryop_sval
= as_a <const unaryop_svalue *> (sval);
enum tree_code op = unaryop_sval->get_op ();
const svalue *arg = unaryop_sval->get_arg ();
switch (op)
{
case NOP_EXPR:
{
state_t arg_state = map.get_state (arg, ext_state);
return arg_state;
}
default:
break;
}
}
break;
case SK_BINOP:
{
const binop_svalue *binop_sval = as_a <const binop_svalue *> (sval);
enum tree_code op = binop_sval->get_op ();
const svalue *arg0 = binop_sval->get_arg0 ();
const svalue *arg1 = binop_sval->get_arg1 ();
switch (op)
{
default:
break;
case EQ_EXPR:
case GE_EXPR:
case LE_EXPR:
case NE_EXPR:
case GT_EXPR:
case LT_EXPR:
case UNORDERED_EXPR:
case ORDERED_EXPR:
case PLUS_EXPR:
case MINUS_EXPR:
case MULT_EXPR:
case POINTER_PLUS_EXPR:
case TRUNC_DIV_EXPR:
case TRUNC_MOD_EXPR:
{
state_t arg0_state = map.get_state (arg0, ext_state);
state_t arg1_state = map.get_state (arg1, ext_state);
return combine_states (arg0_state, arg1_state);
}
break;
case BIT_AND_EXPR:
case RSHIFT_EXPR:
return NULL;
}
}
break;
}
return NULL;
}
/* Return true iff FNDECL should be considered to be an assertion failure
handler by -Wanalyzer-tainted-assertion. */
static bool
is_assertion_failure_handler_p (tree fndecl)
{
// i.e. "noreturn"
if (TREE_THIS_VOLATILE (fndecl))
return true;
return false;
}
/* Implementation of state_machine::on_stmt vfunc for taint_state_machine. */
bool
taint_state_machine::on_stmt (sm_context *sm_ctxt,
const supernode *node,
const gimple *stmt) const
{
if (const gcall *call = dyn_cast <const gcall *> (stmt))
if (tree callee_fndecl = sm_ctxt->get_fndecl_for_call (call))
{
if (is_named_call_p (callee_fndecl, "fread", call, 4))
{
tree arg = gimple_call_arg (call, 0);
sm_ctxt->on_transition (node, stmt, arg, m_start, m_tainted);
/* Dereference an ADDR_EXPR. */
// TODO: should the engine do this?
if (TREE_CODE (arg) == ADDR_EXPR)
sm_ctxt->on_transition (node, stmt, TREE_OPERAND (arg, 0),
m_start, m_tainted);
return true;
}
/* External function with "access" attribute. */
if (sm_ctxt->unknown_side_effects_p ())
check_for_tainted_size_arg (sm_ctxt, node, call, callee_fndecl);
if (is_assertion_failure_handler_p (callee_fndecl)
&& sm_ctxt->get_global_state () == m_tainted_control_flow)
{
sm_ctxt->warn (node, call, NULL_TREE,
make_unique<tainted_assertion> (*this, NULL_TREE,
callee_fndecl));
}
}
// TODO: ...etc; many other sources of untrusted data
if (const gassign *assign = dyn_cast <const gassign *> (stmt))
{
enum tree_code op = gimple_assign_rhs_code (assign);
switch (op)
{
default:
break;
case TRUNC_DIV_EXPR:
case CEIL_DIV_EXPR:
case FLOOR_DIV_EXPR:
case ROUND_DIV_EXPR:
case TRUNC_MOD_EXPR:
case CEIL_MOD_EXPR:
case FLOOR_MOD_EXPR:
case ROUND_MOD_EXPR:
case RDIV_EXPR:
case EXACT_DIV_EXPR:
check_for_tainted_divisor (sm_ctxt, node, assign);
break;
}
}
if (const gcond *cond = dyn_cast <const gcond *> (stmt))
{
/* Reset the state of "tainted-control-flow" before each
control flow statement, so that only the last one before
an assertion-failure-handler counts. */
sm_ctxt->set_global_state (m_start);
check_control_flow_arg_for_taint (sm_ctxt, cond, gimple_cond_lhs (cond));
check_control_flow_arg_for_taint (sm_ctxt, cond, gimple_cond_rhs (cond));
}
if (const gswitch *switch_ = dyn_cast <const gswitch *> (stmt))
{
/* Reset the state of "tainted-control-flow" before each
control flow statement, so that only the last one before
an assertion-failure-handler counts. */
sm_ctxt->set_global_state (m_start);
check_control_flow_arg_for_taint (sm_ctxt, switch_,
gimple_switch_index (switch_));
}
return false;
}
/* If EXPR is tainted, mark this execution path with the
"tainted-control-flow" global state, in case we're about
to call an assertion-failure-handler. */
void
taint_state_machine::check_control_flow_arg_for_taint (sm_context *sm_ctxt,
const gimple *stmt,
tree expr) const
{
const region_model *old_model = sm_ctxt->get_old_region_model ();
const svalue *sval = old_model->get_rvalue (expr, NULL);
state_t state = sm_ctxt->get_state (stmt, sval);
enum bounds b;
if (get_taint (state, TREE_TYPE (expr), &b))
sm_ctxt->set_global_state (m_tainted_control_flow);
}
/* Implementation of state_machine::on_condition vfunc for taint_state_machine.
Potentially transition state 'tainted' to 'has_ub' or 'has_lb',
and states 'has_ub' and 'has_lb' to 'stop'. */
void
taint_state_machine::on_condition (sm_context *sm_ctxt,
const supernode *node,
const gimple *stmt,
const svalue *lhs,
enum tree_code op,
const svalue *rhs) const
{
if (stmt == NULL)
return;
// TODO
switch (op)
{
//case NE_EXPR:
//case EQ_EXPR:
case GE_EXPR:
case GT_EXPR:
{
/* (LHS >= RHS) or (LHS > RHS)
LHS gains a lower bound
RHS gains an upper bound. */
sm_ctxt->on_transition (node, stmt, lhs, m_tainted,
m_has_lb);
sm_ctxt->on_transition (node, stmt, lhs, m_has_ub,
m_stop);
sm_ctxt->on_transition (node, stmt, rhs, m_tainted,
m_has_ub);
sm_ctxt->on_transition (node, stmt, rhs, m_has_lb,
m_stop);
}
break;
case LE_EXPR:
case LT_EXPR:
{
/* Detect where build_range_check has optimized
(c>=low) && (c<=high)
into
(c-low>=0) && (c-low<=high-low)
and thus into:
(unsigned)(c - low) <= (unsigned)(high-low). */
if (const binop_svalue *binop_sval
= lhs->dyn_cast_binop_svalue ())
{
const svalue *inner_lhs = binop_sval->get_arg0 ();
enum tree_code inner_op = binop_sval->get_op ();
const svalue *inner_rhs = binop_sval->get_arg1 ();
if (const svalue *before_cast = inner_lhs->maybe_undo_cast ())
inner_lhs = before_cast;
if (tree outer_rhs_cst = rhs->maybe_get_constant ())
if (tree inner_rhs_cst = inner_rhs->maybe_get_constant ())
if (inner_op == PLUS_EXPR
&& TREE_CODE (inner_rhs_cst) == INTEGER_CST
&& TREE_CODE (outer_rhs_cst) == INTEGER_CST
&& TYPE_UNSIGNED (TREE_TYPE (inner_rhs_cst))
&& TYPE_UNSIGNED (TREE_TYPE (outer_rhs_cst)))
{
/* We have
(unsigned)(INNER_LHS + CST_A) </<= UNSIGNED_CST_B
and thus an optimized test of INNER_LHS (before any
cast to unsigned) against a range.
Transition any of the tainted states to the stop state.
We have to special-case this here rather than in
region_model::on_condition since we can't apply
both conditions simultaneously (we'd have a transition
from the old state to has_lb, then a transition from
the old state *again* to has_ub). */
state_t old_state
= sm_ctxt->get_state (stmt, inner_lhs);
if (old_state == m_tainted
|| old_state == m_has_lb
|| old_state == m_has_ub)
sm_ctxt->set_next_state (stmt, inner_lhs, m_stop);
return;
}
}
/* (LHS <= RHS) or (LHS < RHS)
LHS gains an upper bound
RHS gains a lower bound. */
sm_ctxt->on_transition (node, stmt, lhs, m_tainted,
m_has_ub);
sm_ctxt->on_transition (node, stmt, lhs, m_has_lb,
m_stop);
sm_ctxt->on_transition (node, stmt, rhs, m_tainted,
m_has_lb);
sm_ctxt->on_transition (node, stmt, rhs, m_has_ub,
m_stop);
}
break;
default:
break;
}
}
/* Implementation of state_machine::on_bounded_ranges vfunc for
taint_state_machine, for handling switch statement cases.
Potentially transition state 'tainted' to 'has_ub' or 'has_lb',
and states 'has_ub' and 'has_lb' to 'stop'. */
void
taint_state_machine::on_bounded_ranges (sm_context *sm_ctxt,
const supernode *,
const gimple *stmt,
const svalue &sval,
const bounded_ranges &ranges) const
{
gcc_assert (!ranges.empty_p ());
gcc_assert (ranges.get_count () > 0);
/* We have one or more ranges; this could be a "default:", or one or
more single or range cases.
Look at the overall endpoints to see if the ranges impose any lower
bounds or upper bounds beyond those of the underlying numeric type. */
tree lowest_bound = ranges.get_range (0).m_lower;
tree highest_bound = ranges.get_range (ranges.get_count () - 1).m_upper;
gcc_assert (lowest_bound);
gcc_assert (highest_bound);
bool ranges_have_lb
= (lowest_bound != TYPE_MIN_VALUE (TREE_TYPE (lowest_bound)));
bool ranges_have_ub
= (highest_bound != TYPE_MAX_VALUE (TREE_TYPE (highest_bound)));
if (!ranges_have_lb && !ranges_have_ub)
return;
/* We have new bounds from the ranges; combine them with any
existing bounds on SVAL. */
state_t old_state = sm_ctxt->get_state (stmt, &sval);
if (old_state == m_tainted)
{
if (ranges_have_lb && ranges_have_ub)
sm_ctxt->set_next_state (stmt, &sval, m_stop);
else if (ranges_have_lb)
sm_ctxt->set_next_state (stmt, &sval, m_has_lb);
else if (ranges_have_ub)
sm_ctxt->set_next_state (stmt, &sval, m_has_ub);
}
else if (old_state == m_has_ub && ranges_have_lb)
sm_ctxt->set_next_state (stmt, &sval, m_stop);
else if (old_state == m_has_lb && ranges_have_ub)
sm_ctxt->set_next_state (stmt, &sval, m_stop);
}
bool
taint_state_machine::can_purge_p (state_t s ATTRIBUTE_UNUSED) const
{
return true;
}
/* If STATE is a tainted state, write the bounds to *OUT and return true.
Otherwise return false.
Use the signedness of TYPE to determine if "has_ub" is tainted. */
bool
taint_state_machine::get_taint (state_t state, tree type,
enum bounds *out) const
{
/* Unsigned types have an implicit lower bound. */
bool is_unsigned = false;
if (type)
if (INTEGRAL_TYPE_P (type))
is_unsigned = TYPE_UNSIGNED (type);
/* Can't use a switch as the states are non-const. */
if (state == m_tainted)
{
*out = is_unsigned ? BOUNDS_LOWER : BOUNDS_NONE;
return true;
}
else if (state == m_has_lb)
{
*out = BOUNDS_LOWER;
return true;
}
else if (state == m_has_ub && !is_unsigned)
{
/* Missing lower bound. */
*out = BOUNDS_UPPER;
return true;
}
return false;
}
/* Find the most tainted state of S0 and S1. */
state_machine::state_t
taint_state_machine::combine_states (state_t s0, state_t s1) const
{
gcc_assert (s0);
gcc_assert (s1);
if (s0 == s1)
return s0;
if (s0 == m_tainted || s1 == m_tainted)
return m_tainted;
if (s0 == m_start)
return s1;
if (s1 == m_start)
return s0;
if (s0 == m_stop)
return s1;
if (s1 == m_stop)
return s0;
/* The only remaining combinations are one of has_ub and has_lb
(in either order). */
gcc_assert ((s0 == m_has_lb && s1 == m_has_ub)
|| (s0 == m_has_ub && s1 == m_has_lb));
return m_tainted;
}
/* Check for calls to external functions marked with
__attribute__((access)) with a size-index: complain about
tainted values passed as a size to such a function. */
void
taint_state_machine::check_for_tainted_size_arg (sm_context *sm_ctxt,
const supernode *node,
const gcall *call,
tree callee_fndecl) const
{
tree fntype = TREE_TYPE (callee_fndecl);
if (!fntype)
return;
if (!TYPE_ATTRIBUTES (fntype))
return;
/* Initialize a map of attribute access specifications for arguments
to the function call. */
rdwr_map rdwr_idx;
init_attr_rdwr_indices (&rdwr_idx, TYPE_ATTRIBUTES (fntype));
unsigned argno = 0;
for (tree iter = TYPE_ARG_TYPES (fntype); iter;
iter = TREE_CHAIN (iter), ++argno)
{
const attr_access* access = rdwr_idx.get (argno);
if (!access)
continue;
/* Ignore any duplicate entry in the map for the size argument. */
if (access->ptrarg != argno)
continue;
if (access->sizarg == UINT_MAX)
continue;
tree size_arg = gimple_call_arg (call, access->sizarg);
state_t state = sm_ctxt->get_state (call, size_arg);
enum bounds b;
if (get_taint (state, TREE_TYPE (size_arg), &b))
{
const char* const access_str =
TREE_STRING_POINTER (access->to_external_string ());
tree diag_size = sm_ctxt->get_diagnostic_tree (size_arg);
sm_ctxt->warn (node, call, size_arg,
make_unique<tainted_access_attrib_size>
(*this, diag_size, b,
callee_fndecl,
access->sizarg,
access_str));
}
}
}
/* Complain if ASSIGN (a division operation) has a tainted divisor
that could be zero. */
void
taint_state_machine::check_for_tainted_divisor (sm_context *sm_ctxt,
const supernode *node,
const gassign *assign) const
{
const region_model *old_model = sm_ctxt->get_old_region_model ();
if (!old_model)
return;
tree divisor_expr = gimple_assign_rhs2 (assign);;
const svalue *divisor_sval = old_model->get_rvalue (divisor_expr, NULL);
state_t state = sm_ctxt->get_state (assign, divisor_sval);
enum bounds b;
if (get_taint (state, TREE_TYPE (divisor_expr), &b))
{
const svalue *zero_sval
= old_model->get_manager ()->get_or_create_int_cst
(TREE_TYPE (divisor_expr), 0);
tristate ts
= old_model->eval_condition (divisor_sval, NE_EXPR, zero_sval);
if (ts.is_true ())
/* The divisor is known to not equal 0: don't warn. */
return;
tree diag_divisor = sm_ctxt->get_diagnostic_tree (divisor_expr);
sm_ctxt->warn (node, assign, divisor_expr,
make_unique <tainted_divisor> (*this, diag_divisor, b));
sm_ctxt->set_next_state (assign, divisor_sval, m_stop);
}
}
} // anonymous namespace
/* Internal interface to this file. */
state_machine *
make_taint_state_machine (logger *logger)
{
return new taint_state_machine (logger);
}
/* Complain to CTXT if accessing REG leads could lead to arbitrary
memory access under an attacker's control (due to taint). */
void
region_model::check_region_for_taint (const region *reg,
enum access_direction,
region_model_context *ctxt) const
{
gcc_assert (reg);
gcc_assert (ctxt);
LOG_SCOPE (ctxt->get_logger ());
sm_state_map *smap;
const state_machine *sm;
unsigned sm_idx;
if (!ctxt->get_taint_map (&smap, &sm, &sm_idx))
return;
gcc_assert (smap);
gcc_assert (sm);
const taint_state_machine &taint_sm = (const taint_state_machine &)*sm;
const extrinsic_state *ext_state = ctxt->get_ext_state ();
if (!ext_state)
return;
const region *iter_region = reg;
while (iter_region)
{
switch (iter_region->get_kind ())
{
default:
break;
case RK_ELEMENT:
{
const element_region *element_reg
= (const element_region *)iter_region;
const svalue *index = element_reg->get_index ();
const state_machine::state_t
state = smap->get_state (index, *ext_state);
gcc_assert (state);
enum bounds b;
if (taint_sm.get_taint (state, index->get_type (), &b))
{
tree arg = get_representative_tree (index);
ctxt->warn (make_unique<tainted_array_index> (taint_sm, arg, b));
}
}
break;
case RK_OFFSET:
{
const offset_region *offset_reg
= (const offset_region *)iter_region;
const svalue *offset = offset_reg->get_byte_offset ();
const state_machine::state_t
state = smap->get_state (offset, *ext_state);
gcc_assert (state);
/* Handle implicit cast to sizetype. */
tree effective_type = offset->get_type ();
if (const svalue *cast = offset->maybe_undo_cast ())
if (cast->get_type ())
effective_type = cast->get_type ();
enum bounds b;
if (taint_sm.get_taint (state, effective_type, &b))
{
tree arg = get_representative_tree (offset);
ctxt->warn (make_unique<tainted_offset> (taint_sm, arg, b));
}
}
break;
case RK_CAST:
{
const cast_region *cast_reg
= as_a <const cast_region *> (iter_region);
iter_region = cast_reg->get_original_region ();
continue;
}
case RK_SIZED:
{
const sized_region *sized_reg
= (const sized_region *)iter_region;
const svalue *size_sval = sized_reg->get_byte_size_sval (m_mgr);
const state_machine::state_t
state = smap->get_state (size_sval, *ext_state);
gcc_assert (state);
enum bounds b;
if (taint_sm.get_taint (state, size_sval->get_type (), &b))
{
tree arg = get_representative_tree (size_sval);
ctxt->warn (make_unique<tainted_size> (taint_sm, arg, b));
}
}
break;
}
iter_region = iter_region->get_parent_region ();
}
}
/* Complain to CTXT about a tainted allocation size if SIZE_IN_BYTES is
under an attacker's control (due to taint), where the allocation
is happening within MEM_SPACE. */
void
region_model::check_dynamic_size_for_taint (enum memory_space mem_space,
const svalue *size_in_bytes,
region_model_context *ctxt) const
{
gcc_assert (size_in_bytes);
gcc_assert (ctxt);
LOG_SCOPE (ctxt->get_logger ());
sm_state_map *smap;
const state_machine *sm;
unsigned sm_idx;
if (!ctxt->get_taint_map (&smap, &sm, &sm_idx))
return;
gcc_assert (smap);
gcc_assert (sm);
const taint_state_machine &taint_sm = (const taint_state_machine &)*sm;
const extrinsic_state *ext_state = ctxt->get_ext_state ();
if (!ext_state)
return;
const state_machine::state_t
state = smap->get_state (size_in_bytes, *ext_state);
gcc_assert (state);
enum bounds b;
if (taint_sm.get_taint (state, size_in_bytes->get_type (), &b))
{
tree arg = get_representative_tree (size_in_bytes);
ctxt->warn (make_unique<tainted_allocation_size>
(taint_sm, arg, b, mem_space));
}
}
/* Mark SVAL as TAINTED. CTXT must be non-NULL. */
void
region_model::mark_as_tainted (const svalue *sval,
region_model_context *ctxt)
{
gcc_assert (sval);
gcc_assert (ctxt);
sm_state_map *smap;
const state_machine *sm;
unsigned sm_idx;
if (!ctxt->get_taint_map (&smap, &sm, &sm_idx))
return;
gcc_assert (smap);
gcc_assert (sm);
const taint_state_machine &taint_sm = (const taint_state_machine &)*sm;
const extrinsic_state *ext_state = ctxt->get_ext_state ();
if (!ext_state)
return;
smap->set_state (this, sval, taint_sm.m_tainted, NULL, *ext_state);
}
} // namespace ana
#endif /* #if ENABLE_ANALYZER */