* [PATCH] cfgfile: fix isspace() undefined behavior
@ 2026-10-22 12:05 Wei Chen
0 siblings, 0 replies; 4+ messages in thread
From: Wei Chen @ 2026-10-22 12:05 UTC (permalink / raw)
To: dev; +Cc: cristian.dumitrescu, stable
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: stable@dpdk.org
Signed-off-by: Wei Chen <allen.chen@jaguarmicro.com>
---
lib/cfgfile/rte_cfgfile.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lib/cfgfile/rte_cfgfile.c b/lib/cfgfile/rte_cfgfile.c
index 6edd83b5b3..958cec6672 100644
--- a/lib/cfgfile/rte_cfgfile.c
+++ b/lib/cfgfile/rte_cfgfile.c
@@ -97,16 +97,16 @@ _strip(char *str, unsigned len)
if (len == 0)
return 0;
- if (isspace(str[len-1])) {
+ if (isspace((unsigned char)str[len-1])) {
/* strip trailing whitespace */
- while (newlen > 0 && isspace(str[newlen - 1]))
+ while (newlen > 0 && isspace((unsigned char)str[newlen - 1]))
str[--newlen] = '\0';
}
- if (isspace(str[0])) {
+ if (isspace((unsigned char)str[0])) {
/* strip leading whitespace */
int i, start = 1;
- while (isspace(str[start]) && start < newlen)
+ while (isspace((unsigned char)str[start]) && start < newlen)
start++
; /* do nothing */
newlen -= start;
--
2.33.0.windows.2
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH] cfgfile: fix isspace() undefined behavior
@ 2026-10-22 12:23 Wei Chen
2026-10-22 15:54 ` Stephen Hemminger
0 siblings, 1 reply; 4+ messages in thread
From: Wei Chen @ 2026-10-22 12:23 UTC (permalink / raw)
To: dev; +Cc: cristian.dumitrescu, stable
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: stable@dpdk.org
Signed-off-by: Wei Chen <allen.chen@jaguarmicro.com>
---
lib/cfgfile/rte_cfgfile.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lib/cfgfile/rte_cfgfile.c b/lib/cfgfile/rte_cfgfile.c
index 6edd83b5b3..958cec6672 100644
--- a/lib/cfgfile/rte_cfgfile.c
+++ b/lib/cfgfile/rte_cfgfile.c
@@ -97,16 +97,16 @@ _strip(char *str, unsigned len)
if (len == 0)
return 0;
- if (isspace(str[len-1])) {
+ if (isspace((unsigned char)str[len-1])) {
/* strip trailing whitespace */
- while (newlen > 0 && isspace(str[newlen - 1]))
+ while (newlen > 0 && isspace((unsigned char)str[newlen - 1]))
str[--newlen] = '\0';
}
- if (isspace(str[0])) {
+ if (isspace((unsigned char)str[0])) {
/* strip leading whitespace */
int i, start = 1;
- while (isspace(str[start]) && start < newlen)
+ while (isspace((unsigned char)str[start]) && start < newlen)
start++
; /* do nothing */
newlen -= start;
--
2.33.0.windows.2
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH] cfgfile: fix isspace() undefined behavior
2026-10-22 12:23 [PATCH] cfgfile: fix isspace() undefined behavior Wei Chen
@ 2026-10-22 15:54 ` Stephen Hemminger
2026-10-22 16:35 ` Bruce Richardson
0 siblings, 1 reply; 4+ messages in thread
From: Stephen Hemminger @ 2026-10-22 15:54 UTC (permalink / raw)
To: Wei Chen; +Cc: dev, cristian.dumitrescu, stable
On Thu, 22 Oct 2026 20:23:48 +0800
Wei Chen <allen.chen@jaguarmicro.com> 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: stable@dpdk.org
>
> Signed-off-by: Wei Chen <allen.chen@jaguarmicro.com>
> ---
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 <stephen@networkplumber.org>
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 <stephen@networkplumber.org>
---
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
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH] cfgfile: fix isspace() undefined behavior
2026-10-22 15:54 ` Stephen Hemminger
@ 2026-10-22 16:35 ` Bruce Richardson
0 siblings, 0 replies; 4+ messages in thread
From: Bruce Richardson @ 2026-10-22 16:35 UTC (permalink / raw)
To: Stephen Hemminger; +Cc: Wei Chen, dev, cristian.dumitrescu, stable
On Thu, Oct 22, 2026 at 08:54:11AM -0700, Stephen Hemminger wrote:
> On Thu, 22 Oct 2026 20:23:48 +0800
> Wei Chen <allen.chen@jaguarmicro.com> 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: stable@dpdk.org
> >
> > Signed-off-by: Wei Chen <allen.chen@jaguarmicro.com>
> > ---
>
> 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.
>
Not fully convinced we need a DPDK wrapper for this. If we do, I suggest
simplifying to just being a casting wrapper:
static inline bool
rte_isspace(char c) { return isspace((unsigned char)c); }
Rather than a wrapper, I'd prefer some sort of static check for this, if
such a thing is possible in checkpatches or similar. Better to teach how to
use the standard API properly than try and hide it. IMHO rte_ prefixed-
wrappers/replacements for standard functions should only be used in cases
where the standard function is missing on some platforms (e.g. strlcpy) or
has varying behaviour (basename).
/Bruce
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-24 16:47 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-22 12:23 [PATCH] cfgfile: fix isspace() undefined behavior Wei Chen
2026-10-22 15:54 ` Stephen Hemminger
2026-10-22 16:35 ` Bruce Richardson
-- strict thread matches above, loose matches on Subject: below --
2026-10-22 12:05 Wei Chen
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox