Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions batch.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
74 changes: 74 additions & 0 deletions testsuite/read-batch-pipe_test.py
Original file line number Diff line number Diff line change
@@ -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)

1 change: 1 addition & 0 deletions testsuite/skiplist/cygwin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions testsuite/skiplist/macos.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading