On Thu, 22 Oct 2026 20:23:48 +0800
Wei Chen <[email protected]> wrote:

> The _strip() function passes a char directly to isspace(), which is
> undefined behavior. The ctype functions require an int argument that is
> either an unsigned char value or EOF, but char is signed by default on
> x86 and ARM, so any byte >= 0x80 in the input is promoted to a negative
> int and indexes the ctype table out of bounds.
> 
> Such bytes are reachable from a user-provided config file containing
> UTF-8 or other non-ASCII content.
> 
> Cast the argument to unsigned char, as is already done in
> app/test-bbdev/test_bbdev_vector.c and elsewhere.
> 
> Fixes: eaafbad419bf ("cfgfile: library to interpret config files")
> Cc: [email protected]
> 
> Signed-off-by: Wei Chen <[email protected]>
> ---

Looks good, there seems to be lots of places with same issue.
Perhaps putting helpers in rte_string_fns.h which handle the
common ctype case and use comparisons to avoid pitfalls.

   static inline bool
   rte_isspace(char c)
   {
        return c == ' ' || c == '\t' || c == '\n' ||
               c == '\r' || c == '\f' || c == '\v';
   }

Quick AI doodle.

From 653b2204ef72a45e460ba557c6b765b1965154c5 Mon Sep 17 00:00:00 2001
From: Stephen Hemminger <[email protected]>
Date: Thu, 22 Oct 2026 08:52:58 -0700
Subject: [PATCH] eal: add ASCII character classification functions

The <ctype.h> functions take an int that must be EOF or an
unsigned char value. Passing a plain char is undefined behavior
for bytes >= 0x80 where char is signed. Most of the ctype calls
in the tree do exactly that, including rte_str_skip_leading_spaces().

The ctype functions are also locale dependent, which is not
wanted when parsing devargs, config files and addresses.

Add rte_isspace(), rte_isdigit(), rte_tolower() and friends.
They take a char, use ASCII only, and never match bytes >= 0x80.
Use rte_isspace() in rte_str_skip_leading_spaces().

Signed-off-by: Stephen Hemminger <[email protected]>
---
 app/test/test_string_fns.c             | 44 +++++++++++++
 doc/guides/rel_notes/release_26_11.rst |  6 ++
 lib/eal/include/rte_string_fns.h       | 89 +++++++++++++++++++++++++-
 3 files changed, 138 insertions(+), 1 deletion(-)

diff --git a/app/test/test_string_fns.c b/app/test/test_string_fns.c
index 213a9312ea..ba6b6fa8b2 100644
--- a/app/test/test_string_fns.c
+++ b/app/test/test_string_fns.c
@@ -2,6 +2,7 @@
  * Copyright(c) 2010-2014 Intel Corporation
  */
 
+#include <ctype.h>
 #include <stdio.h>
 #include <stdarg.h>
 #include <stddef.h>
@@ -378,6 +379,47 @@ test_rte_str_to_size(void)
        return 0;
 }
 
+static int
+test_rte_ctype(void)
+{
+       unsigned int i;
+
+       /* Must match <ctype.h> in the C locale for 7-bit ASCII */
+       for (i = 0; i < 0x80; i++) {
+               char c = (char)i;
+
+               if (rte_isspace(c) != !!isspace(i) ||
+                   rte_isblank(c) != !!isblank(i) ||
+                   rte_isdigit(c) != !!isdigit(i) ||
+                   rte_isupper(c) != !!isupper(i) ||
+                   rte_islower(c) != !!islower(i) ||
+                   rte_isalpha(c) != !!isalpha(i) ||
+                   rte_isalnum(c) != !!isalnum(i) ||
+                   rte_isxdigit(c) != !!isxdigit(i) ||
+                   rte_isprint(c) != !!isprint(i) ||
+                   rte_tolower(c) != (char)tolower(i) ||
+                   rte_toupper(c) != (char)toupper(i)) {
+                       LOG("FAIL: mismatch with ctype for 0x%02x\n", i);
+                       return -1;
+               }
+       }
+
+       /* No byte outside 7-bit ASCII matches any class */
+       for (i = 0x80; i < 0x100; i++) {
+               char c = (char)i;
+
+               if (rte_isspace(c) || rte_isblank(c) || rte_isdigit(c) ||
+                   rte_isalpha(c) || rte_isxdigit(c) || rte_isprint(c) ||
+                   rte_tolower(c) != c || rte_toupper(c) != c) {
+                       LOG("FAIL: non-ASCII byte 0x%02x matched\n", i);
+                       return -1;
+               }
+       }
+
+       LOG("%s - PASSED\n", __func__);
+       return 0;
+}
+
 static int
 test_string_fns(void)
 {
@@ -391,6 +433,8 @@ test_string_fns(void)
                return -1;
        if (test_rte_str_to_size() < 0)
                return -1;
+       if (test_rte_ctype() < 0)
+               return -1;
        return 0;
 }
 
diff --git a/doc/guides/rel_notes/release_26_11.rst 
b/doc/guides/rel_notes/release_26_11.rst
index be758a0c02..c6ccf6ae2d 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst
@@ -60,6 +60,12 @@ New Features
   Added the ``RTE_DIV_ROUND_UP`` macro in ``rte_common.h``
   to divide dividend by divisor, rounding up to the nearest integer.
 
+* **Added ASCII character classification functions.**
+
+  Added ``rte_isspace()``, ``rte_isdigit()``, ``rte_tolower()`` and related
+  functions in ``rte_string_fns.h``. They take a ``char``, ignore locale,
+  and are safe for bytes >= 0x80, unlike the ``<ctype.h>`` functions.
+
 * **Improved pointer arithmetic macros.**
 
   Updated  the macros ``RTE_PTR_ADD``, ``RTE_PTR_SUB``,
diff --git a/lib/eal/include/rte_string_fns.h b/lib/eal/include/rte_string_fns.h
index b3b5bdf275..1e3b2df9a1 100644
--- a/lib/eal/include/rte_string_fns.h
+++ b/lib/eal/include/rte_string_fns.h
@@ -12,6 +12,7 @@
 #define _RTE_STRING_FNS_H_
 
 #include <ctype.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <string.h>
 
@@ -115,6 +116,92 @@ extern "C" {
 ssize_t
 rte_strscpy(char *dst, const char *src, size_t dsize);
 
+/*
+ * ASCII character classification.
+ *
+ * These are locale independent replacements for the <ctype.h> functions.
+ * Unlike the standard versions they take a char, so they are safe to call
+ * with any byte from a string, including values >= 0x80 on targets where
+ * char is signed. Characters outside the 7-bit ASCII range never match.
+ */
+
+/** Return true if c is ' ', '\t', '\n', '\v', '\f' or '\r'. */
+static inline bool
+rte_isspace(char c)
+{
+       return c == ' ' || (c >= '\t' && c <= '\r');
+}
+
+/** Return true if c is ' ' or '\t'. */
+static inline bool
+rte_isblank(char c)
+{
+       return c == ' ' || c == '\t';
+}
+
+/** Return true if c is a decimal digit. */
+static inline bool
+rte_isdigit(char c)
+{
+       return c >= '0' && c <= '9';
+}
+
+/** Return true if c is an ASCII uppercase letter. */
+static inline bool
+rte_isupper(char c)
+{
+       return c >= 'A' && c <= 'Z';
+}
+
+/** Return true if c is an ASCII lowercase letter. */
+static inline bool
+rte_islower(char c)
+{
+       return c >= 'a' && c <= 'z';
+}
+
+/** Return true if c is an ASCII letter. */
+static inline bool
+rte_isalpha(char c)
+{
+       return rte_isupper(c) || rte_islower(c);
+}
+
+/** Return true if c is an ASCII letter or decimal digit. */
+static inline bool
+rte_isalnum(char c)
+{
+       return rte_isalpha(c) || rte_isdigit(c);
+}
+
+/** Return true if c is a hexadecimal digit. */
+static inline bool
+rte_isxdigit(char c)
+{
+       return rte_isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 
'F');
+}
+
+/** Return true if c is a printable ASCII character, including space. */
+static inline bool
+rte_isprint(char c)
+{
+       return c >= ' ' && c <= '~';
+}
+
+/** Convert an ASCII uppercase letter to lowercase, else return c. */
+static inline char
+rte_tolower(char c)
+{
+       return rte_isupper(c) ? c - 'A' + 'a' : c;
+}
+
+/** Convert an ASCII lowercase letter to uppercase, else return c. */
+static inline char
+rte_toupper(char c)
+{
+       return rte_islower(c) ? c - 'a' + 'A' : c;
+}
+
 /**
  * @warning
  * @b EXPERIMENTAL: this API may change without prior notice.
@@ -133,7 +220,7 @@ rte_str_skip_leading_spaces(const char *src)
 {
        const char *p = src;
 
-       while (isspace(*p))
+       while (rte_isspace(*p))
                p++;
 
        return p;
-- 
2.43.0

Reply via email to