This patch implements known_function handling for strnlen and
__builtin_strnlen within the analyzer, it uses bifurcation logic
for truncated and full read conditions.

gcc/analyzer/ChangeLog:
        * kf.cc (class kf_strnlen): New known_function subclass.
        (register_known_functions_libc): Register strnlen and
        __builtin_strnlen.

gcc/testsuite/ChangeLog:
        * gcc.dg/analyzer/strnlen-1.c: New test.

Signed-off-by: Saish Sandip Kambali <[email protected]>
---
 gcc/analyzer/kf.cc                        | 191 ++++++++++++++++++++++
 gcc/testsuite/gcc.dg/analyzer/strnlen-1.c | 121 ++++++++++++++
 2 files changed, 312 insertions(+)
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/strnlen-1.c

diff --git a/gcc/analyzer/kf.cc b/gcc/analyzer/kf.cc
index b1ccbd6584a..a292e220c1e 100644
--- a/gcc/analyzer/kf.cc
+++ b/gcc/analyzer/kf.cc
@@ -1710,6 +1710,194 @@ 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_pre (const call_details &cd) const final override;
+  void impl_call_post (const call_details &cd) const final override;
+};
+
+void
+kf_strnlen::impl_call_pre (const call_details &cd) const
+{
+  cd.check_for_null_terminated_string_arg (0);
+}
+
+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));
+
+      /* Arguments 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;
+
+      /* Truncated read.  */
+      if (m_truncated_read)
+        {
+         result_sval = limit_n_sval;
+
+         if (m_num_bytes_with_terminator_sval)
+           {
+             /* The null-terminator is not in the bounds of n.  */
+             if (!model->add_constraint (m_num_bytes_with_terminator_sval,
+                                         GT_EXPR, limit_n_sval, ctxt))
+               return false;
+           }
+         else
+           {
+           }
+        }
+      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;
+           }
+         else
+           {
+             /* This bifurcation part is removed once failed.  */
+             return false;
+           }
+        }
+
+      /* Computing return value only if lhs is present.  */
+      if (tree lhs_type = cd.get_lhs_type ())
+        {
+         if (!m_truncated_read && m_num_bytes_with_terminator_sval)
+           {
+             /* 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)));
+           }
+
+         /* This sets the return value, if the lhs type is decalred.  */
+         if (result_sval)
+           {
+             cd.maybe_set_lhs (result_sval);
+           }
+         else
+           {
+           }
+        }
+
+      /* This checks for buffer over read, and poison bytes as a safety 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 '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 +2525,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 +2592,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..78cfe0c0629
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/strnlen-1.c
@@ -0,0 +1,121 @@
+#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_1 (void)
+{
+  __builtin_strnlen (NULL, 1); /* { dg-warning "use of NULL where non-null 
expected" } */
+}
+
+void test_null_0 (void)
+{
+  /* 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" } 
*/   
+}
+
+/* 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" } */
+}
+
+/* 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); /* { dg-warning "stack-based buffer 
over-read" } */
+}
+
+/* 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);
+}
+
+/* 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" 
} */
+}
-- 
2.43.0

Reply via email to