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