[PATCH next 2/3] fortify: Optimise strnlen()

david.laight.linux@gmail.com posted 3 patches 2 days, 14 hours ago
[PATCH next 2/3] fortify: Optimise strnlen()
Posted by david.laight.linux@gmail.com 2 days, 14 hours ago
From: David Laight <david.laight.linux@gmail.com>

If the string is constant there is no need to call __real_strlen()
even when maxlen is a variable - just return the smaller value.

If the size of the string variable is unknown fortify_panic() can't be
called, change the condition so that the compiler can optimise it away.

Change __compiletime_strlen(p) to return a 'non-constant' value
for non-constant strings (the same as __builtin_strlen()).
Simplify since it is only necessary to check that the size is constant
and that the last character is '\0'.
Explain why it is different from __builtin_strlen().
Update the kunit tests to match.

Signed-off-by: David Laight <david.laight.linux@gmail.com>
---
 include/linux/fortify-string.h | 44 +++++++++++++++++-----------------
 lib/tests/fortify_kunit.c      |  8 +++----
 2 files changed, 26 insertions(+), 26 deletions(-)

diff --git a/include/linux/fortify-string.h b/include/linux/fortify-string.h
index 214d237214d5..758afd7c5f8a 100644
--- a/include/linux/fortify-string.h
+++ b/include/linux/fortify-string.h
@@ -58,19 +58,22 @@ void __read_overflow2_field(size_t avail, size_t wanted) __compiletime_warning("
 void __write_overflow(void) __compiletime_error("detected write beyond size of object (1st parameter)");
 void __write_overflow_field(size_t avail, size_t wanted) __compiletime_warning("detected write beyond size of field (1st parameter); maybe use struct_group()?");
 
-#define __compiletime_strlen(p)					\
-({								\
-	char *__p = (char *)(p);				\
-	size_t __ret = SIZE_MAX;				\
-	const size_t __p_size = __member_size(p);		\
-	if (__p_size != SIZE_MAX &&				\
-	    __builtin_constant_p(*__p)) {			\
-		size_t __p_len = __p_size - 1;			\
-		if (__builtin_constant_p(__p[__p_len]) &&	\
-		    __p[__p_len] == '\0')			\
-			__ret = __builtin_strlen(__p);		\
-	}							\
-	__ret;							\
+/*
+ * __builtin_strlen() generates a compile-time error for 'const char foo[4] = "abcd";'.
+ * But that is a valid source for both strnlen() and strscpy() with a constant
+ * length less than or equal to 4.
+ * __compiletime_strlen() returns a non-constant for such items.
+ * Beware of strings with embedded '\0', __builtin_strlen() can be much smaller
+ * than __member_size();
+ * The return value must only be used when it is a constant.
+ */
+extern size_t __fortify_undefined;
+#define __compiletime_strlen(p)							\
+({										\
+	char *__p = (char *)(p);						\
+	const size_t __p_size = __member_size(p);				\
+	__p_size == SIZE_MAX || !statically_true(__p[__p_size - 1] == '\0') ?	\
+		__fortify_undefined : __builtin_strlen(__p);			\
 })
 
 #if defined(__SANITIZE_ADDRESS__)
@@ -215,16 +218,13 @@ __FORTIFY_INLINE __kernel_size_t strnlen(const char * const POS p, __kernel_size
 	const size_t p_len = __compiletime_strlen(p);
 	size_t ret;
 
-	/* We can take compile-time actions when maxlen is const. */
-	if (__builtin_constant_p(maxlen) && p_len != SIZE_MAX) {
-		/* If p is const, we can use its compile-time-known len. */
-		if (maxlen >= p_size)
-			return p_len;
-	}
+	/* If p is const, we can use its compile-time-known len. */
+	if (__builtin_constant_p(p_len))
+		return p_len < maxlen ? p_len : maxlen;
 
 	/* Do not check characters beyond the end of p. */
-	ret = __real_strnlen(p, maxlen < p_size ? maxlen : p_size);
-	if (p_size <= ret && maxlen != ret)
+	ret = __real_strnlen(p, p_size < maxlen ? p_size : maxlen);
+	if (ret == p_size && p_size < maxlen)
 		fortify_panic(FORTIFY_FUNC_strnlen, FORTIFY_READ, p_size, ret + 1, ret);
 	return ret;
 }
@@ -289,7 +289,7 @@ __FORTIFY_INLINE ssize_t sized_strscpy(char * const POS p, const char * const PO
 	if (statically_true(p_size < SIZE_MAX)) {
 		len = __compiletime_strlen(q);
 
-		if (len < SIZE_MAX && statically_true(len < size)) {
+		if (statically_true(len < size)) {
 			__underlying_memcpy(p, q, len + 1);
 			return len;
 		}
diff --git a/lib/tests/fortify_kunit.c b/lib/tests/fortify_kunit.c
index fc9c76f026d6..9b3b7201c02d 100644
--- a/lib/tests/fortify_kunit.c
+++ b/lib/tests/fortify_kunit.c
@@ -102,11 +102,11 @@ static void fortify_test_known_sizes(struct kunit *test)
 	KUNIT_EXPECT_EQ(test, __compiletime_strlen(unchanging_12), 12);
 
 	KUNIT_EXPECT_FALSE(test, __is_constexpr(__builtin_strlen(array_unknown)));
-	KUNIT_EXPECT_EQ(test, __compiletime_strlen(array_unknown), SIZE_MAX);
+	KUNIT_EXPECT_FALSE(test, __is_constexpr(__compiletime_strlen(array_unknown)));
 
 	/* Externally defined and dynamically sized string pointer: */
 	KUNIT_EXPECT_FALSE(test, __is_constexpr(__builtin_strlen(test->name)));
-	KUNIT_EXPECT_EQ(test, __compiletime_strlen(test->name), SIZE_MAX);
+	KUNIT_EXPECT_FALSE(test, __is_constexpr(__compiletime_strlen(test->name)));
 }
 
 /* This is volatile so the optimizer can't perform DCE below. */
@@ -128,12 +128,12 @@ static noinline size_t want_minus_one(int pick)
 		str = "1";
 		break;
 	}
-	return __compiletime_strlen(str);
+	return __builtin_constant_p(__compiletime_strlen(str));
 }
 
 static void fortify_test_control_flow_split(struct kunit *test)
 {
-	KUNIT_EXPECT_EQ(test, want_minus_one(pick), SIZE_MAX);
+	KUNIT_EXPECT_FALSE(test, want_minus_one(pick));
 }
 
 #define KUNIT_EXPECT_BOS(test, p, expected, name)			\
-- 
2.39.5
Re: [PATCH next 2/3] fortify: Optimise strnlen()
Posted by kernel test robot 1 day, 4 hours ago
Hi,

kernel test robot noticed the following build errors:

[auto build test ERROR on next-20260327]

url:    https://github.com/intel-lab-lkp/linux/commits/david-laight-linux-gmail-com/fortify-replace-__compiletime_lessthan-with-statically_true/20260331-041123
base:   next-20260327
patch link:    https://lore.kernel.org/r/20260330132003.3379-3-david.laight.linux%40gmail.com
patch subject: [PATCH next 2/3] fortify: Optimise strnlen()
config: x86_64-buildonly-randconfig-001-20260331 (https://download.01.org/0day-ci/archive/20260401/202604010835.IWCsUV7z-lkp@intel.com/config)
compiler: clang version 20.1.8 (https://github.com/llvm/llvm-project 87f0227cb60147a26a1eeb4fb06e3b505e9c7261)
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260401/202604010835.IWCsUV7z-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202604010835.IWCsUV7z-lkp@intel.com/

All errors (new ones prefixed by >>):

>> ld.lld: error: undefined symbol: __fortify_undefined
   >>> referenced by fortify-string.h:290 (include/linux/fortify-string.h:290)
   >>>               vmlinux.o:(parse_early_param)
   >>> referenced by fortify-string.h:218 (include/linux/fortify-string.h:218)
   >>>               vmlinux.o:(parse_early_param)
   >>> referenced by fortify-string.h:290 (include/linux/fortify-string.h:290)
   >>>               vmlinux.o:(setup_boot_config)
   >>> referenced 2103 more times

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
Re: [PATCH next 2/3] fortify: Optimise strnlen()
Posted by Kees Cook 1 day, 21 hours ago
On Mon, Mar 30, 2026 at 02:20:02PM +0100, david.laight.linux@gmail.com wrote:
> From: David Laight <david.laight.linux@gmail.com>
> 
> If the string is constant there is no need to call __real_strlen()
> even when maxlen is a variable - just return the smaller value.
> 
> If the size of the string variable is unknown fortify_panic() can't be
> called, change the condition so that the compiler can optimise it away.
> 
> Change __compiletime_strlen(p) to return a 'non-constant' value
> for non-constant strings (the same as __builtin_strlen()).
> Simplify since it is only necessary to check that the size is constant
> and that the last character is '\0'.
> Explain why it is different from __builtin_strlen().
> Update the kunit tests to match.

See also
commit d07c0acb4f41 ("fortify: Fix __compiletime_strlen() under UBSAN_BOUNDS_LOCAL")

-Kees

-- 
Kees Cook
Re: [PATCH next 2/3] fortify: Optimise strnlen()
Posted by David Laight 1 day, 18 hours ago
On Mon, 30 Mar 2026 23:36:07 -0700
Kees Cook <kees@kernel.org> wrote:

> On Mon, Mar 30, 2026 at 02:20:02PM +0100, david.laight.linux@gmail.com wrote:
> > From: David Laight <david.laight.linux@gmail.com>
> > 
> > If the string is constant there is no need to call __real_strlen()
> > even when maxlen is a variable - just return the smaller value.
> > 
> > If the size of the string variable is unknown fortify_panic() can't be
> > called, change the condition so that the compiler can optimise it away.
> > 
> > Change __compiletime_strlen(p) to return a 'non-constant' value
> > for non-constant strings (the same as __builtin_strlen()).
> > Simplify since it is only necessary to check that the size is constant
> > and that the last character is '\0'.
> > Explain why it is different from __builtin_strlen().
> > Update the kunit tests to match.  
> 
> See also
> commit d07c0acb4f41 ("fortify: Fix __compiletime_strlen() under UBSAN_BOUNDS_LOCAL")
> 
> -Kees
> 

It is far more subtle that that;
There shouldn't be a run-time access to __p[__p_len] at all.
And you really don't want one.

The problematic code was:
	if (__builtin_constant(__p[__p_len]) && __p[__p_len] == 0)
If the compiler thinks __p[__p_len] is constant then it will also
think that __p[0] is constant.
So the extra check should really make no difference.

I suspect this is what happened, consider:
	const char *foo;
	if (cond)
		foo = "foo";
	else
		foo = "fubar";
	return __compiletime_strlen(foo);

This is first converted to (ignoring any silly typos):
	const char *foo;
	if (cond)
		foo = "foo";
	else
		foo = "fubar";
	len = __builtin_object_size(foo,1) - 1; // 6 - 1
	if (__builtin_constant(foo[len]) && foo[len] == 0)
		return __builtin_strlen(foo);
	return SIZE_MAX;
Since foo isn't constant that returns SIZE_MAX.

The code is then moved into the conditional giving:
	if (cond) {
		foo = "foo";
		if (__builtin_constant(foo[5]) && foo[5] == 0)
			return __builtin_strlen(foo);
		return SIZE_MAX;
	} else {
		foo = "fubar";
		if (__builtin_constant(foo[5]) && foo[5] == 0)
			return __builtin_strlen(foo);
		return SIZE_MAX;
	}	

Since since foo is now 'pointer to constant' foo[] is constant, giving: 
	if (cond) {
		foo = "foo";
		if (foo[5] == 0)
			return __builtin_strlen(foo);
		return SIZE_MAX;
	} else {
		foo = "fubar";
		if (foo[5] == 0)
			return __builtin_strlen(foo);
		return SIZE_MAX;
	}	

In the bottom bit foo[5] is well defined and known to be zero.
In the top bit foo[5] is UB and gcc leaves the code it, giving:
	if (cond) {
		foo = "foo";
		if (foo[5] == 0)
			return __builtin_strlen(foo);
		return SIZE_MAX;
	} else {
		foo = "fubar";
		return __builtin_strlen(foo);
	}

and you get a real reference off the end of foo[] - which UBSAN_LOCAL_BOUNDS
rightly picks up on.

clang has a habit of silently deleting everything after UB, so might
generate:
	if (cond) {
		return whatever_happens_to_be_in_ax;
	} else {
		foo = "fubar";
		return __builtin_strlen(foo);
	}

The 'fix' of checking __p[0] actually makes no real difference.
I'd guess that the longer code block stops gcc moving the code
into the conditional and hides the bug.
But that could easily change by just breathing on the code somewhere
or in a future compiler version.

I suspect this should be a compiler bug.
But with the compiler behaving this way you can't write
__compiletime_strlen() with a check for the '\0' terminator.

That really means you can only use __builtin_strlen().
Which means you'll get a compile-time error from:
	char foo[3] = "foo";
	__builtin_strlen(foo);
rather the 'not a constant' when checking strscpy(tgt, foo, 3);
At a guess that never happens except in the tests.

	David
Re: [PATCH next 2/3] fortify: Optimise strnlen()
Posted by David Laight 1 day, 13 hours ago
On Tue, 31 Mar 2026 11:14:28 +0100
David Laight <david.laight.linux@gmail.com> wrote:

> On Mon, 30 Mar 2026 23:36:07 -0700
> Kees Cook <kees@kernel.org> wrote:
> 
> > On Mon, Mar 30, 2026 at 02:20:02PM +0100, david.laight.linux@gmail.com wrote:  
> > > From: David Laight <david.laight.linux@gmail.com>
> > > 
> > > If the string is constant there is no need to call __real_strlen()
> > > even when maxlen is a variable - just return the smaller value.
> > > 
> > > If the size of the string variable is unknown fortify_panic() can't be
> > > called, change the condition so that the compiler can optimise it away.
> > > 
> > > Change __compiletime_strlen(p) to return a 'non-constant' value
> > > for non-constant strings (the same as __builtin_strlen()).
> > > Simplify since it is only necessary to check that the size is constant
> > > and that the last character is '\0'.
> > > Explain why it is different from __builtin_strlen().
> > > Update the kunit tests to match.    
> > 
> > See also
> > commit d07c0acb4f41 ("fortify: Fix __compiletime_strlen() under UBSAN_BOUNDS_LOCAL")
> > 
> > -Kees
...
> That really means you can only use __builtin_strlen().
> Which means you'll get a compile-time error from:
> 	char foo[3] = "foo";
> 	__builtin_strlen(foo);
> rather the 'not a constant' when checking strscpy(tgt, foo, 3);
> At a guess that never happens except in the tests.

I wrote this change a while ago, I tried using __builtin_strlen()
but got a compile error in the tests.

However I've just built an x86-64 allmodconfig kernel on top of
my patches with:
#define __compiletime_strlen(p) __builtin_strlen()
so something must have changed since then (probably related to the
__nonstring changes).

So the actual fix for the above is to use __builtin_strlen().
IIRC it also detects a few more strings being constant.

	David
Re: [PATCH next 2/3] fortify: Optimise strnlen()
Posted by Kees Cook 1 day, 12 hours ago
On Tue, Mar 31, 2026 at 03:55:41PM +0100, David Laight wrote:
> On Tue, 31 Mar 2026 11:14:28 +0100
> David Laight <david.laight.linux@gmail.com> wrote:
> 
> > On Mon, 30 Mar 2026 23:36:07 -0700
> > Kees Cook <kees@kernel.org> wrote:
> > 
> > > On Mon, Mar 30, 2026 at 02:20:02PM +0100, david.laight.linux@gmail.com wrote:  
> > > > From: David Laight <david.laight.linux@gmail.com>
> > > > 
> > > > If the string is constant there is no need to call __real_strlen()
> > > > even when maxlen is a variable - just return the smaller value.
> > > > 
> > > > If the size of the string variable is unknown fortify_panic() can't be
> > > > called, change the condition so that the compiler can optimise it away.
> > > > 
> > > > Change __compiletime_strlen(p) to return a 'non-constant' value
> > > > for non-constant strings (the same as __builtin_strlen()).
> > > > Simplify since it is only necessary to check that the size is constant
> > > > and that the last character is '\0'.
> > > > Explain why it is different from __builtin_strlen().
> > > > Update the kunit tests to match.    
> > > 
> > > See also
> > > commit d07c0acb4f41 ("fortify: Fix __compiletime_strlen() under UBSAN_BOUNDS_LOCAL")
> > > 
> > > -Kees
> ...

Your series does pass all the wacky corner case tests that got added
over the years, though I need to go through older GCC and Clang releases
to double-check. I've pushed a branch to kernel.org for 0day testing:
https://git.kernel.org/pub/scm/linux/kernel/git/kees/linux.git/log/?h=dev/v7.0-rc2/fortify-strlen

> > That really means you can only use __builtin_strlen().
> > Which means you'll get a compile-time error from:
> > 	char foo[3] = "foo";
> > 	__builtin_strlen(foo);
> > rather the 'not a constant' when checking strscpy(tgt, foo, 3);
> > At a guess that never happens except in the tests.
> 
> I wrote this change a while ago, I tried using __builtin_strlen()
> but got a compile error in the tests.
> 
> However I've just built an x86-64 allmodconfig kernel on top of
> my patches with:
> #define __compiletime_strlen(p) __builtin_strlen()
> so something must have changed since then (probably related to the
> __nonstring changes).
> 
> So the actual fix for the above is to use __builtin_strlen().
> IIRC it also detects a few more strings being constant.

Hrm, the "no NUL in the string" case is a weird one, but yeah, probably
hit by the __nonstring work.

Can you write up new test cases to validate what you're after? I can
update the branch I pushed.

-Kees

-- 
Kees Cook
Re: [PATCH next 2/3] fortify: Optimise strnlen()
Posted by Kees Cook 2 days, 4 hours ago
On Mon, Mar 30, 2026 at 02:20:02PM +0100, david.laight.linux@gmail.com wrote:
> From: David Laight <david.laight.linux@gmail.com>
> 
> If the string is constant there is no need to call __real_strlen()
> even when maxlen is a variable - just return the smaller value.
> 
> If the size of the string variable is unknown fortify_panic() can't be
> called, change the condition so that the compiler can optimise it away.
> 
> Change __compiletime_strlen(p) to return a 'non-constant' value
> for non-constant strings (the same as __builtin_strlen()).
> Simplify since it is only necessary to check that the size is constant
> and that the last character is '\0'.
> Explain why it is different from __builtin_strlen().
> Update the kunit tests to match.

This will need very very careful checking. It took a lot of tweaking to
get __compiletime_strlen() to behave correctly across all supported
versions of GCC and Clang. It has some very ugly glitches possible. I'm
not saying your replacement is broken: I mean to say I'm going to need
to spend a bunch of time proving to myself that this is a safe
replacement.

Outside of that, though, I don't think dropping the various SIZE_MAX
tests is correct. Again, I'll need to spend some time double-checking.

-Kees

> 
> Signed-off-by: David Laight <david.laight.linux@gmail.com>
> ---
>  include/linux/fortify-string.h | 44 +++++++++++++++++-----------------
>  lib/tests/fortify_kunit.c      |  8 +++----
>  2 files changed, 26 insertions(+), 26 deletions(-)
> 
> diff --git a/include/linux/fortify-string.h b/include/linux/fortify-string.h
> index 214d237214d5..758afd7c5f8a 100644
> --- a/include/linux/fortify-string.h
> +++ b/include/linux/fortify-string.h
> @@ -58,19 +58,22 @@ void __read_overflow2_field(size_t avail, size_t wanted) __compiletime_warning("
>  void __write_overflow(void) __compiletime_error("detected write beyond size of object (1st parameter)");
>  void __write_overflow_field(size_t avail, size_t wanted) __compiletime_warning("detected write beyond size of field (1st parameter); maybe use struct_group()?");
>  
> -#define __compiletime_strlen(p)					\
> -({								\
> -	char *__p = (char *)(p);				\
> -	size_t __ret = SIZE_MAX;				\
> -	const size_t __p_size = __member_size(p);		\
> -	if (__p_size != SIZE_MAX &&				\
> -	    __builtin_constant_p(*__p)) {			\
> -		size_t __p_len = __p_size - 1;			\
> -		if (__builtin_constant_p(__p[__p_len]) &&	\
> -		    __p[__p_len] == '\0')			\
> -			__ret = __builtin_strlen(__p);		\
> -	}							\
> -	__ret;							\
> +/*
> + * __builtin_strlen() generates a compile-time error for 'const char foo[4] = "abcd";'.
> + * But that is a valid source for both strnlen() and strscpy() with a constant
> + * length less than or equal to 4.
> + * __compiletime_strlen() returns a non-constant for such items.
> + * Beware of strings with embedded '\0', __builtin_strlen() can be much smaller
> + * than __member_size();
> + * The return value must only be used when it is a constant.
> + */
> +extern size_t __fortify_undefined;
> +#define __compiletime_strlen(p)							\
> +({										\
> +	char *__p = (char *)(p);						\
> +	const size_t __p_size = __member_size(p);				\
> +	__p_size == SIZE_MAX || !statically_true(__p[__p_size - 1] == '\0') ?	\
> +		__fortify_undefined : __builtin_strlen(__p);			\
>  })
>  
>  #if defined(__SANITIZE_ADDRESS__)
> @@ -215,16 +218,13 @@ __FORTIFY_INLINE __kernel_size_t strnlen(const char * const POS p, __kernel_size
>  	const size_t p_len = __compiletime_strlen(p);
>  	size_t ret;
>  
> -	/* We can take compile-time actions when maxlen is const. */
> -	if (__builtin_constant_p(maxlen) && p_len != SIZE_MAX) {
> -		/* If p is const, we can use its compile-time-known len. */
> -		if (maxlen >= p_size)
> -			return p_len;
> -	}
> +	/* If p is const, we can use its compile-time-known len. */
> +	if (__builtin_constant_p(p_len))
> +		return p_len < maxlen ? p_len : maxlen;
>  
>  	/* Do not check characters beyond the end of p. */
> -	ret = __real_strnlen(p, maxlen < p_size ? maxlen : p_size);
> -	if (p_size <= ret && maxlen != ret)
> +	ret = __real_strnlen(p, p_size < maxlen ? p_size : maxlen);
> +	if (ret == p_size && p_size < maxlen)
>  		fortify_panic(FORTIFY_FUNC_strnlen, FORTIFY_READ, p_size, ret + 1, ret);
>  	return ret;
>  }
> @@ -289,7 +289,7 @@ __FORTIFY_INLINE ssize_t sized_strscpy(char * const POS p, const char * const PO
>  	if (statically_true(p_size < SIZE_MAX)) {
>  		len = __compiletime_strlen(q);
>  
> -		if (len < SIZE_MAX && statically_true(len < size)) {
> +		if (statically_true(len < size)) {
>  			__underlying_memcpy(p, q, len + 1);
>  			return len;
>  		}
> diff --git a/lib/tests/fortify_kunit.c b/lib/tests/fortify_kunit.c
> index fc9c76f026d6..9b3b7201c02d 100644
> --- a/lib/tests/fortify_kunit.c
> +++ b/lib/tests/fortify_kunit.c
> @@ -102,11 +102,11 @@ static void fortify_test_known_sizes(struct kunit *test)
>  	KUNIT_EXPECT_EQ(test, __compiletime_strlen(unchanging_12), 12);
>  
>  	KUNIT_EXPECT_FALSE(test, __is_constexpr(__builtin_strlen(array_unknown)));
> -	KUNIT_EXPECT_EQ(test, __compiletime_strlen(array_unknown), SIZE_MAX);
> +	KUNIT_EXPECT_FALSE(test, __is_constexpr(__compiletime_strlen(array_unknown)));
>  
>  	/* Externally defined and dynamically sized string pointer: */
>  	KUNIT_EXPECT_FALSE(test, __is_constexpr(__builtin_strlen(test->name)));
> -	KUNIT_EXPECT_EQ(test, __compiletime_strlen(test->name), SIZE_MAX);
> +	KUNIT_EXPECT_FALSE(test, __is_constexpr(__compiletime_strlen(test->name)));
>  }
>  
>  /* This is volatile so the optimizer can't perform DCE below. */
> @@ -128,12 +128,12 @@ static noinline size_t want_minus_one(int pick)
>  		str = "1";
>  		break;
>  	}
> -	return __compiletime_strlen(str);
> +	return __builtin_constant_p(__compiletime_strlen(str));
>  }
>  
>  static void fortify_test_control_flow_split(struct kunit *test)
>  {
> -	KUNIT_EXPECT_EQ(test, want_minus_one(pick), SIZE_MAX);
> +	KUNIT_EXPECT_FALSE(test, want_minus_one(pick));
>  }
>  
>  #define KUNIT_EXPECT_BOS(test, p, expected, name)			\
> -- 
> 2.39.5
> 

-- 
Kees Cook
Re: [PATCH next 2/3] fortify: Optimise strnlen()
Posted by David Laight 1 day, 6 hours ago
On Mon, 30 Mar 2026 16:54:27 -0700
Kees Cook <kees@kernel.org> wrote:

> On Mon, Mar 30, 2026 at 02:20:02PM +0100, david.laight.linux@gmail.com wrote:
> > From: David Laight <david.laight.linux@gmail.com>
> > 
> > If the string is constant there is no need to call __real_strlen()
> > even when maxlen is a variable - just return the smaller value.
> > 
> > If the size of the string variable is unknown fortify_panic() can't be
> > called, change the condition so that the compiler can optimise it away.
> > 
> > Change __compiletime_strlen(p) to return a 'non-constant' value
> > for non-constant strings (the same as __builtin_strlen()).
> > Simplify since it is only necessary to check that the size is constant
> > and that the last character is '\0'.
> > Explain why it is different from __builtin_strlen().
> > Update the kunit tests to match.  
> 
> This will need very very careful checking. It took a lot of tweaking to
> get __compiletime_strlen() to behave correctly across all supported
> versions of GCC and Clang. It has some very ugly glitches possible. I'm
> not saying your replacement is broken: I mean to say I'm going to need
> to spend a bunch of time proving to myself that this is a safe
> replacement.

There are two differences:
1) I removed the check for p[0] being constant, just p[size - 1].
2) The return value is 'not constant' (rather than SIZE_MAX) for
   non-constant strings.
   This includes any case where __builtin_strlen() returns a non-constant.

I did wonder what happens if you have:
static char foo[] = "foo";
then later:
	foo[1] = 0;
The compiler could determine that both foo[0] and foo[3] are never
changed, so (both versions of) __compiletime_strlen() would return
the non-constant result of __builtin_strlen().
The current compilers don't report foo[3] as being constant unless
the string itself in constant, but they might get 'better'. 

> Outside of that, though, I don't think dropping the various SIZE_MAX
> tests is correct. Again, I'll need to spend some time double-checking.

They needed changing to match the function returning 'not constant'
rather than SIZE_MAX.

Actually, as I noted in the other email (after writing, but not sending,
this one) __compiletime_strlen() is fundamentally broken with the current
compilers.
Any uses should be replaced by __builtin_strlen().

	David

> 
> -Kees
> 
> > 
> > Signed-off-by: David Laight <david.laight.linux@gmail.com>
> > ---
> >  include/linux/fortify-string.h | 44 +++++++++++++++++-----------------
> >  lib/tests/fortify_kunit.c      |  8 +++----
> >  2 files changed, 26 insertions(+), 26 deletions(-)
> > 
> > diff --git a/include/linux/fortify-string.h b/include/linux/fortify-string.h
> > index 214d237214d5..758afd7c5f8a 100644
> > --- a/include/linux/fortify-string.h
> > +++ b/include/linux/fortify-string.h
> > @@ -58,19 +58,22 @@ void __read_overflow2_field(size_t avail, size_t wanted) __compiletime_warning("
> >  void __write_overflow(void) __compiletime_error("detected write beyond size of object (1st parameter)");
> >  void __write_overflow_field(size_t avail, size_t wanted) __compiletime_warning("detected write beyond size of field (1st parameter); maybe use struct_group()?");
> >  
> > -#define __compiletime_strlen(p)					\
> > -({								\
> > -	char *__p = (char *)(p);				\
> > -	size_t __ret = SIZE_MAX;				\
> > -	const size_t __p_size = __member_size(p);		\
> > -	if (__p_size != SIZE_MAX &&				\
> > -	    __builtin_constant_p(*__p)) {			\
> > -		size_t __p_len = __p_size - 1;			\
> > -		if (__builtin_constant_p(__p[__p_len]) &&	\
> > -		    __p[__p_len] == '\0')			\
> > -			__ret = __builtin_strlen(__p);		\
> > -	}							\
> > -	__ret;							\
> > +/*
> > + * __builtin_strlen() generates a compile-time error for 'const char foo[4] = "abcd";'.
> > + * But that is a valid source for both strnlen() and strscpy() with a constant
> > + * length less than or equal to 4.
> > + * __compiletime_strlen() returns a non-constant for such items.
> > + * Beware of strings with embedded '\0', __builtin_strlen() can be much smaller
> > + * than __member_size();
> > + * The return value must only be used when it is a constant.
> > + */
> > +extern size_t __fortify_undefined;
> > +#define __compiletime_strlen(p)							\
> > +({										\
> > +	char *__p = (char *)(p);						\
> > +	const size_t __p_size = __member_size(p);				\
> > +	__p_size == SIZE_MAX || !statically_true(__p[__p_size - 1] == '\0') ?	\
> > +		__fortify_undefined : __builtin_strlen(__p);			\
> >  })
> >  
> >  #if defined(__SANITIZE_ADDRESS__)
> > @@ -215,16 +218,13 @@ __FORTIFY_INLINE __kernel_size_t strnlen(const char * const POS p, __kernel_size
> >  	const size_t p_len = __compiletime_strlen(p);
> >  	size_t ret;
> >  
> > -	/* We can take compile-time actions when maxlen is const. */
> > -	if (__builtin_constant_p(maxlen) && p_len != SIZE_MAX) {
> > -		/* If p is const, we can use its compile-time-known len. */
> > -		if (maxlen >= p_size)
> > -			return p_len;
> > -	}
> > +	/* If p is const, we can use its compile-time-known len. */
> > +	if (__builtin_constant_p(p_len))
> > +		return p_len < maxlen ? p_len : maxlen;
> >  
> >  	/* Do not check characters beyond the end of p. */
> > -	ret = __real_strnlen(p, maxlen < p_size ? maxlen : p_size);
> > -	if (p_size <= ret && maxlen != ret)
> > +	ret = __real_strnlen(p, p_size < maxlen ? p_size : maxlen);
> > +	if (ret == p_size && p_size < maxlen)
> >  		fortify_panic(FORTIFY_FUNC_strnlen, FORTIFY_READ, p_size, ret + 1, ret);
> >  	return ret;
> >  }
> > @@ -289,7 +289,7 @@ __FORTIFY_INLINE ssize_t sized_strscpy(char * const POS p, const char * const PO
> >  	if (statically_true(p_size < SIZE_MAX)) {
> >  		len = __compiletime_strlen(q);
> >  
> > -		if (len < SIZE_MAX && statically_true(len < size)) {
> > +		if (statically_true(len < size)) {
> >  			__underlying_memcpy(p, q, len + 1);
> >  			return len;
> >  		}
> > diff --git a/lib/tests/fortify_kunit.c b/lib/tests/fortify_kunit.c
> > index fc9c76f026d6..9b3b7201c02d 100644
> > --- a/lib/tests/fortify_kunit.c
> > +++ b/lib/tests/fortify_kunit.c
> > @@ -102,11 +102,11 @@ static void fortify_test_known_sizes(struct kunit *test)
> >  	KUNIT_EXPECT_EQ(test, __compiletime_strlen(unchanging_12), 12);
> >  
> >  	KUNIT_EXPECT_FALSE(test, __is_constexpr(__builtin_strlen(array_unknown)));
> > -	KUNIT_EXPECT_EQ(test, __compiletime_strlen(array_unknown), SIZE_MAX);
> > +	KUNIT_EXPECT_FALSE(test, __is_constexpr(__compiletime_strlen(array_unknown)));
> >  
> >  	/* Externally defined and dynamically sized string pointer: */
> >  	KUNIT_EXPECT_FALSE(test, __is_constexpr(__builtin_strlen(test->name)));
> > -	KUNIT_EXPECT_EQ(test, __compiletime_strlen(test->name), SIZE_MAX);
> > +	KUNIT_EXPECT_FALSE(test, __is_constexpr(__compiletime_strlen(test->name)));
> >  }
> >  
> >  /* This is volatile so the optimizer can't perform DCE below. */
> > @@ -128,12 +128,12 @@ static noinline size_t want_minus_one(int pick)
> >  		str = "1";
> >  		break;
> >  	}
> > -	return __compiletime_strlen(str);
> > +	return __builtin_constant_p(__compiletime_strlen(str));
> >  }
> >  
> >  static void fortify_test_control_flow_split(struct kunit *test)
> >  {
> > -	KUNIT_EXPECT_EQ(test, want_minus_one(pick), SIZE_MAX);
> > +	KUNIT_EXPECT_FALSE(test, want_minus_one(pick));
> >  }
> >  
> >  #define KUNIT_EXPECT_BOS(test, p, expected, name)			\
> > -- 
> > 2.39.5
> >   
>
Re: [PATCH next 2/3] fortify: Optimise strnlen()
Posted by Kees Cook 1 day, 4 hours ago
On Tue, Mar 31, 2026 at 11:09:14PM +0100, David Laight wrote:
> Any uses should be replaced by __builtin_strlen().

When I looked at this before, __builtin_strlen() flip to run-time strlen
on non-constant strings, which is why I had to jump through all the
hoops to avoid calling it in those cases.

-- 
Kees Cook
Re: [PATCH next 2/3] fortify: Optimise strnlen()
Posted by David Laight 14 hours ago
On Tue, 31 Mar 2026 16:51:26 -0700
Kees Cook <kees@kernel.org> wrote:

> On Tue, Mar 31, 2026 at 11:09:14PM +0100, David Laight wrote:
> > Any uses should be replaced by __builtin_strlen().  
> 
> When I looked at this before, __builtin_strlen() flip to run-time strlen
> on non-constant strings, which is why I had to jump through all the
> hoops to avoid calling it in those cases.
> 

It should be fine provided that you check that the result is constant.
So doing:
	size_t len = __builtin_strlen(p);
	if (__builtin_constant_p(len))
		...
should never generate a run-time call to strlen().
(Probably the optimiser throws the call away because it knows it has
no side effects.)

I did notice that:
	if (__builtin_constant_p(__builtin_strlen(p)))
		...
is true less often (more so with clang than gcc).
I suspect than an early compiler pass generates 'no' rather than 'maybe'
when used inside an inlined function.

There is also something odd going on with one of the 'bot' builds.
I've compiled x86 allmodconfig with clang-18, no warning or link fails.
But I've not tried the specific config being tested.
The link for reproducing the error isn't entirely helpful.

Looking into that error I noticed that clang fails to optimise the
strscpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE) in
init/main.c:setup_boot_config() to a memcpy().
That means it calls strnlen() and then strscpy() - two scans to find
the length is also silly.
(At some point early on that code needs to call a real function to
do all the work instead of inlining everything into the caller.)

	David