This patch adds modeling for strnlen and __builtin_strnlen to the analyzer.
It handles bifurcation for when the null terminator is found and when the
limit 'n' is reached.
Tested on x86_64-pc-linux-gnu.
gcc/analyzer/ChangeLog:
* kf.cc (class kf_strnlen): New class.
(kf_strnlen::impl_call_post): New function.
(register_known_functions): Register kf_strnlen.
gcc/testsuite/ChangeLog:
* gcc.dg/analyzer/strnlen-1.c: New test.
Signed-off-by: Saish Kambali <[email protected]>
---
gcc/analyzer/kf.cc | 182
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
gcc/testsuite/gcc.dg/analyzer/strnlen-1.c | 127
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 309 insertions(+)
diff --git a/gcc/analyzer/kf.cc b/gcc/analyzer/kf.cc
index b1ccbd6584a..446e8198fec 100644
--- a/gcc/analyzer/kf.cc
+++ b/gcc/analyzer/kf.cc
@@ -1710,6 +1710,185 @@ public:
}
};
+
+/* Handles strnlen by splitting into two outcomes:
+ a) Truncated read: The limit 'n' is reached before the null-terminator,
+ in this case the result is 'n'.
+ b) Full read: The null-terminator is found before or at the limit n,
+ in this case the result is the number of bytes read before the
+ null-terminator. */
+
+class kf_strnlen : public builtin_known_function
+{
+public:
+ bool matches_call_types_p (const call_details &cd) const final override
+ {
+ return (cd.num_args () == 2
+ && cd.arg_is_pointer_p (0)
+ && cd.arg_is_integral_p (1));
+ }
+
+ enum built_in_function builtin_code () const final override
+ {
+ return BUILT_IN_STRNLEN;
+ }
+
+ void impl_call_post (const call_details &cd) const final override;
+};
+
+void
+kf_strnlen::impl_call_post (const call_details &cd) const
+{
+ class strnlen_call_info : public call_info
+ {
+ public:
+ strnlen_call_info (const call_details &cd,
+ const svalue *num_bytes_with_terminator_sval,
+ bool truncated_read)
+ : call_info (cd),
+ m_num_bytes_with_terminator_sval (num_bytes_with_terminator_sval),
+ m_truncated_read (truncated_read)
+ {
+ }
+
+ void print_desc (pretty_printer &pp) const final override
+ {
+ if (m_truncated_read)
+ pp_printf (&pp, "when %qE reaches the maximum limit", get_fndecl ());
+ else
+ pp_printf (&pp, "when %qE finds the null terminator", get_fndecl ());
+ }
+
+ bool update_model (region_model *model,
+ const exploded_edge *,
+ region_model_context *ctxt) const final override
+ {
+ const call_details cd (get_call_details (model, ctxt));
+
+ /* Argument at index '0' is the string pointer. */
+ const svalue *src_sval = cd.get_arg_svalue (0);
+ const region *src_reg
+ = model->deref_rvalue (src_sval, cd.get_arg_tree (0), ctxt);
+
+ /* Argument 1 is the limit n. */
+ const svalue *limit_n_sval = cd.get_arg_svalue (1);
+
+ /* Symbolic values for return value of the call. */
+ const svalue *result_sval = nullptr;
+ const svalue *bytes_to_check = nullptr;
+
+ if (tree lhs_type = cd.get_lhs_type ())
+ {
+ /* Limit n reached before finding null-terminator. */
+ if (m_truncated_read)
+ {
+ result_sval = limit_n_sval;
+
+ /* False for poisoned(uninitialized) values. */
+ if (m_num_bytes_with_terminator_sval)
+ {
+ /* The null-terminator is not in bounds of n. */
+ if (!model->add_constraint (m_num_bytes_with_terminator_sval,
+ GT_EXPR, limit_n_sval, ctxt))
+ return false;
+ }
+ else
+ {
+ }
+ }
+
+ /* Found the null-terminator within or on the limit 'n'. */
+ else
+ {
+ if (m_num_bytes_with_terminator_sval)
+ {
+ /* The string length is less than or equal to n. */
+ if (!model->add_constraint (m_num_bytes_with_terminator_sval,
+ LE_EXPR, limit_n_sval, ctxt))
+ return false;
+
+ /* Since the strnlen() returns the count of bytes
+ excluding \0. */
+ result_sval = model->get_manager ()->get_or_create_binop (
+ lhs_type, MINUS_EXPR, m_num_bytes_with_terminator_sval,
+ model->get_manager ()->get_or_create_constant_svalue (lhs_type,
build_int_cst (lhs_type, 1)));
+ }
+ else
+ {
+ /* This bifurcation part is removed once failed. */
+ return false;
+ }
+ }
+ /* This sets the return value, if the lhs type is declared. */
+ if (result_sval)
+ {
+ cd.maybe_set_lhs (result_sval);
+ }
+ else
+ {
+ }
+ }
+
+ /* This checks for buffer over read, and poison bytes as a safety
check
+ check for both the lhs data type for null and non-null. */
+ bytes_to_check = m_truncated_read ? limit_n_sval :
m_num_bytes_with_terminator_sval;
+
+ if (bytes_to_check)
+ {
+ model->read_bytes (src_reg, cd.get_arg_tree (0), bytes_to_check, ctxt);
+ }
+
+ return true;
+ }
+
+ private:
+ /* This symbolic value representing the number of bytes read upto the
+ first non-terminating character, it will be a null pointer. */
+ const svalue *m_num_bytes_with_terminator_sval;
+
+ /* If true: We are simulating the 'truncated read'
+ If false: We are simulating the 'full read'. */
+ bool m_truncated_read;
+ };
+
+ const svalue *limit_n_sval = cd.get_arg_svalue (1);
+
+ if (tree n_cst = limit_n_sval->maybe_get_constant ())
+ {
+ if (zerop (n_cst))
+ {
+ cd.maybe_set_lhs (limit_n_sval);
+ return;
+ }
+ }
+
+ /* Body of kf_strnlen::impl_call_post. */
+ if (cd.get_ctxt ())
+ {
+ /* First, scan for a null terminator as if there were no limit,
+ with a null ctxt so no errors reported. */
+ const region_model *model = cd.get_model ();
+ const svalue *ptr_arg_sval = cd.get_arg_svalue (0);
+ const region *buf_reg
+ = model->deref_rvalue (ptr_arg_sval, cd.get_arg_tree (0), nullptr);
+
+ const svalue *num_bytes_with_terminator_sval
+ = model->scan_for_null_terminator (buf_reg, cd.get_arg_tree (0),
+ nullptr, nullptr);
+
+ cd.get_ctxt ()->bifurcate (
+ std::make_unique<strnlen_call_info> (cd,
+ num_bytes_with_terminator_sval,
+ false));
+ cd.get_ctxt ()->bifurcate (
+ std::make_unique<strnlen_call_info> (cd,
+ num_bytes_with_terminator_sval,
+ true));
+ cd.get_ctxt ()->terminate_path ();
+ }
+}
+
+
/* Handler for "strstr" and "__builtin_strstr".
extern char *strstr (const char* str, const char* substr);
See e.g. https://en.cppreference.com/w/c/string/byte/strstr */
@@ -2337,6 +2516,8 @@ register_known_functions (known_function_manager &kfm,
kfm.add ("__builtin_strncpy", std::make_unique<kf_strncpy> ());
kfm.add ("strndup", std::make_unique<kf_strndup> ());
kfm.add ("__builtin_strndup", std::make_unique<kf_strndup> ());
+ kfm.add ("strnlen", std::make_unique<kf_strnlen> ());
+ kfm.add ("__builtin_strnlen", std::make_unique<kf_strnlen> ());
kfm.add ("strlen", std::make_unique<kf_strlen> ());
kfm.add ("__builtin_strlen", std::make_unique<kf_strlen> ());
kfm.add ("strstr", std::make_unique<kf_strstr> ());
@@ -2402,6 +2583,7 @@ register_known_functions (known_function_manager &kfm,
kfm.add_std_ns ("strcat", std::make_unique<kf_strcat> (2, false));
kfm.add_std_ns ("strcpy", std::make_unique<kf_strcpy> (2, false));
kfm.add_std_ns ("strlen", std::make_unique<kf_strlen> ());
+ kfm.add_std_ns ("strnlen", std::make_unique<kf_strnlen> ());
kfm.add_std_ns ("strncpy", std::make_unique<kf_strncpy> ());
kfm.add_std_ns ("strtok", std::make_unique<kf_strtok> (rmm));
}
diff --git a/gcc/testsuite/gcc.dg/analyzer/strnlen-1.c
b/gcc/testsuite/gcc.dg/analyzer/strnlen-1.c
new file mode 100644
index 00000000000..a07c5313f75
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/strnlen-1.c
@@ -0,0 +1,127 @@
+#include "analyzer-decls.h"
+
+typedef __SIZE_TYPE__ size_t;
+
+/* noinline wrapper of test_string. */
+static size_t __attribute__((noinline))
+call_strnlen_1 (const char *p, size_t n)
+{
+ return __builtin_strnlen (p, n);
+}
+
+void test_string (void)
+{
+ /* case 1: n is greater than string length */
+ __analyzer_eval (call_strnlen_1 ("abc", 10) == 3); /* { dg-warning
"TRUE" } */
+
+ /* case 2: n is smaller than the string length */
+ __analyzer_eval (call_strnlen_1 ("abc", 2) == 2); /* { dg-warning "TRUE"
} */
+
+ /* case 3: n is exactly the string length */
+ __analyzer_eval (call_strnlen_1 ("abc", 3) == 3); /* { dg-warning "TRUE"
} */
+
+ /* case 4: n is zero and should return 0 immediately. */
+ __analyzer_eval (call_strnlen_1 ("abc", 0) == 0); /* { dg-warning "TRUE"
} */
+}
+
+/* checks if pointer is NULL */
+void test_null (size_t n)
+{
+ if (n > 0)
+ {
+ __builtin_strnlen (NULL, n); /* { dg-warning "use of NULL string
pointer" } */
+ /* { dg-message "argument 1 NULL where non-null expected" "event" {
target *-*-* } .-1 } */
+ }
+ else
+ {
+ /* n == 0 should not trigger a NULL warning in strnlen. */
+ __analyzer_eval (__builtin_strnlen (NULL, 0) == 0); /* { dg-warning
"TRUE" } */
+ }
+}
+
+/* buffer declared but not initialized yet, warning if read uninitialized
values */
+void test_uninitialized (size_t n)
+{
+ char buf[16];
+ if(n > 0)
+ {
+ __builtin_strnlen (buf, n); /* { dg-warning "use of uninitialized
value '&buf'" } */
+ }
+}
+
+/* buffer is declared and partially initialized, warning if read
uninitialized values */
+void test_partially_initialized (void)
+{
+ char buf[16];
+ buf[0] = 'a';
+ __builtin_strnlen (buf, 2); /* { dg-warning "use of uninitialized value
'buf\\\[1\\\]'" } */
+}
+
+/* noinline wrapper of test_unterminated case 1 */
+static size_t __attribute__((noinline))
+call_strnlen_2 (const char *p, size_t n)
+{
+ return __builtin_strnlen (p, n);
+}
+
+/* noinline wrapper of test_unterminated case 2 */
+static size_t __attribute__((noinline))
+call_strnlen_3 (const char *p, size_t n)
+{
+ return __builtin_strnlen (p, n); /* { dg-warning "stack-based buffer
over-read" } */
+}
+
+void test_unterminated (void)
+{
+ const char buf[3] = {'a', 'b', 'c'};
+
+ /* case 1: SAFE as strnlen stops at n = 3, so never looked for \0 */
+ size_t result = call_strnlen_2 (buf, 3);
+ __analyzer_eval (result == 3); /* { dg-warning "TRUE" } */
+
+ /* case 2: Unsafe as n=4 forces the analyzer to look past the 3 bytes
buffer */
+ call_strnlen_3 (buf, 4);
+ /* { dg-message "while looking for null terminator for argument 1
\\(&buf\\) of 'strnlen'" "event" { target *-*-* } .-1 } */
+}
+
+/* noinline wrapper of test_max_n */
+static size_t __attribute__((noinline))
+call_strnlen_4 (const char *p, size_t n)
+{
+ return __builtin_strnlen (p, n);
+}
+
+/* for abstraction, assures the length l < n */
+void test_abstract (const char *s, size_t n)
+{
+ if (s == NULL)
+ return;
+ size_t length = call_strnlen_4 (s, n);
+
+ __analyzer_eval (length <= n); /* { dg-warning "UNKNOWN" } */
+}
+
+/* noinline wrapper of test_array_initialization, case: true */
+static size_t __attribute__((noinline))
+call_strnlen_5 (const char *p, size_t n)
+{
+ return __builtin_strnlen (p, n);
+}
+
+/* noinline wrapper of test_array_initialized, case: unknown */
+static size_t __attribute__((noinline))
+call_strnlen_5a (const char *p, size_t n)
+{
+ return __builtin_strnlen (p, n); /* { dg-warning "stack-based buffer
over-read" } */
+}
+
+/* pointer arithmetic can be performed and warnings for over reading
buffer */
+void test_array_initialization_implicit_length (void)
+{
+ const char buf[] = "abc";
+
+ __analyzer_eval (call_strnlen_5 (buf, 10) == 3); /* { dg-warning "TRUE"
} */
+ __analyzer_eval (call_strnlen_5 (buf + 2, 3) == 1); /* { dg-warning
"TRUE" } */
+ __analyzer_eval (call_strnlen_5 (buf + 3, 3) == 0); /* { dg-warning
"TRUE" } */
+ __analyzer_eval (call_strnlen_5a (buf + 4, 3) == 0); /* { dg-warning
"FALSE" } */
+}
\ No newline at end of file