Hello GNU cpio maintainers,

I'd like to propose a small optional memory-safety hardening for GNU cpio.

Summary:
- Annotate dynamic_string.ds_string with optional Clang -fbounds-safety /
counted_by macros tied to ds_size (capacity).
- Capacity-before-pointer already; grow via x2nrealloc.
- Default builds unchanged (--enable-fbounds-safety OFF).

This is secure-by-design hardening (Google Patch Rewards style), not a
security bug report and not a PoC.

Unified diff attached (against savannah cpio.git tip).

Thanks,
Jeff Bindel
[email protected]
diff --git a/configure.ac b/configure.ac
index cb8a888..ffaac7c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -79,6 +79,20 @@ AC_ARG_ENABLE(mt,
 
 AM_CONDITIONAL([CPIO_MT_COND], [test "$enable_mt" = yes])
 
+dnl Optional Clang -fbounds-safety. Default OFF: DS_*COUNTED_BY* /
+dnl DS_*SIZED_BY* macros in src/dstring_bounds_safety.h are inert.
+AC_ARG_ENABLE([fbounds-safety],
+ AS_HELP_STRING([--enable-fbounds-safety],
+  [enable experimental Clang -fbounds-safety annotations @<:@default=no@:>@]),
+ [enable_fbounds_safety="$enableval"],
+ [enable_fbounds_safety="no"])
+if test "${enable_fbounds_safety}" = "yes"; then
+  AC_MSG_NOTICE([ENABLE_FBOUNDS_SAFETY enabled])
+  CFLAGS="${CFLAGS} -DDS_SUPPORT_FBOUNDS_SAFETY -fbounds-safety"
+else
+  AC_MSG_NOTICE([ENABLE_FBOUNDS_SAFETY disabled])
+fi
+
 AC_CHECK_HEADERS([unistd.h stdlib.h string.h fcntl.h pwd.h grp.h sys/io/trioctl.h utmp.h getopt.h locale.h libintl.h sys/wait.h utime.h locale.h process.h sys/ioctl.h])
 
 AC_CHECK_DECLS([errno, getpwnam, getgrnam, getgrgid, strdup, strerror, getenv, atoi, exit], , , [
diff --git a/src/Makefile.am b/src/Makefile.am
index fa17a36..2e012ad 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -46,6 +46,7 @@ noinst_HEADERS =\
  tarhdr.h\
  defer.h\
  dstring.h\
+ dstring_bounds_safety.h\
  extern.h\
  filetypes.h\
  safe-stat.h
diff --git a/src/dstring.c b/src/dstring.c
index bc17093..4a00d83 100644
--- a/src/dstring.c
+++ b/src/dstring.c
@@ -44,13 +44,16 @@ ds_free (dynamic_string *string)
   free (string->ds_string);
 }
 
-/* Expand dynamic string STRING, if necessary.  */
+/* Expand dynamic string STRING, if necessary.
+   x2nrealloc updates *ds_size (capacity) before returning the new pointer,
+   so the assignment keeps capacity-before-pointer for counted_by.  */
 
 void
 ds_resize (dynamic_string *string, size_t len)
 {
   while (len + string->ds_idx >= string->ds_size)
     {
+      /* Capacity updated via &ds_size inside x2nrealloc, then pointer.  */
       string->ds_string = x2nrealloc (string->ds_string, &string->ds_size,
 				      1);
     }
diff --git a/src/dstring.h b/src/dstring.h
index e3d833a..d013fd2 100644
--- a/src/dstring.h
+++ b/src/dstring.h
@@ -22,11 +22,17 @@
    dynamic_string routines know how to get more space if it is needed
    by allocating new space and copying the current string.  */
 
+#include "dstring_bounds_safety.h" /* optional -fbounds-safety macros */
+
 typedef struct
 {
+  /* ds_size is capacity (bytes allocated at *ds_string).  Field order keeps
+     capacity before the pointer so counted_by companions are well-formed;
+     optional -fbounds-safety uses DS_COUNTED_BY_OR_NULL when enabled.
+     ds_string may be NULL after ds_init / DYNAMIC_STRING_INITIALIZER.  */
   size_t ds_size;   /* Actual amount of storage allocated.  */
   size_t ds_idx;    /* Index of the next free byte in the string. */
-  char *ds_string;  /* String storage. */
+  char *DS_COUNTED_BY_OR_NULL(ds_size) ds_string;  /* String storage. */
 } dynamic_string;
 
 #define DYNAMIC_STRING_INITIALIZER { 0, 0, NULL }
diff --git a/src/dstring_bounds_safety.h b/src/dstring_bounds_safety.h
new file mode 100644
index 0000000..58dc424
--- /dev/null
+++ b/src/dstring_bounds_safety.h
@@ -0,0 +1,53 @@
+/* dstring_bounds_safety.h - portability macros for optional -fbounds-safety
+   Copyright (C) 2026 Jeff Bindel <[email protected]>
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3, or (at your option)
+   any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public
+   License along with this program.  If not, see
+   <http://www.gnu.org/licenses/>.
+
+   When DS_SUPPORT_FBOUNDS_SAFETY is defined (typically via
+   -DDS_SUPPORT_FBOUNDS_SAFETY and a Clang toolchain that implements
+   -fbounds-safety), these macros expand to Clang bounds annotations.
+   Otherwise they expand to nothing so default builds are unchanged.
+
+   Pattern matches libzip / libwebp / libpng / giflib / lz4 / zstd
+   inert-macro -fbounds-safety adoption: annotations are inert unless
+   explicitly enabled.  */
+
+#ifndef DSTRING_BOUNDS_SAFETY_H
+#define DSTRING_BOUNDS_SAFETY_H
+
+#ifdef DS_SUPPORT_FBOUNDS_SAFETY
+
+# include <ptrcheck.h>
+/* Non-ABI-breaking counted-by annotations for char buffers whose companion
+ * field is a capacity in elements/bytes (e.g. dynamic_string.ds_size).
+ * Prefer DS_COUNTED_BY for buffers that are non-NULL when live; use
+ * *_OR_NULL when the pointer may be NULL while the companion size is zero
+ * (ds_string is NULL after ds_init / DYNAMIC_STRING_INITIALIZER).
+ */
+# define DS_COUNTED_BY(n) __counted_by(n)
+# define DS_COUNTED_BY_OR_NULL(n) __counted_by_or_null(n)
+# define DS_SIZED_BY(n) __sized_by(n)
+# define DS_SIZED_BY_OR_NULL(n) __sized_by_or_null(n)
+
+#else /* !DS_SUPPORT_FBOUNDS_SAFETY */
+
+# define DS_COUNTED_BY(n)
+# define DS_COUNTED_BY_OR_NULL(n)
+# define DS_SIZED_BY(n)
+# define DS_SIZED_BY_OR_NULL(n)
+
+#endif /* DS_SUPPORT_FBOUNDS_SAFETY */
+
+#endif /* DSTRING_BOUNDS_SAFETY_H */

Reply via email to