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 */