diff --git a/batch.c b/batch.c index a4d19c058..0027a5029 100644 --- a/batch.c +++ b/batch.c @@ -272,10 +272,12 @@ void open_batch_files(void) } /* --read-batch: the file's bytes drive the protocol parser, so refuse - * non-regular files (FIFO, device, socket) at the batch path. */ + * non-regular files (device, socket) at the batch path. + * pipes should be accepted to avoid blocking Bash substitute. processes (eg. /dev/fd/63.) */ + if (!write_batch && batch_fd != STDIN_FILENO) { STRUCT_STAT st; - if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode)) { + if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode) && !S_ISFIFO(st.st_mode)) { rprintf(FERROR, "Batch file %s is not a regular file\n", full_fname(batch_name)); exit_cleanup(RERR_FILEIO); diff --git a/testsuite/read-batch-pipe_test.py b/testsuite/read-batch-pipe_test.py new file mode 100644 index 000000000..d78b2d568 --- /dev/null +++ b/testsuite/read-batch-pipe_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""--read-batch process substitution /dev/fd/ pipe must not crash with strict file-type checks.""" + +import os +import shlex +import shutil +import subprocess +import tempfile +from pathlib import Path + +from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped + +# We require bash specifically because standard POSIX /bin/sh does not +# guarantee support for <(...) process substitution syntax. +bash = shutil.which('bash') +if bash is None: + test_skipped('bash is unavailable, cannot test process substitution') + +# Verify the host bash actually supports process substitution +probe = subprocess.run( + [bash, '-c', 'cat <(echo "probe")'], + capture_output=True) + +if probe.returncode != 0: + test_skipped('bash process substitution is not supported on this system') + +base = Path(SCRATCHDIR / 'rsync-batch-fifo') +src = base / 'src' +dest = base / 'dest' +batch_file = base / 'update.batch' +makepath(src, dest) + +# 1. Create dummy data +(src / 'payload.txt').write_text('batch payload data\n') + +# 2. ---> THE MISSING STEP <--- +# Generate a valid batch file so `cat` actually has a real file to read. +subprocess.run([*rsync_argv('-a', f'--write-batch={batch_file}'), f'{src}/', f'{dest}/'], check=True) + + +# 3. Now we can test reading it via bash process substitution +rsync_base_cmd = shlex.join(rsync_argv('-a')) +batch_path = shlex.quote(str(batch_file)) +dest_path = shlex.quote(str(dest) + '/') + +# Construct the bash command: rsync -a --read-batch=<(cat /path/to/batch) /dest/ +bash_script = f"{rsync_base_cmd} --read-batch=<(cat {batch_path}) {dest_path}" + +try: + proc_read = subprocess.run( + [bash, '-c', bash_script], + capture_output=True, + text=True, + timeout=10, + ) +except subprocess.TimeoutExpired: + rmtree(base) + test_fail('process substitution batch test timed out') + +ctx = f'rc={proc_read.returncode}, stderr={proc_read.stderr.strip()!r}' + +# Evaluate result against the strict S_ISREG check bug +if proc_read.returncode != 0: + rmtree(base) + test_fail(f'rsync crashed reading batch file from pipe ({ctx})') + +if not (dest / 'payload.txt').is_file(): + rmtree(base) + test_fail(f'rsync exited successfully but payload is missing in target ({ctx})') + +rmtree(base) +print('rsync successfully parsed batch stream via process substitution pseudo-path') +raise SystemExit(0) + diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt index ba499cc54..2f6f63638 100644 --- a/testsuite/skiplist/cygwin.txt +++ b/testsuite/skiplist/cygwin.txt @@ -52,6 +52,7 @@ partial-protected-regular-retry-linux partial-protected-regular-retry-policy # deterministic partial EACCES recovery uses dyld interposing password-file-symlink protected-regular +read-batch-pipe rename-mixed-parent-transfer rrsync-sender-leaf-flip rrsync-sender-parent-pin diff --git a/testsuite/skiplist/macos.txt b/testsuite/skiplist/macos.txt index 3e7653ce8..b862d68b8 100644 --- a/testsuite/skiplist/macos.txt +++ b/testsuite/skiplist/macos.txt @@ -22,6 +22,7 @@ open-noatime partial-protected-regular-retry-linux preallocate protected-regular +read-batch-pipe readonly-partial-abort-mode-regression # rrsync-sender-leaf-flip rrsync-sender-parent-pin