Convert CopyStat regression tests to Python suite

This commit is contained in:
parasol-aser
2026-04-30 16:20:46 +00:00
parent da05ef838c
commit 7d9d520712
17 changed files with 345 additions and 946 deletions

View File

@@ -0,0 +1,339 @@
#!/usr/bin/env python3
# Copyright 2026 The Brotli Authors. All rights reserved.
#
# Distributed under MIT license.
# See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
"""Regression suite for brotli CLI CopyStat behavior.
Usage:
python3 tests/regression/t01/copystat_regression_test.py /path/to/brotli
"""
import argparse
import contextlib
import hashlib
import os
from pathlib import Path
import shutil
import stat
import subprocess
import tempfile
import unittest
PLAIN_BYTES = b"A" * 65537
TARGET_BYTES = b"TARGET\n"
@contextlib.contextmanager
def umask(mask):
previous = os.umask(mask)
try:
yield
finally:
os.umask(previous)
def file_mode(path):
return stat.S_IMODE(os.stat(path).st_mode)
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
class CopyStatRegressionTest(unittest.TestCase):
brotli = None
def setUp(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(self.tmpdir.cleanup)
self.workdir = Path(self.tmpdir.name)
def write_plain(self, path):
path.write_bytes(PLAIN_BYTES)
def write_target(self, path):
path.write_bytes(TARGET_BYTES)
def run_brotli(self, *args, input_bytes=None, env=None, check=True):
proc = subprocess.run(
[str(self.brotli)] + [str(arg) for arg in args],
input=input_bytes,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
check=False)
if check and proc.returncode != 0:
self.fail(
"brotli exited with %d\nstdout:\n%s\nstderr:\n%s" % (
proc.returncode,
proc.stdout.decode("utf-8", "replace"),
proc.stderr.decode("utf-8", "replace")))
return proc
def compress(self, src, dst, no_copy_stat=False):
args = ["-f", "-k"]
if no_copy_stat:
args.append("-n")
args.extend([src, "-o", dst])
return self.run_brotli(*args)
def decompress(self, src, dst, no_copy_stat=False):
args = ["-d", "-f"]
if no_copy_stat:
args.append("-n")
args.extend([src, "-o", dst])
return self.run_brotli(*args)
def test_modes_propagate_on_compress_and_decompress(self):
modes = [0o600, 0o640, 0o644, 0o664, 0o660,
0o400, 0o444, 0o755, 0o700, 0o604]
with umask(0o022):
for mode in modes:
with self.subTest(mode="%03o" % mode):
case_dir = self.workdir / ("%03o" % mode)
case_dir.mkdir()
src = case_dir / "in.bin"
compressed = case_dir / "in.bin.br"
decoded = case_dir / "out.bin"
self.write_plain(src)
os.chmod(src, mode)
self.compress(src, compressed)
self.decompress(compressed, decoded)
self.assertEqual(file_mode(compressed), mode)
self.assertEqual(file_mode(decoded), mode)
self.assertEqual(decoded.read_bytes(), PLAIN_BYTES)
def test_special_mode_bits_are_masked(self):
with umask(0o022):
src = self.workdir / "in.bin"
compressed = self.workdir / "in.bin.br"
self.write_plain(src)
os.chmod(src, 0o3644)
actual_input_mode = file_mode(src)
if actual_input_mode == 0o644:
self.skipTest("filesystem stripped special mode bits from input")
self.compress(src, compressed)
compressed_mode = file_mode(compressed)
self.assertEqual(compressed_mode & 0o7000, 0)
self.assertEqual(compressed_mode & 0o777, actual_input_mode & 0o777)
def test_no_copy_stat_flag_suppresses_mode_copy(self):
with umask(0o022):
src = self.workdir / "in.bin"
without_copy = self.workdir / "without_copy.br"
with_copy = self.workdir / "with_copy.br"
self.write_plain(src)
os.chmod(src, 0o606)
self.compress(src, without_copy, no_copy_stat=True)
self.compress(src, with_copy)
self.assertNotEqual(file_mode(without_copy), 0o606)
self.assertEqual(file_mode(with_copy), 0o606)
def test_timestamp_is_copied(self):
with umask(0o022):
src = self.workdir / "in.bin"
compressed = self.workdir / "in.bin.br"
decoded = self.workdir / "out.bin"
self.write_plain(src)
expected_mtime = 1577934245
os.utime(src, (expected_mtime, expected_mtime))
self.compress(src, compressed)
self.decompress(compressed, decoded)
self.assertEqual(int(os.stat(compressed).st_mtime), expected_mtime)
self.assertEqual(int(os.stat(decoded).st_mtime), expected_mtime)
def test_stdin_input_has_no_path_to_copy_from(self):
payload = b"stdin -> compressed file\n"
compressed = self.run_brotli("-c", input_bytes=payload).stdout
decoded = self.workdir / "decoded.txt"
self.run_brotli("-d", "-o", decoded, input_bytes=compressed)
self.assertEqual(decoded.read_bytes(), payload)
self.assertEqual(file_mode(decoded) & ~0o666, 0)
def test_stdout_path_skips_copystat(self):
payload = b"hello brotli stdout pipeline\n"
compressed = self.run_brotli("-c", input_bytes=payload).stdout
decoded = self.run_brotli("-d", "-c", input_bytes=compressed).stdout
self.assertEqual(decoded, payload)
def test_content_roundtrip(self):
cases = {
"small-ascii": b"hello brotli\n",
"empty": b"",
"plain-65537": PLAIN_BYTES,
"aligned-65536": b"\0" * 65536,
"deterministic-binary": bytes((i * 73 + 41) % 256
for i in range(12288)),
}
with umask(0o022):
for label, data in cases.items():
with self.subTest(label=label):
case_dir = self.workdir / label
case_dir.mkdir()
src = case_dir / "in.bin"
compressed = case_dir / "in.bin.br"
decoded = case_dir / "out.bin"
src.write_bytes(data)
os.chmod(src, 0o644)
self.compress(src, compressed)
if data:
self.assertGreater(compressed.stat().st_size, 0)
self.decompress(compressed, decoded)
self.assertEqual(decoded.read_bytes(), data)
def test_fclose_swap_does_not_redirect_copystat(self):
compressed, out_path, target_path = self.prepare_attack_files()
pre_hash = sha256(target_path)
self.run_brotli_with_swap(compressed, out_path, target_path)
self.assertTrue(out_path.is_symlink())
self.assertEqual(file_mode(target_path), 0o600)
self.assertEqual(sha256(target_path), pre_hash)
def test_fclose_swap_with_no_copy_stat_leaves_target_unchanged(self):
compressed, out_path, target_path = self.prepare_attack_files()
pre_hash = sha256(target_path)
self.run_brotli_with_swap(compressed, out_path, target_path,
no_copy_stat=True)
self.assertTrue(out_path.is_symlink())
self.assertEqual(file_mode(target_path), 0o600)
self.assertEqual(sha256(target_path), pre_hash)
def prepare_attack_files(self):
src = self.workdir / "plain.bin"
compressed = self.workdir / "plain.bin.br"
out_path = self.workdir / "out"
target_path = self.workdir / "target.txt"
self.write_plain(src)
self.write_target(target_path)
os.chmod(src, 0o644)
os.chmod(target_path, 0o600)
self.compress(src, compressed)
os.chmod(compressed, 0o644)
return compressed, out_path, target_path
def run_brotli_with_swap(self, compressed, out_path, target_path,
no_copy_stat=False):
env = dict(os.environ)
env["BROTLI_SWAP_OUTPUT_ABS"] = str(out_path)
env["BROTLI_SWAP_TARGET_ABS"] = str(target_path)
env["LD_PRELOAD"] = self.ld_preload_value()
args = ["-d", "-f"]
if no_copy_stat:
args.append("-n")
args.extend(["-o", out_path, compressed])
proc = self.run_brotli(*args, env=env, check=False)
if proc.returncode != 0:
self.fail(
"brotli with fclose_swap exited with %d\nstdout:\n%s\nstderr:\n%s" % (
proc.returncode,
proc.stdout.decode("utf-8", "replace"),
proc.stderr.decode("utf-8", "replace")))
def ld_preload_value(self):
preloads = []
asan_runtime = self.asan_runtime()
if asan_runtime:
preloads.append(asan_runtime)
preloads.append(str(self.swap_helper()))
existing = os.environ.get("LD_PRELOAD")
if existing:
preloads.append(existing)
return ":".join(preloads)
def swap_helper(self):
if not Path("/proc/self/fd").is_dir():
self.skipTest("fclose_swap helper requires /proc/self/fd")
cc = shutil.which("cc")
if cc is None:
self.skipTest("cc is required to build fclose_swap.so")
helper_src = Path(__file__).with_name("fclose_swap.c")
helper_out = self.workdir / "libfclose_swap.so"
proc = subprocess.run(
[cc, "-shared", "-fPIC", str(helper_src), "-o", str(helper_out),
"-ldl"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False)
if proc.returncode != 0:
self.fail(
"failed to build fclose_swap.so\nstdout:\n%s\nstderr:\n%s" % (
proc.stdout.decode("utf-8", "replace"),
proc.stderr.decode("utf-8", "replace")))
return helper_out
def asan_runtime(self):
try:
proc = subprocess.run(["ldd", str(self.brotli)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False)
except FileNotFoundError:
return None
if b"libasan" not in proc.stdout + proc.stderr:
return None
cc = shutil.which("cc")
if cc is None:
return None
proc = subprocess.run([cc, "-print-file-name=libasan.so"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False)
candidate = proc.stdout.strip()
if candidate and Path(candidate).is_file():
return candidate
return None
def main():
parser = argparse.ArgumentParser()
parser.add_argument("brotli", help="path to the brotli CLI binary to test")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
brotli = Path(args.brotli).resolve()
if not brotli.is_file():
parser.error("%s is not a file" % brotli)
CopyStatRegressionTest.brotli = brotli
unittest.main(argv=[__file__], verbosity=2 if args.verbose else 1)
if __name__ == "__main__":
main()

View File

@@ -1,3 +1,9 @@
/* Copyright 2026 The Brotli Authors. All rights reserved.
Distributed under MIT license.
See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
*/
#define _GNU_SOURCE
#include <dlfcn.h>

File diff suppressed because one or more lines are too long

View File

@@ -1,59 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "usage: $0 /path/to/brotli /path/to/libfclose_swap.so" >&2
exit 2
fi
brotli_bin=$(readlink -f -- "$1")
swap_so=$(readlink -f -- "$2")
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d)
ld_preload="$swap_so"
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
cp -- "$script_dir/plain.bin" "$script_dir/plain.bin.br" \
"$script_dir/target.txt" "$workdir"/
chmod 0644 "$workdir/plain.bin"
chmod 0644 "$workdir/plain.bin.br"
chmod 0600 "$workdir/target.txt"
out_path="$workdir/out"
target_path="$workdir/target.txt"
if ldd "$brotli_bin" | grep -q 'libasan'; then
asan_runtime=$(cc -print-file-name=libasan.so)
if [[ -f "$asan_runtime" ]]; then
ld_preload="$asan_runtime:$ld_preload"
fi
fi
if [[ -n "${LD_PRELOAD:-}" ]]; then
ld_preload="$ld_preload:$LD_PRELOAD"
fi
env \
BROTLI_SWAP_OUTPUT_ABS="$out_path" \
BROTLI_SWAP_TARGET_ABS="$target_path" \
LD_PRELOAD="$ld_preload" \
"$brotli_bin" -d -o "$out_path" "$workdir/plain.bin.br"
if [[ ! -L "$out_path" ]]; then
echo "output path was not swapped to a symlink" >&2
exit 1
fi
if [[ "$(stat -c '%a' "$target_path")" != "644" ]]; then
echo "target mode was not flipped to 0644" >&2
exit 1
fi
if ! cmp -s -- "$workdir/target.txt" "$script_dir/target.txt"; then
echo "target contents changed; expected metadata-only effect" >&2
exit 1
fi
printf 'T-01 OK: target mode flipped to %s via post-close CopyStat path\n' \
"$(stat -c '%a' "$target_path")"

View File

@@ -1,99 +0,0 @@
#!/usr/bin/env bash
# T-01 master test runner.
#
# Runs all tests for google/brotli#1461 (CopyStat TOCTOU) and summarizes
# results. Tests that require the LD_PRELOAD helper receive its path as
# the second argument; tests that don't are invoked with the brotli binary
# only.
#
# Usage:
# run_all.sh /path/to/brotli /path/to/libfclose_swap.so
#
# Exit code: 0 if every test passed, non-zero otherwise.
#
# Individual tests exit with 0 on pass, non-zero on fail. Infrastructure
# failures from test_copystat_swap_fixed_strict.sh (exit 2) are surfaced
# distinctly in the summary.
set -u
if [[ $# -ne 2 ]]; then
echo "usage: $0 /path/to/brotli /path/to/libfclose_swap.so" >&2
exit 2
fi
brotli_bin="$1"
swap_so="$2"
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
# Tests that need only the brotli binary.
brotli_only=(
"test_copystat_positive.sh"
"test_copystat_various_modes.sh"
"test_copystat_mode_mask.sh"
"test_copystat_stdout_skip.sh"
"test_copystat_stdin_input.sh"
"test_copystat_no_copy_stat_flag.sh"
"test_copystat_roundtrip.sh"
"test_copystat_timestamp.sh"
)
# Tests that need both the brotli binary and the LD_PRELOAD helper.
attack_tests=(
"test_copystat_swap_fixed.sh"
"test_copystat_swap_fixed_strict.sh"
"test_copystat_swap_with_no_copy_stat.sh"
)
passed=0
failed=0
infra_fail=0
fail_names=()
infra_names=()
run_one() {
local script="$1"
shift
local label="$script"
printf '=== %s ===\n' "$label"
if "$script_dir/$script" "$@"; then
passed=$((passed + 1))
printf 'PASS: %s\n\n' "$label"
return 0
fi
local rc=$?
if [[ $rc -eq 2 ]]; then
infra_fail=$((infra_fail + 1))
infra_names+=("$label")
printf 'INFRA: %s (exit %d)\n\n' "$label" "$rc"
else
failed=$((failed + 1))
fail_names+=("$label")
printf 'FAIL: %s (exit %d)\n\n' "$label" "$rc"
fi
return $rc
}
for t in "${brotli_only[@]}"; do
run_one "$t" "$brotli_bin" || true
done
for t in "${attack_tests[@]}"; do
run_one "$t" "$brotli_bin" "$swap_so" || true
done
total=$((passed + failed + infra_fail))
printf -- '----- T-01 SUMMARY -----\n'
printf 'passed: %d / %d\n' "$passed" "$total"
if [[ $failed -ne 0 ]]; then
printf 'failed: %d\n' "$failed"
for n in "${fail_names[@]}"; do printf ' - %s\n' "$n"; done
fi
if [[ $infra_fail -ne 0 ]]; then
printf 'infra-failures: %d\n' "$infra_fail"
for n in "${infra_names[@]}"; do printf ' - %s\n' "$n"; done
fi
if [[ $failed -ne 0 || $infra_fail -ne 0 ]]; then
exit 1
fi
exit 0

View File

@@ -1 +0,0 @@
TARGET

View File

@@ -1,72 +0,0 @@
#!/usr/bin/env bash
# Mode-mask test for T-01.
#
# PLAN.md §3.1 specifies that CopyStat() calls:
# fchmod(fd, statbuf.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
#
# i.e. only the 9 low permission bits are copied. Setuid (04000), setgid
# (02000), and sticky (01000) bits MUST NOT propagate from the input to
# the output: that would be a silent privilege-escalation surprise.
#
# This test sets a setgid-like mode on the input (chmod will typically
# clear setuid on a non-executable file, so we test setgid + sticky),
# compresses, and asserts the compressed output's mode lacks those bits.
#
# Usage: test_copystat_mode_mask.sh /path/to/brotli
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 /path/to/brotli" >&2
exit 2
fi
brotli_bin=$(readlink -f -- "$1")
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d)
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
umask 0022
cp -- "$script_dir/plain.bin" "$workdir/in.bin"
# Set setgid + sticky + rw-r--r--. Setuid (04000) is often cleared by kernel
# on non-executable regular files owned by non-root; setgid (02000) is the
# portable bit we can reliably set without root.
chmod 03644 "$workdir/in.bin" || true
actual_input_mode=$(stat -c '%a' "$workdir/in.bin")
# Proceed only if the kernel accepted at least some special bit; otherwise
# the test cannot meaningfully assert anything.
if [[ "$actual_input_mode" == "644" ]]; then
echo "SKIP: filesystem/kernel stripped all special mode bits from input" >&2
exit 0
fi
"$brotli_bin" -fk "$workdir/in.bin" -o "$workdir/in.bin.br"
compressed_mode=$(stat -c '%a' "$workdir/in.bin.br")
# The compressed output should carry the 9 permission bits only (644), NOT
# any of the special bits. Extract the 3 lowest octal digits.
# stat -c '%a' returns either "NNN" or "NNNN" depending on whether any
# special bit is set in the output.
if [[ ${#compressed_mode} -eq 4 ]]; then
echo "FAIL: output carries special bits (mode=$compressed_mode); only 9" \
"permission bits should be copied" >&2
echo " input_mode=$actual_input_mode, output_mode=$compressed_mode" >&2
exit 1
fi
# Also assert the 9 perm bits match the input's 9 perm bits.
input_perm="${actual_input_mode: -3}"
if [[ "$compressed_mode" != "$input_perm" ]]; then
echo "FAIL: 9-bit permission part did not propagate (input=$input_perm," \
"output=$compressed_mode)" >&2
exit 1
fi
echo "T-01 MODE MASK: special bits not copied (input=$actual_input_mode," \
"output=$compressed_mode)"

View File

@@ -1,58 +0,0 @@
#!/usr/bin/env bash
# --no-copy-stat (-n) flag test for T-01.
#
# PLAN.md §4 row 1: when copy_stat == BROTLI_FALSE (user passed -n /
# --no-copy-stat), CopyStat() must not be called. The new CloseFiles()
# preserves this condition.
#
# We verify two things:
# 1. With -n, the output file does NOT inherit the input mode. Instead,
# it gets whatever the default mode is (derived from umask and the
# OpenOutputFile creat() mode).
# 2. Without -n, the same input reliably produces an output with the
# input's mode — this cross-checks that (1)'s behavior is due to the
# flag, not some other failure to propagate mode.
#
# Usage: test_copystat_no_copy_stat_flag.sh /path/to/brotli
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 /path/to/brotli" >&2
exit 2
fi
brotli_bin=$(readlink -f -- "$1")
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d)
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
# Set a predictable umask so the default-mode assertion below is stable.
umask 0022
cp -- "$script_dir/plain.bin" "$workdir/in.bin"
# Choose an unusual mode distinct from the 0644 default so we can detect
# whether it propagated. 0600 was already used by the swap repro; choose
# 0606 here to avoid colliding with "would-be 0644 with group stripped".
chmod 0606 "$workdir/in.bin"
# Case A: with -n, mode must NOT propagate.
"$brotli_bin" -fkn "$workdir/in.bin" -o "$workdir/out_n.br"
mode_n=$(stat -c '%a' "$workdir/out_n.br")
if [[ "$mode_n" == "606" ]]; then
echo "FAIL: with -n, mode 0606 propagated anyway — CopyStat was still called" >&2
exit 1
fi
# Case B: without -n, mode MUST propagate.
"$brotli_bin" -fk "$workdir/in.bin" -o "$workdir/out_y.br"
mode_y=$(stat -c '%a' "$workdir/out_y.br")
if [[ "$mode_y" != "606" ]]; then
echo "FAIL: without -n, mode 0606 did not propagate (got $mode_y)" >&2
exit 1
fi
echo "T-01 NO-COPY-STAT: -n suppressed CopyStat, default preserved elsewhere"

View File

@@ -1,68 +0,0 @@
#!/usr/bin/env bash
# Positive regression test for T-01 (google/brotli#1461).
#
# After the fix, CopyStat() uses fchmod() / fchown() via fileno(fout) to copy
# the input file's permission bits onto the output file. This test confirms
# that behavior is preserved by the fix (i.e. we did not regress by breaking
# CopyStat entirely).
#
# Creates a plaintext input with an unusual mode (0640, distinct from the
# typical 0644 default), decompresses a compressed copy to a fresh output
# path, and asserts the output file carries the input file's 0640 mode.
#
# Usage: test_copystat_positive.sh /path/to/brotli
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 /path/to/brotli" >&2
exit 2
fi
brotli_bin=$(readlink -f -- "$1")
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d)
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
# Default umask can mask out group/other bits on new files; start permissive.
umask 0022
cp -- "$script_dir/plain.bin" "$workdir/input.bin"
chmod 0640 "$workdir/input.bin"
# Compress (use -k to keep the input; output mode also gets CopyStat-copied).
"$brotli_bin" -fk "$workdir/input.bin" -o "$workdir/input.bin.br"
input_mode=$(stat -c '%a' "$workdir/input.bin")
compressed_mode=$(stat -c '%a' "$workdir/input.bin.br")
if [[ "$input_mode" != "640" ]]; then
echo "pre-condition failed: input mode is $input_mode, expected 640" >&2
exit 1
fi
if [[ "$compressed_mode" != "640" ]]; then
echo "POSITIVE FAIL: compress direction did not copy mode" >&2
echo " input mode = $input_mode, compressed mode = $compressed_mode (want 640)" >&2
exit 1
fi
# Decompress to a fresh output path.
rm -f -- "$workdir/out.bin"
"$brotli_bin" -d "$workdir/input.bin.br" -o "$workdir/out.bin"
out_mode=$(stat -c '%a' "$workdir/out.bin")
if [[ "$out_mode" != "640" ]]; then
echo "POSITIVE FAIL: decompress did not copy mode from input.bin.br to out.bin" >&2
echo " compressed mode = $compressed_mode, decompressed mode = $out_mode (want 640)" >&2
exit 1
fi
# Content round-trip check: decompressed output must match the original.
if ! cmp -s -- "$workdir/input.bin" "$workdir/out.bin"; then
echo "POSITIVE FAIL: decompressed content differs from original input" >&2
exit 1
fi
echo "T-01 POSITIVE: CopyStat still propagates 0640 through compress+decompress"

View File

@@ -1,79 +0,0 @@
#!/usr/bin/env bash
# Content-roundtrip regression test for T-01.
#
# PLAN.md §5.3 asks us to confirm the basic compress+decompress round-trip
# is not broken by the CopyStat reorder in CloseFiles(). The reorder moves
# CopyStat() before fclose(); if that reorder accidentally also moved or
# dropped data-writing code, content would diverge.
#
# Inputs tested:
# - Small ASCII text
# - Empty file (edge case: zero-length input)
# - Medium binary (plain.bin, 65537 bytes — odd size to hit unaligned paths)
# - A file exactly on a common buffer boundary (65536 bytes)
#
# For each: compress, then decompress, then cmp against the original.
# Also asserts each intermediate file exists and is non-zero where
# appropriate, which guards against "CopyStat ran before write completed"
# style regressions from a bad reorder.
#
# Usage: test_copystat_roundtrip.sh /path/to/brotli
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 /path/to/brotli" >&2
exit 2
fi
brotli_bin=$(readlink -f -- "$1")
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d)
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
umask 0022
roundtrip() {
local label="$1" src="$2"
local sub
sub=$(mktemp -d -p "$workdir")
cp -- "$src" "$sub/in.bin"
chmod 0644 "$sub/in.bin"
"$brotli_bin" -fk "$sub/in.bin" -o "$sub/in.bin.br"
if [[ ! -s "$sub/in.bin.br" && -s "$sub/in.bin" ]]; then
echo "FAIL [$label]: compressed output is empty but input was not" >&2
return 1
fi
"$brotli_bin" -df "$sub/in.bin.br" -o "$sub/out.bin"
if ! cmp -s -- "$sub/in.bin" "$sub/out.bin"; then
echo "FAIL [$label]: round-trip content mismatch" >&2
return 1
fi
}
# Case: small ASCII text.
printf 'hello brotli\n' > "$workdir/small.txt"
roundtrip "small-ascii" "$workdir/small.txt"
# Case: empty file.
: > "$workdir/empty.bin"
roundtrip "empty" "$workdir/empty.bin"
# Case: the stock 65537-byte plain.bin used by the T-01 assets.
roundtrip "plain-65537" "$script_dir/plain.bin"
# Case: exactly 65536 bytes (a buffer-boundary-adjacent size).
dd if=/dev/zero of="$workdir/aligned.bin" bs=65536 count=1 status=none
roundtrip "aligned-65536" "$workdir/aligned.bin"
# Case: high-entropy binary (so compression is a no-op-ish) to exercise the
# close path with minimal buffering differences.
dd if=/dev/urandom of="$workdir/random.bin" bs=4096 count=3 status=none
roundtrip "random-12288" "$workdir/random.bin"
echo "T-01 ROUNDTRIP: 5 input shapes round-tripped cleanly"

View File

@@ -1,86 +0,0 @@
#!/usr/bin/env bash
# Stdin-input test for T-01.
#
# PLAN.md §4 row 3: when input is stdin (context->current_input_path == NULL),
# CopyStat() early-returns on the input_path == NULL branch:
# if (input_path == NULL || fout == NULL) { return; }
# There is no fd-based metadata to copy *from*.
#
# We exercise this by piping content into brotli with an explicit -o output
# path. The input has no pathname; the output does. CopyStat() must:
# - not crash
# - not try to stat() a NULL path
# - not leave the output with a wrong mode (no mode is copied, so the
# output file just gets whatever the process umask / OpenOutputFile
# grants it)
#
# Usage: test_copystat_stdin_input.sh /path/to/brotli
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 /path/to/brotli" >&2
exit 2
fi
brotli_bin=$(readlink -f -- "$1")
workdir=$(mktemp -d)
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
umask 0022
payload='stdin -> compressed file'
out_path="$workdir/out.br"
stderr_log="$workdir/stderr.log"
# Compress from stdin to an explicit output file.
echo "$payload" | "$brotli_bin" -c > "$out_path.stdio" 2>"$stderr_log"
# Now decompress it from stdin to a real output file path (exercises the
# asymmetric case: NULL input_path, non-NULL output_path).
"$brotli_bin" -d -o "$workdir/decoded.txt" < "$out_path.stdio" \
2>>"$stderr_log"
# File exists and contents match.
if [[ ! -f "$workdir/decoded.txt" ]]; then
echo "FAIL: decoded.txt was not created" >&2
exit 1
fi
decoded=$(cat "$workdir/decoded.txt")
if [[ "$decoded" != "$payload" ]]; then
echo "FAIL: decoded content differs from payload" >&2
echo " got: $decoded" >&2
echo " wanted: $payload" >&2
exit 1
fi
# Sanity: the output file must have a sane regular-file mode.
# We explicitly do NOT assert any particular permission mode here, because
# when input is stdin there is no source mode to copy.
out_mode=$(stat -c '%a' "$workdir/decoded.txt")
case "$out_mode" in
"644"|"664"|"640"|"600"|"666") ;; # any typical creat()+umask outcome
*)
echo "WARN: unexpected output mode $out_mode (not a failure, just noting)" >&2
;;
esac
# Guard against brotli diagnostics caused by a null-path deref.
if [[ -s "$stderr_log" ]]; then
filtered=$(grep -v -E \
-e '^==[0-9]+==' \
-e '^SUMMARY:' \
-e 'AddressSanitizer' \
-e 'LeakSanitizer' \
-e 'UndefinedBehaviorSanitizer' \
"$stderr_log" || true)
if [[ -n "$filtered" ]]; then
echo "FAIL: unexpected diagnostics from NULL input_path path:" >&2
echo "$filtered" >&2
exit 1
fi
fi
echo "T-01 STDIN INPUT: CopyStat handled NULL input_path cleanly"

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env bash
# Stdout-skip test for T-01.
#
# PLAN.md §3.3 adds this guard in CloseFiles():
# if (!rm_output && context->copy_stat && context->current_output_path) {
# CopyStat(context->current_input_path, context->fout);
# }
# so that when current_output_path is NULL (stdout), CopyStat() is skipped
# entirely — not even called with a NULL path. PLAN.md §5.6 prescribes
# exercising this by running a compress|decompress pipe that uses stdout
# on both sides.
#
# Success criteria:
# - The pipe exits with status 0.
# - The decompressed bytes on the final stdout equal the original input.
# - No sanitizer/diagnostic output on stderr.
#
# Usage: test_copystat_stdout_skip.sh /path/to/brotli
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 /path/to/brotli" >&2
exit 2
fi
brotli_bin=$(readlink -f -- "$1")
workdir=$(mktemp -d)
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
payload='hello brotli stdout pipeline'
echo "$payload" > "$workdir/expected.txt"
stderr_log="$workdir/stderr.log"
# Both stages use stdin/stdout (-c). On the compress side, context->fout is
# bound to stdout and context->current_output_path should be NULL; same on
# the decompress side. CopyStat must therefore be skipped on both.
# We separate stderr so we can assert it is empty.
result=$(
echo "$payload" \
| "$brotli_bin" -c 2>"$stderr_log" \
| "$brotli_bin" -dc 2>>"$stderr_log"
)
if [[ "$result" != "$payload" ]]; then
echo "FAIL: stdout pipeline roundtrip did not preserve payload" >&2
echo " got: $result" >&2
echo " wanted: $payload" >&2
exit 1
fi
# The fix must not introduce spurious diagnostics on the stdout path.
# Some sanitizers may print LeakSanitizer summaries; accept those only.
if [[ -s "$stderr_log" ]]; then
# Filter sanitizer chatter that is not a brotli diagnostic.
filtered=$(grep -v -E \
-e '^==[0-9]+==' \
-e '^SUMMARY:' \
-e 'AddressSanitizer' \
-e 'LeakSanitizer' \
-e 'UndefinedBehaviorSanitizer' \
"$stderr_log" || true)
if [[ -n "$filtered" ]]; then
echo "FAIL: unexpected stderr output from stdout pipeline:" >&2
echo "$filtered" >&2
exit 1
fi
fi
echo "T-01 STDOUT SKIP: compress|decompress stdout pipeline clean"

View File

@@ -1,30 +0,0 @@
#!/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"

View File

@@ -1,100 +0,0 @@
#!/usr/bin/env bash
# Strict negative regression test for T-01.
#
# test_copystat_swap_fixed.sh simply inverts repro_copystat_swap.sh's exit
# status. That inversion conflates two failure modes inside the repro:
#
# (a) The symlink WAS placed but target mode did NOT flip — THE FIX WORKS.
# (b) The symlink was never placed — the LD_PRELOAD helper didn't run,
# meaning the TEST INFRASTRUCTURE failed, not the fix.
#
# Both make the repro exit non-zero, but only (a) means the vulnerability
# is fixed. This strict test replicates the repro setup inline so it can
# distinguish (a) from (b):
#
# - Pass: symlink placed AND target mode is still 0600.
# - Fail (regression): symlink placed AND target mode flipped to 0644.
# - Fail (infra): symlink never placed — the test cannot conclude
# anything about the fix.
#
# Usage: test_copystat_swap_fixed_strict.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
brotli_bin=$(readlink -f -- "$1")
swap_so=$(readlink -f -- "$2")
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d)
ld_preload="$swap_so"
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
cp -- "$script_dir/plain.bin" "$script_dir/plain.bin.br" \
"$script_dir/target.txt" "$workdir"/
chmod 0644 "$workdir/plain.bin"
chmod 0644 "$workdir/plain.bin.br"
chmod 0600 "$workdir/target.txt"
out_path="$workdir/out"
target_path="$workdir/target.txt"
pre_target_hash=$(sha256sum "$target_path" | awk '{print $1}')
if ldd "$brotli_bin" | grep -q 'libasan'; then
asan_runtime=$(cc -print-file-name=libasan.so)
if [[ -f "$asan_runtime" ]]; then
ld_preload="$asan_runtime:$ld_preload"
fi
fi
if [[ -n "${LD_PRELOAD:-}" ]]; then
ld_preload="$ld_preload:$LD_PRELOAD"
fi
env \
BROTLI_SWAP_OUTPUT_ABS="$out_path" \
BROTLI_SWAP_TARGET_ABS="$target_path" \
LD_PRELOAD="$ld_preload" \
"$brotli_bin" -d -o "$out_path" "$workdir/plain.bin.br" \
|| true # brotli may exit non-zero when it sees the symlinked output
# Infra check: the helper must have run and placed the symlink. If not,
# the conclusion is ambiguous.
#
# The helper fires on fclose() of the output fd. Under the fix, CopyStat()
# runs BEFORE fclose(), which is fine — fclose() is still called, and the
# helper still swaps the path after that fclose() returns. So we expect
# the symlink to be placed regardless of whether the fix is in effect.
if [[ ! -L "$out_path" ]]; then
echo "INFRA FAIL: symlink was never placed at $out_path" >&2
echo " The LD_PRELOAD fclose_swap helper did not fire." >&2
echo " Can't conclude whether the fix works. Check LD_PRELOAD, ASan" \
"interaction, and helper build." >&2
exit 2
fi
# Real check: target mode must still be 0600.
post_mode=$(stat -c '%a' "$target_path")
post_target_hash=$(sha256sum "$target_path" | awk '{print $1}')
if [[ "$post_mode" == "644" ]]; then
echo "REGRESSION: target mode flipped 0600 -> 0644 via post-close" \
"CopyStat path" >&2
echo " The T-01 TOCTOU is reproducible — the fix has regressed." >&2
exit 1
fi
if [[ "$post_mode" != "600" ]]; then
echo "REGRESSION (unexpected): target mode is $post_mode (expected 600)" >&2
exit 1
fi
if [[ "$pre_target_hash" != "$post_target_hash" ]]; then
echo "REGRESSION: target content changed (hash differs)" >&2
exit 1
fi
echo "T-01 FIXED STRICT: swap was placed but target remained 0600 unchanged"

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env bash
# Swap-under--n test for T-01.
#
# Belt-and-suspenders check: even under the LD_PRELOAD fclose-swap attack,
# passing -n (which skips CopyStat entirely) must leave target.txt's mode
# unchanged. This test runs the swap repro with -n injected into the brotli
# command line — CopyStat never runs, so the symlink is irrelevant and the
# target's mode must stay 0600.
#
# This is a defense-in-depth test: if a future change were to wire
# CopyStat() up on a code path that ignored copy_stat, this test would
# catch it even if the primary fix is intact.
#
# We inline the attack setup here instead of reusing repro_copystat_swap.sh
# because that script hard-codes the brotli arg list. The same helper
# (libfclose_swap.so) is reused as-is.
#
# Usage: test_copystat_swap_with_no_copy_stat.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
brotli_bin=$(readlink -f -- "$1")
swap_so=$(readlink -f -- "$2")
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d)
ld_preload="$swap_so"
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
cp -- "$script_dir/plain.bin" "$script_dir/plain.bin.br" \
"$script_dir/target.txt" "$workdir"/
chmod 0644 "$workdir/plain.bin"
chmod 0644 "$workdir/plain.bin.br"
chmod 0600 "$workdir/target.txt"
out_path="$workdir/out"
target_path="$workdir/target.txt"
if ldd "$brotli_bin" | grep -q 'libasan'; then
asan_runtime=$(cc -print-file-name=libasan.so)
if [[ -f "$asan_runtime" ]]; then
ld_preload="$asan_runtime:$ld_preload"
fi
fi
if [[ -n "${LD_PRELOAD:-}" ]]; then
ld_preload="$ld_preload:$LD_PRELOAD"
fi
env \
BROTLI_SWAP_OUTPUT_ABS="$out_path" \
BROTLI_SWAP_TARGET_ABS="$target_path" \
LD_PRELOAD="$ld_preload" \
"$brotli_bin" -d -n -o "$out_path" "$workdir/plain.bin.br" || true
# With -n, CopyStat never runs, so target.txt must remain 0600.
mode=$(stat -c '%a' "$target_path")
if [[ "$mode" != "600" ]]; then
echo "FAIL: target mode changed to $mode under -n; CopyStat was called" \
"despite copy_stat==BROTLI_FALSE" >&2
exit 1
fi
# Bonus: the content must also be unchanged.
if ! cmp -s -- "$workdir/target.txt" "$script_dir/target.txt"; then
echo "FAIL: target content changed under -n attack" >&2
exit 1
fi
echo "T-01 SWAP+NO-COPY-STAT: target unchanged under attack with -n"

View File

@@ -1,67 +0,0 @@
#!/usr/bin/env bash
# Timestamp-copy test for T-01.
#
# PLAN.md §3.2 replaces path-based utime() with fd-based futimens() under
# HAVE_UTIMENSAT. On Linux we always have HAVE_UTIMENSAT, so this test
# validates futimens() is actually invoked with the input's mtime.
#
# Procedure:
# 1. Create an input file and set its mtime to a well-known past date.
# 2. Sleep briefly so the ambient "now" drifts from that mtime.
# 3. Compress. The compressed output should have the same mtime as the
# input (give or take filesystem-timestamp resolution).
# 4. Decompress to a fresh path. The decompressed output should also
# carry the input's mtime (actually: the compressed file's mtime,
# which is the input's mtime from step 3).
#
# This guards against:
# - CopyTimeStat refactor dropping the call entirely.
# - The futimens path using wrong indices or wrong struct fields.
#
# Usage: test_copystat_timestamp.sh /path/to/brotli
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 /path/to/brotli" >&2
exit 2
fi
brotli_bin=$(readlink -f -- "$1")
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d)
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
umask 0022
cp -- "$script_dir/plain.bin" "$workdir/in.bin"
# Pinned mtime in the past (well before the current CI run).
past="202001020304.05"
touch -m -t "$past" "$workdir/in.bin"
expected_mtime=$(stat -c '%Y' "$workdir/in.bin")
# Small drift to ensure "now" != expected_mtime in the default-create case.
sleep 1
"$brotli_bin" -fk "$workdir/in.bin" -o "$workdir/in.bin.br"
compressed_mtime=$(stat -c '%Y' "$workdir/in.bin.br")
if [[ "$compressed_mtime" != "$expected_mtime" ]]; then
echo "FAIL: compressed mtime ($compressed_mtime) != input mtime" \
"($expected_mtime)" >&2
exit 1
fi
"$brotli_bin" -df "$workdir/in.bin.br" -o "$workdir/out.bin"
decompressed_mtime=$(stat -c '%Y' "$workdir/out.bin")
if [[ "$decompressed_mtime" != "$expected_mtime" ]]; then
echo "FAIL: decompressed mtime ($decompressed_mtime) != input mtime" \
"($expected_mtime)" >&2
exit 1
fi
echo "T-01 TIMESTAMP: futimens copied mtime through compress+decompress"

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env bash
# Mode-matrix test for T-01. Exercises fchmod over the 9 permission bits
# (S_IRWXU | S_IRWXG | S_IRWXO) that CopyStat() is specified to copy.
#
# For each canonical mode below, set the input file mode, compress, and
# verify the compressed output carries the same mode. Also decompress and
# verify the decompressed output carries the same mode.
#
# The mode mask documented in PLAN.md §3.1 is:
# statbuf.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)
# i.e. only the low 9 bits. Any setuid/setgid/sticky bits (02000, 04000,
# 01000) MUST NOT leak through — that is tested separately in
# test_copystat_mode_mask.sh.
#
# Usage: test_copystat_various_modes.sh /path/to/brotli
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 /path/to/brotli" >&2
exit 2
fi
brotli_bin=$(readlink -f -- "$1")
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d)
cleanup() {
rm -rf -- "$workdir"
}
trap cleanup EXIT
umask 0022
# Representative modes: owner-only, group-readable, world-readable,
# world-readable-exec, and asymmetric combinations that round-trip cleanly
# through fchmod() but would be corrupted by any accidental umask masking.
modes=(
"600"
"640"
"644"
"664"
"660"
"400"
"444"
"755"
"700"
"604"
)
failures=0
for mode in "${modes[@]}"; do
sub=$(mktemp -d -p "$workdir")
cp -- "$script_dir/plain.bin" "$sub/in.bin"
chmod "$mode" "$sub/in.bin"
"$brotli_bin" -fk "$sub/in.bin" -o "$sub/in.bin.br"
compressed_mode=$(stat -c '%a' "$sub/in.bin.br")
"$brotli_bin" -df "$sub/in.bin.br" -o "$sub/out.bin"
decompressed_mode=$(stat -c '%a' "$sub/out.bin")
if [[ "$compressed_mode" != "$mode" ]]; then
echo "FAIL mode=$mode: compress produced $compressed_mode" >&2
failures=$((failures + 1))
fi
if [[ "$decompressed_mode" != "$mode" ]]; then
echo "FAIL mode=$mode: decompress produced $decompressed_mode" >&2
failures=$((failures + 1))
fi
done
if [[ $failures -ne 0 ]]; then
echo "T-01 MODE MATRIX: $failures failures across ${#modes[@]} modes" >&2
exit 1
fi
echo "T-01 MODE MATRIX: all ${#modes[@]} modes propagated correctly"