From ff2169b8812fc44f3dd2c708475d58cc9ba2d425 Mon Sep 17 00:00:00 2001 From: arbelonson-source <269032023+arbelonson-source@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:39:00 +0300 Subject: [PATCH] tr: stop an absurd [c*N] repeat count from crashing or hanging An explicit repeat count in a [c*N] construct (as opposed to the open-ended [c*], which is already bounded by the other set's real length) was taken completely literally: Sequence::flatten's lazy std::iter::repeat_n(c, n) got collected into an N-byte Vec with no upper bound, both when solving set1 and when solving set2. tr '[a*999999999999]' x aborts the process outright (a ~1 TB allocation request); a merely huge rather than astronomical N -- still far more than any real command-line argument could need -- hangs for seconds to minutes instead, since materializing and then iterating N bytes has a real cost even when the allocation itself succeeds. Every position past the point where the other set's real (bounded, by construction, to whatever a single shell argument can hold) length runs out collapses to the same result regardless of exactly how much further N reaches, since that's tr's own "pad the shorter set with its last element" rule -- confirmed empirically: GNU tr's output for [a*N] x is identical for N from 1 up to the largest N it can complete in reasonable time (50000), and for a matched pair of one-million character sets, this fix's output is byte-identical to GNU's own. GNU itself is not immune to this class of input, though: [a*999999999999] x hangs GNU tr too (confirmed directly, contradicting an initial, unverified report that it handled the value gracefully) -- a [x*999999999999] does not, which is the more common practical case (padding one set to match a long literal replacement) and completes instantly on both GNU and, after this fix, on this implementation. Capping the parsed repeat count at 2 MiB -- far more headroom than any single shell argument can realistically contain, confirmed directly: attempting to even construct the equivalent real arguments to test at exactly that boundary hit the OS's own ARG_MAX first -- keeps every realistic use working exactly as before while turning both the crash and the hang into an instant, correct result. AI-assisted-by: Claude Opus 5, via Claude Code --- src/uu/tr/src/operation.rs | 16 ++++++++++++++-- tests/by-util/test_tr.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index 8c503490ff2..4927d51de0b 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -592,6 +592,18 @@ impl Sequence { .map(|(l, a)| (l, Ok(Self::CharStar(a)))) } + // `[c*N]` only ever needs to be as long as the other set it is padding + // out to match, which -- since it comes from a single command-line + // argument -- cannot realistically exceed a few MiB. A literal N well + // beyond that (nothing stops someone writing `[a*999999999999]`) still + // produces the exact same translation as this cap does, since every + // position past the other set's real length collapses to the same + // "pad with the last element" rule regardless of how much further past + // it N actually reaches; not capping it means materializing N literal + // bytes downstream, which is either an out-of-memory abort or, for a + // merely huge rather than astronomical N, a multi-second hang. + const MAX_CHAR_REPEAT: usize = 2 * 1024 * 1024; + fn parse_char_repeat(input: &[u8]) -> IResult<&[u8], Result> { delimited( tag("["), @@ -611,13 +623,13 @@ impl Sequence { let result = if cnt_str.starts_with(b"0") { match usize::from_str_radix(&s, 8) { Ok(0) => Ok(Self::CharStar(c)), - Ok(count) => Ok(Self::CharRepeat(c, count)), + Ok(count) => Ok(Self::CharRepeat(c, count.min(Self::MAX_CHAR_REPEAT))), Err(_) => Err(BadSequence::InvalidRepeatCount(s.to_string())), } } else { match s.parse::() { Ok(0) => Ok(Self::CharStar(c)), - Ok(count) => Ok(Self::CharRepeat(c, count)), + Ok(count) => Ok(Self::CharRepeat(c, count.min(Self::MAX_CHAR_REPEAT))), Err(_) => Err(BadSequence::InvalidRepeatCount(s.to_string())), } }; diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index 493fb1916a8..a6164c1aba0 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -993,6 +993,32 @@ fn tr_translate_overlap_repeat_squeeze() { .stdout_is("BCx"); } +#[test] +fn tr_huge_repeat_count_does_not_crash_or_hang() { + // An explicit [c*N] repeat count far larger than any set actually + // needs (here, a trillion) must not be taken literally: the value + // used to be materialized into an N-byte Vec, which is either an + // out-of-memory abort for an N this size or, for a merely huge + // rather than astronomical N, a multi-second hang -- while every N + // beyond what the other set actually needs produces the identical + // translation regardless of its exact size. + new_ucmd!() + .args(&["[a*999999999999]", "x"]) + .pipe_in("abc") + .succeeds() + .stdout_is("xbc"); + new_ucmd!() + .args(&["a", "[x*999999999999]"]) + .pipe_in("abc") + .succeeds() + .stdout_is("xbc"); + new_ucmd!() + .args(&["-d", "[a*999999999999]"]) + .pipe_in("abc") + .succeeds() + .stdout_is("bc"); +} + #[test] fn octal_repeat_count_test() { //below will result in 8'x' and 4'y' as octal 010 = decimal 8