Skip to content

diff: don't panic on an oversized context count - #274

Open
MsfPablo wants to merge 1 commit into
uutils:mainfrom
MsfPablo:fix-oversized-context
Open

MsfPablo wants to merge 1 commit into
uutils:mainfrom
MsfPablo:fix-oversized-context

Conversation

@MsfPablo

Copy link
Copy Markdown

Fixes #245.

Both panics in the issue come from the context count being used unchecked:

  • -u99999999999999999999 and friends reach numvalue.as_str().parse::<usize>().unwrap() in params.rs, and PosOverflow aborts the process.
  • A count that does fit in usize but is large enough that N * 16 > isize::MAX gets past the parser and aborts in VecDeque::with_capacity instead, which is the second case @leeewee added in the comments.

The parse now saturates, and make_diff clamps context_size to the combined length of the two inputs. Neither file can have more lines than it has bytes, so a request above that already meant "the whole file" — the clamp only removes values that were never reachable, and it keeps both the preallocation and the context_size + 1 arithmetic below it in range. That covers the unified and context paths in one place rather than guarding each site.

All six affected spellings now produce the same output as a plain -u/-c run and exit 1.

Tests: a unit test in params.rs for the saturating parse, and an integration test running each spelling end to end.

One thing I left alone: the separate-argument forms (-C 99999999999999999999) already return invalid context length rather than panicking. GNU accepts those, so the behaviour still differs, but since it is not a crash I did not want to fold a compatibility change into this fix. Happy to do it separately if you want them aligned.

@github-actions

Copy link
Copy Markdown

GNU diffutils testsuite comparison:

Test results comparison:
  Current:   TOTAL: 33 / PASSED: 0 / FAILED: 33 / SKIPPED: 0
  Reference: TOTAL: 33 / PASSED: 8 / FAILED: 21 / SKIPPED: 4

Changes from main branch:
  TOTAL: +0
  PASSED: -8
  FAILED: +12

New test failures (12):
  - basic
  - bignum
  - brief-vs-stat-zero-kernel-lies
  - bug-64316
  - cmp
  - diff3
  - help-version
  - large-subopt
  - strcoll-0-names
  - strip-trailing-cr
  - timezone
  - y2038-vs-32bit

An inline count like -u99999999999999999999 reached
`parse::<usize>().unwrap()` and aborted with a PosOverflow. A count that
fits in usize but is large enough that N * 16 exceeds isize::MAX aborted
later instead, in VecDeque::with_capacity.

Saturate the parse, and clamp context_size to the combined input length
in both make_diff implementations. A file cannot have more lines than
bytes, so anything larger already meant the whole file; the clamp keeps
the surrounding arithmetic in range and stops the preallocation from
scaling with a number the caller picked.

Fixes uutils#245
@sylvestre
sylvestre force-pushed the fix-oversized-context branch from e852df3 to 8428c8f Compare August 18, 2026 06:55
Comment thread src/params.rs
if let Some(numvalue) = num {
if !numvalue.as_str().is_empty() {
context_count = Some(numvalue.as_str().parse::<usize>().unwrap());
// GNU accepts a count larger than the machine can represent and

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the upstream bignum test covers huge counts, could you please check it?
TESTS=bignum ./tests/run-upstream-testsuite.sh
also, the same comment is duplicated below, one line is enough

Comment thread src/unified_diff.rs
// already means "the whole file"; clamping keeps the arithmetic below in
// range and the preallocation proportional to the input.
let context_size = context_size.min(expected.len() + actual.len() + 1);
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this still preallocates 16 bytes per input byte, so -U999999999 on a 1GB file asks for 16GB.
could you please clamp to the line count instead, or just drop the with_capacity?

Comment thread tests/integration.rs
.arg(file2.path());
cmd.assert()
.code(predicate::eq(1))
.stdout(predicate::str::contains("X"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contains("X") is weak, could you please assert the full expected output?

@sylvestre

sylvestre commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks, this fixes both panics: saturating the parse in src/params.rs and clamping in make_diff are the right idea.
A few things before merge:

  1. src/unified_diff.rs:51 and src/context_diff.rs:60: the clamp uses the byte count, but the queue holds &[u8] slices, which are 16 bytes each. So -U999999999 on two 1 GB files still asks for about 32 GB up front. expected_lines and actual_lines are built a few lines below (unified_diff.rs:57-58). Could you move the clamp after them and use expected_lines.len().max(actual_lines.len())? Simply dropping the with_capacity would also work.
  2. The comments are too long. One line above the clamp, something like "clamp: a count past the line count means the whole file", is enough. In params.rs the same two-line comment appears twice (around lines 279 and 325). Keep one short line, or put the parse in a small helper both call sites share.
  3. tests/integration.rs:251: contains("X") passes for nearly any output. Could you assert the full expected stdout for -u and -c? With a huge context it's just the whole-file hunk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

diff panics on an oversized inline context count (-c/-u/-C/-U): parse overflow and capacity overflow

2 participants