From 3f6d698460b5e47426baa022c41a69f2b99a5e5d Mon Sep 17 00:00:00 2001 From: zfaustk <4340287+zfaustk@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:09:20 +0800 Subject: [PATCH] unexpand: prevent overflow on over-large tab stop values In write_tabs, tabstop target arithmetic previously added without overflow checks, panicking in debug builds and wrapping in release builds when a tabstop near usize::MAX is specified. Use checked_add and saturating_add in write_tabs and column tracking so over-large tabstops are handled cleanly without panic or runaway loops. --- src/uu/unexpand/src/unexpand.rs | 35 +++++++++++++++++++++------------ tests/by-util/test_unexpand.rs | 21 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 61a80f9c013..56640fcc99f 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -375,12 +375,19 @@ fn write_tabs( // We never turn a single space before a non-blank into // a tab, unless it's at the start of the line. let ai = print_state.leading || amode; - if (ai && print_state.pctype != CharType::Tab && print_state.col > print_state.scol + 1) + if (ai + && print_state.pctype != CharType::Tab + && print_state.col > print_state.scol.saturating_add(1)) || (print_state.col > print_state.scol && (print_state.leading || ai && print_state.pctype == CharType::Tab)) { - while let Some(nts) = next_tabstop(tab_config, print_state.scol) { - let target = print_state.scol + nts; + while print_state.scol < print_state.col { + let Some(nts) = next_tabstop(tab_config, print_state.scol) else { + break; + }; + let Some(target) = print_state.scol.checked_add(nts) else { + break; + }; if print_state.col < target { break; } @@ -389,7 +396,7 @@ fn write_tabs( if print_state .pending_wide .iter() - .any(|&(start, width, _)| start < target && target < start + width) + .any(|&(start, width, _)| start < target && target < start.saturating_add(width)) { break; } @@ -413,11 +420,11 @@ fn write_tabs( { let (_, width, bytes) = &print_state.pending_wide[wide_idx]; output.write_all(bytes)?; - print_state.scol += width; + print_state.scol = print_state.scol.saturating_add(*width); wide_idx += 1; } else { output.write_all(b" ")?; - print_state.scol += 1; + print_state.scol = print_state.scol.saturating_add(1); } } print_state.pending_wide.clear(); @@ -519,7 +526,7 @@ fn unexpand_buf( { write_tabs(output, tab_config, print_state, options.aflag)?; print_state.scol = print_state.col; - print_state.col += buf.len(); + print_state.col = print_state.col.saturating_add(buf.len()); output.write_all(buf)?; return Ok(()); } @@ -545,9 +552,9 @@ fn unexpand_buf( let mut saw_tab = false; while end < buf.len() { match buf[end] { - b' ' => col += 1, + b' ' => col = col.saturating_add(1), b'\t' => { - col += next_tabstop(tab_config, col).unwrap_or(1); + col = col.saturating_add(next_tabstop(tab_config, col).unwrap_or(1)); saw_tab = true; } _ => break, @@ -602,10 +609,12 @@ fn unexpand_buf( } // compute next col, but only write space or tab chars if not buffering - print_state.col += if ctype == CharType::Space { - cwidth + print_state.col = if ctype == CharType::Space { + print_state.col.saturating_add(cwidth) } else { - next_tabstop(tab_config, print_state.col).unwrap_or(1) + print_state + .col + .saturating_add(next_tabstop(tab_config, print_state.col).unwrap_or(1)) }; if !tabs_buffered { @@ -619,7 +628,7 @@ fn unexpand_buf( print_state.leading = false; // no longer at the start of a line print_state.col = if ctype == CharType::Other { // use computed width - print_state.col + cwidth + print_state.col.saturating_add(cwidth) } else if print_state.col > 0 { // Backspace case, but only if col > 0 print_state.col - 1 diff --git a/tests/by-util/test_unexpand.rs b/tests/by-util/test_unexpand.rs index ae9d86aae16..9dd9d2a4a92 100644 --- a/tests/by-util/test_unexpand.rs +++ b/tests/by-util/test_unexpand.rs @@ -328,6 +328,27 @@ fn test_extended_tabstop_increment_overflow() { .stderr_contains("tab stop value is too large"); } +#[test] +fn test_tabstop_near_max_does_not_overflow() { + new_ucmd!() + .arg(format!("-t{}", usize::MAX)) + .pipe_in("a\tb") + .succeeds() + .stdout_is("a\tb"); + + new_ucmd!() + .arg(format!("-t{}", usize::MAX - 1)) + .pipe_in("a\tb") + .succeeds() + .stdout_is("a\tb"); + + new_ucmd!() + .arg(format!("--tabs={}", usize::MAX)) + .pipe_in(" a") + .succeeds() + .stdout_is(" a"); +} + #[test] fn test_is_directory() { let (at, mut ucmd) = at_and_ucmd!();