From 113496af2c3e52168aea2e57beeb42e456bd9d76 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 23 Aug 2025 18:29:06 +1000 Subject: [PATCH 1/2] testsuite: added clean-fname-underflow test --- testsuite/clean-fname-underflow.test | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 testsuite/clean-fname-underflow.test diff --git a/testsuite/clean-fname-underflow.test b/testsuite/clean-fname-underflow.test new file mode 100644 index 00000000..56d4fece --- /dev/null +++ b/testsuite/clean-fname-underflow.test @@ -0,0 +1,66 @@ +#!/bin/sh +# clean-fname-underflow.test +# Ensure clean_fname() does not read-before-buffer when collapsing "..". +# This exercises the --server path where a crafted merge filename hits clean_fname(). +# +# Usage: +# ./configure && make +# make check TESTS='clean-fname-underflow.test' + +set -eu + +# Try to find the just-built rsync binary if RSYNC_BIN isn't set. +if [ -z "${RSYNC_BIN:-}" ]; then + if [ -x "./rsync" ]; then + RSYNC_BIN=./rsync + elif [ -x "../rsync" ]; then + RSYNC_BIN=../rsync + else + RSYNC_BIN=rsync + fi +fi + +workdir="${TMPDIR:-/tmp}/rsync-clean-fname.$$" +mkdir -p "$workdir" +trap 'rm -rf "$workdir"' EXIT INT TERM +cd "$workdir" + +# Minimal rsyncd.conf using chroot so the crafted path reaches the server parser. +cat > rsyncd.conf <<'EOF' +pid file = rsyncd.pid +use chroot = true +[mod] + path = ./mod + read only = false +EOF +mkdir -p mod + +# Start daemon on a random high port. +PORT=$(awk 'BEGIN{srand(); printf "%d", 20000+int(rand()*20000)}') +"$RSYNC_BIN" --daemon --no-detach --config=rsyncd.conf --port="$PORT" >/dev/null 2>&1 & +DAEMON_PID=$! +# Give the daemon a moment to come up. +sleep 0.3 + +# Invoke the server-side path. We don't need a real transfer; we just want to +# ensure clean_fname() doesn't crash when given "a/../test" via --filter=merge. +EXIT_OK=0 +if "$RSYNC_BIN" --server --sender -vlr --filter='merge a/../test' . mod/ >/dev/null 2>&1; then + EXIT_OK=1 +else + status=$? + # Non-zero exit is expected for bogus input; ensure it wasn't a signal/crash. + if [ $status -lt 128 ]; then + EXIT_OK=1 + fi +fi + +kill "$DAEMON_PID" >/dev/null 2>&1 || true + +if [ "$EXIT_OK" -ne 1 ]; then + echo "clean-fname-underflow.test: rsync exited due to a signal or unexpected status" + exit 1 +fi + +echo "OK: clean_fname() handled 'a/../test' without crashing" +exit 0 From daea1474b7f03f536d6213e32c7e710c5710b7ee Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 23 Aug 2025 19:14:59 +1000 Subject: [PATCH 2/2] util: fixed issue in clean_fname() fixes buffer underflow (not exploitable) in clean_fname --- util1.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/util1.c b/util1.c index d84bc414..e65e0568 100644 --- a/util1.c +++ b/util1.c @@ -942,7 +942,7 @@ int count_dir_elements(const char *p) * resulting name would be empty, returns ".". */ int clean_fname(char *name, int flags) { - char *limit = name - 1, *t = name, *f = name; + char *limit = name, *t = name, *f = name; int anchored; if (!name) @@ -987,9 +987,13 @@ int clean_fname(char *name, int flags) f += 2; continue; } - while (s > limit && *--s != '/') {} - if (s != t - 1 && (s < name || *s == '/')) { - t = s + 1; + /* backing up for ".." — avoid reading before 'name' */ + while (s > limit && s[-1] != '/') + s--; + + /* If found prior '/', or we reached the start, adjust t. */ + if (s != t - 1 && (s <= name || *s == '/')) { + t = (s == name) ? name : s + 1; f += 2; continue; }