From d7d5d4c1a72929e4436a14bb2a83ae6a03f54d3d Mon Sep 17 00:00:00 2001 From: Nagendra Mohan Date: Wed, 19 Aug 2026 22:55:43 +0530 Subject: [PATCH 1/3] tail: don't panic on '-c +N' with a huge N MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tail -c +N FILE` on a regular file larger than the block size seeks to byte N-1. When N-1 exceeds what the OS can seek to (any offset above i64::MAX always fails with EINVAL), the `.unwrap()` aborted the process. Clamp the start offset to the file length. A start beyond the end of the file produces no output (matching GNU `tail`), and clamping also avoids seeking to an offset the OS cannot represent — so the pathological case yields empty output instead of panicking. Behavior is unchanged for valid offsets. Adds a regression test (verified failing before the fix). Closes #13887. --- src/uu/tail/src/tail.rs | 8 +++++++- tests/by-util/test_tail.rs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index db1b6a5456..1714537b10 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -480,7 +480,13 @@ fn bounded_tail(file: &mut File, settings: &Settings) -> UResult<()> { FilterMode::Bytes(Signum::Positive(count)) if count > &1 => { // GNU `tail` seems to index bytes and lines starting at 1, not // at 0. It seems to treat `+0` and `+1` as the same thing. - file.seek(SeekFrom::Start(*count - 1)).unwrap(); + // + // Clamp the start offset to the file length. A start beyond the end of the file + // produces no output; clamping also avoids seeking to an offset the OS cannot + // represent (anything above `i64::MAX` fails with `EINVAL`), which previously + // panicked via `.unwrap()`. See #13887. + let len = file.metadata()?.len(); + file.seek(SeekFrom::Start((*count - 1).min(len)))?; } _ => {} } diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 985d3702ab..60c2198b38 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -857,6 +857,22 @@ fn test_bytes_single() { .stdout_is_fixture("foobar_bytes_single.expected"); } +#[test] +fn test_positive_bytes_overflowing_offset_does_not_panic() { + // Regression test for #13887: `tail -c +N` on a regular file larger than the block size seeks + // to byte N-1. A very large N (greater than `i64::MAX`) makes the seek fail with `EINVAL`; + // `tail` used to `.unwrap()` and abort. That start is past the end of the file, so the output + // must simply be empty. + let (at, mut ucmd) = at_and_ucmd!(); + // Larger than sane_blksize (~4 KiB) so the seek code path is taken. + at.write("big", &"x".repeat(8192)); + ucmd.arg("-c") + .arg("+18446744073709551615") // u64::MAX + .arg("big") + .succeeds() + .no_output(); +} + #[test] fn test_bytes_stdin() { new_ucmd!() From bdcabfdc5bd7605e0ed3ff7514987330b7ff214b Mon Sep 17 00:00:00 2001 From: Nagendra Mohan Date: Thu, 20 Aug 2026 09:14:22 +0530 Subject: [PATCH 2/3] tail: shorten the code comment per review Signed-off-by: Nagendra Mohan --- src/uu/tail/src/tail.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 1714537b10..25a467fc51 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -480,11 +480,8 @@ fn bounded_tail(file: &mut File, settings: &Settings) -> UResult<()> { FilterMode::Bytes(Signum::Positive(count)) if count > &1 => { // GNU `tail` seems to index bytes and lines starting at 1, not // at 0. It seems to treat `+0` and `+1` as the same thing. - // - // Clamp the start offset to the file length. A start beyond the end of the file - // produces no output; clamping also avoids seeking to an offset the OS cannot - // represent (anything above `i64::MAX` fails with `EINVAL`), which previously - // panicked via `.unwrap()`. See #13887. + // Clamp to the file length so a start past EOF (or above `i64::MAX`) yields empty + // output instead of panicking on an `EINVAL` seek (#13887). let len = file.metadata()?.len(); file.seek(SeekFrom::Start((*count - 1).min(len)))?; } From d9d8821b3ce4d3ddace82c0f1d4e7c705e8c5627 Mon Sep 17 00:00:00 2001 From: Nagendra Mohan Date: Fri, 21 Aug 2026 10:45:42 +0530 Subject: [PATCH 3/3] tail: seek to end on unseekable offset instead of clamping via metadata The previous clamp used file.metadata().len(), which is 0 for device files and made '-c +N' on a device seek to 0 (regressing the GNU end-of-device test). Restore the original success-path behavior: seek to N-1 directly, and only on a seek error (e.g. offset > i64::MAX -> EINVAL) fall back to the end of the input, yielding empty output instead of panicking. Signed-off-by: Nagendra Mohan --- src/uu/tail/src/tail.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 25a467fc51..40deb35ce2 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -480,10 +480,11 @@ fn bounded_tail(file: &mut File, settings: &Settings) -> UResult<()> { FilterMode::Bytes(Signum::Positive(count)) if count > &1 => { // GNU `tail` seems to index bytes and lines starting at 1, not // at 0. It seems to treat `+0` and `+1` as the same thing. - // Clamp to the file length so a start past EOF (or above `i64::MAX`) yields empty - // output instead of panicking on an `EINVAL` seek (#13887). - let len = file.metadata()?.len(); - file.seek(SeekFrom::Start((*count - 1).min(len)))?; + // A start offset the OS cannot seek to (e.g. above `i64::MAX`, which fails with + // `EINVAL`) is past the end of the input, so fall back to seeking to the end and + // producing empty output instead of panicking (#13887). + file.seek(SeekFrom::Start(*count - 1)) + .or_else(|_| file.seek(SeekFrom::End(0)))?; } _ => {} }