Files
brotli/tests/regression/t01/test_copystat_swap_fixed.sh
parasol-aser da05ef838c Fix CopyStat TOCTOU in brotli CLI by using fd-based syscalls
CloseFiles() previously called fclose(context->fout) before invoking
CopyStat() on the output pathname. CopyStat() then used path-based
chmod() and chown(), which gave an attacker with write access to the
output directory a window to replace the path with a symlink between
the close and the metadata syscalls, redirecting the chmod/chown to an
arbitrary target.

Close the race by doing the metadata copy while the output file is
still open and by switching to fd-based syscalls on fileno(fout):

- CopyStat() now takes FILE* fout and calls fchmod()/fchown() on the
  underlying fd.
- CopyTimeStat() uses futimens(fd, ...) in the HAVE_UTIMENSAT branch;
  the legacy utime() fallback is preserved for platforms without it.
- CloseFiles() invokes CopyStat() before fclose() and skips it when
  current_output_path is NULL (stdout), matching the issue's guidance.
- Windows no-op shims updated from chmod/chown to fchmod/fchown.

Adds deterministic regression coverage under tests/regression/t01/:
an LD_PRELOAD fclose() swap reproduces the pre-fix behavior, and a
negative-assertion wrapper confirms the attack no longer succeeds
after the patch. Additional scripts cover mode/timestamp propagation,
the -n (no-copy-stat) flag, stdin input, stdout output, mode mask, and
roundtrip correctness.

Refs google/brotli#1461
2026-04-30 17:19:28 +00:00

31 lines
1.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Negative regression test for T-01 (google/brotli#1461).
#
# Wraps repro_copystat_swap.sh and INVERTS the success condition.
#
# Before the fix: the inner repro script exits 0 and flips target.txt mode
# from 0600 -> 0644 via a post-close symlink swap.
# After the fix: CopyStat() runs BEFORE fclose(), uses fchmod() on the still-
# open fd, and can no longer be redirected at a swapped pathname. The target
# file mode must remain 0600, and the inner script must therefore exit non-
# zero. A non-zero exit from the inner script is what we want.
#
# Usage: test_copystat_swap_fixed.sh /path/to/brotli /path/to/libfclose_swap.so
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "usage: $0 /path/to/brotli /path/to/libfclose_swap.so" >&2
exit 2
fi
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
if "$script_dir/repro_copystat_swap.sh" "$@" >/dev/null 2>&1; then
echo "REGRESSION: T-01 post-close CopyStat TOCTOU is reproducible again" >&2
echo " The attacker still got target.txt mode flipped to 0644." >&2
echo " The fix in CloseFiles() / CopyStat() has regressed." >&2
exit 1
fi
echo "T-01 FIXED: CopyStat no longer follows post-close symlink swap"