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

Reply via email to