Skip to content

Commit b5a49f3

Browse files
authored
tac: use temp file for stdin to respect TMPDIR and handle disk-full errors (#10094)
1 parent 6d1df56 commit b5a49f3

File tree

7 files changed

+94
-60
lines changed

7 files changed

+94
-60
lines changed

.vscode/cspell.dictionaries/jargon.wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,4 @@ TUNABLES
215215
tunables
216216
VMULL
217217
vmull
218+
tmpfs

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fuzz/Cargo.lock

Lines changed: 36 additions & 53 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/tac/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ clap = { workspace = true }
2727
uucore = { workspace = true }
2828
thiserror = { workspace = true }
2929
fluent = { workspace = true }
30+
tempfile = { workspace = true }
3031

3132
[[bin]]
3233
name = "tac"

src/uu/tac/src/tac.rs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::ffi::OsString;
1313
use std::io::{BufWriter, Read, Write, stdin, stdout};
1414
use std::{
1515
fs::{File, read},
16+
io::copy,
1617
path::Path,
1718
};
1819
use uucore::error::UError;
@@ -241,14 +242,22 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR
241242
mmap = mmap1;
242243
&mmap
243244
} else {
244-
let mut buf1 = Vec::new();
245-
if let Err(e) = stdin().read_to_end(&mut buf1) {
246-
let e: Box<dyn UError> = TacError::ReadError(OsString::from("stdin"), e).into();
247-
show!(e);
248-
continue;
245+
// Copy stdin to a temp file (respects TMPDIR), then mmap it.
246+
// Falls back to Vec buffer if temp file creation fails (e.g., bad TMPDIR).
247+
match buffer_stdin() {
248+
Ok(StdinData::Mmap(mmap1)) => {
249+
mmap = mmap1;
250+
&mmap
251+
}
252+
Ok(StdinData::Vec(buf1)) => {
253+
buf = buf1;
254+
&buf
255+
}
256+
Err(e) => {
257+
show!(TacError::ReadError(OsString::from("stdin"), e));
258+
continue;
259+
}
249260
}
250-
buf = buf1;
251-
&buf
252261
}
253262
} else {
254263
let path = Path::new(filename);
@@ -304,6 +313,30 @@ fn try_mmap_stdin() -> Option<Mmap> {
304313
unsafe { Mmap::map(&stdin()).ok() }
305314
}
306315

316+
enum StdinData {
317+
Mmap(Mmap),
318+
Vec(Vec<u8>),
319+
}
320+
321+
/// Copy stdin to a temp file, then memory-map it.
322+
/// Falls back to reading directly into memory if temp file creation fails.
323+
fn buffer_stdin() -> std::io::Result<StdinData> {
324+
// Try to create a temp file (respects TMPDIR)
325+
if let Ok(mut tmp) = tempfile::tempfile() {
326+
// Temp file created - copy stdin to it, then read back
327+
copy(&mut stdin(), &mut tmp)?;
328+
// SAFETY: If the file is truncated while we map it, SIGBUS will be raised
329+
// and our process will be terminated, thus preventing access of invalid memory.
330+
let mmap = unsafe { Mmap::map(&tmp)? };
331+
Ok(StdinData::Mmap(mmap))
332+
} else {
333+
// Fall back to reading directly into memory (e.g., bad TMPDIR)
334+
let mut buf = Vec::new();
335+
stdin().read_to_end(&mut buf)?;
336+
Ok(StdinData::Vec(buf))
337+
}
338+
}
339+
307340
fn try_mmap_path(path: &Path) -> Option<Mmap> {
308341
let file = File::open(path).ok()?;
309342

tests/by-util/test_tac.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,15 @@ fn test_failed_write_is_reported() {
335335
.fails()
336336
.stderr_is("tac: failed to write to stdout: No space left on device (os error 28)\n");
337337
}
338+
339+
#[cfg(target_os = "linux")]
340+
#[test]
341+
fn test_stdin_bad_tmpdir_fallback() {
342+
// When TMPDIR is invalid, tac falls back to reading stdin directly into memory
343+
new_ucmd!()
344+
.env("TMPDIR", "/nonexistent/dir")
345+
.arg("-")
346+
.pipe_in("a\nb\nc\n")
347+
.succeeds()
348+
.stdout_is("c\nb\na\n");
349+
}

util/fetch-gnu.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ curl -L ${repo}/raw/refs/heads/master/tests/csplit/csplit-io-err.sh > tests/cspl
1212
curl -L ${repo}/raw/refs/heads/master/tests/stty/bad-speed.sh > tests/stty/bad-speed.sh
1313
# Avoid incorrect PASS
1414
curl -L ${repo}/raw/refs/heads/master/tests/runcon/runcon-compute.sh > tests/runcon/runcon-compute.sh
15+
curl -L ${repo}/raw/refs/heads/master/tests/tac/tac-continue.sh > tests/tac/tac-continue.sh
16+
# Add tac-continue.sh to root tests (it requires root to mount tmpfs)
17+
sed -i 's|tests/split/l-chunk-root.sh.*|tests/split/l-chunk-root.sh\t\t\t\\\n tests/tac/tac-continue.sh\t\t\t\\|' tests/local.mk

0 commit comments

Comments
 (0)