Files
rsync/t_safe_arg.c
Andrew Tridgell 94a7ccd63d testsuite: link t_safe_arg against real objects (portable, drop --gc-sections)
The KI-54 helper linked options.o compiled with -ffunction-sections and relied on
-Wl,--gc-sections to drop the option parser.  That is GNU-ld-only: the fleet
showed macOS (ld64; also its custom rule dropped the openssl include path) and the
cygwin PE linker leave parse_arguments and its deps undefined, failing the build
of every CHECK_PROGS target on those hosts.  Instead link the real rsync objects
(the same set as the rsync binary, minus main.o -- supplied renamed via
t_safe_arg_main.o) so safe_arg's deps all resolve with a plain link on every
platform.  t_safe_arg_main.o depends on $(HEADERS) so the generated proto.h is
built before it under a parallel make.  No behaviour change to the test itself.

Reported-by: Leonid Bugaev
2026-07-20 14:05:32 +10:00

57 lines
2.1 KiB
C

/* Unit test for KI-54: safe_arg() in filename mode (opt==NULL) must not leak an
* uninitialized heap byte. The escape counter and the writer disagreed on a
* backslash before a wildcard / a trailing backslash, leaving a gap that
* strlen() walks into. We poison the heap first so the leaked byte is a
* deterministic non-NUL; then any extra byte makes the output differ from the
* expected exact quoting. Exits 0 if all outputs are exact, 1 otherwise. */
/* We link the real rsync objects (see the Makefile), so safe_arg() and all its
* globals come from options.o/exclude.o/etc; we only declare what we touch. */
#include "rsync.h"
#include <stdio.h>
extern char *safe_arg(const char *opt, const char *arg);
extern int protect_args, old_style_args, am_sender, relative_paths;
extern int trust_sender_args;
static const struct { const char *arg, *exp; } cases[] = {
{ "\\*", "\\*" }, /* backslash+wildcard: NOT doubled */
{ "\\?", "\\?" },
{ "\\[", "\\[" },
{ "\\", "\\\\" }, /* trailing backslash: doubled (NUL-footgun case) */
{ "\\*\\?", "\\*\\?" }, /* two suppressed backslashes */
{ "a\\*b", "a\\*b" },
{ "\\a", "\\\\a" }, /* backslash+non-wildcard: doubled */
};
int main(int argc, char *argv[])
{
(void)argc; (void)argv;
int i, fails = 0, n = (int)(sizeof cases / sizeof cases[0]);
protect_args = 0; old_style_args = 0; am_sender = 1;
relative_paths = 0; trust_sender_args = 1;
/* Poison the heap so an uninitialized byte reads as 0xbe, not a lucky NUL. */
for (i = 0; i < 256; i++) {
void *p = malloc(64);
if (p) { memset(p, 0xbe, 64); free(p); }
}
for (i = 0; i < n; i++) {
char *r = safe_arg(NULL, cases[i].arg);
if (!r || strcmp(r, cases[i].exp) != 0) {
printf("FAIL: safe_arg(NULL, \"%s\") = \"%s\" (len %zu), expected \"%s\"\n",
cases[i].arg, r ? r : "(null)", r ? strlen(r) : 0, cases[i].exp);
fails++;
}
}
if (fails) {
printf("safe_arg: %d case(s) wrong -- uninitialized-byte leak / miscount\n", fails);
return 1;
}
printf("safe_arg: filename-mode quoting is exact (no uninit byte)\n");
return 0;
}