shuf: fix panic/abort on large -i range without small --head-count (#12501)

* shuf: fix panic/abort on large -i range without small --head-count

NonrepeatingIterator::new sized its sparse hash map to
min(head_count, range_len) and allocated it with the infallible
with_capacity_and_hasher. head_count defaults to u64::MAX when -n is
absent (and can be passed a huge value explicitly), so for a large -i
range the map was asked to reserve the whole range:

  shuf -i 1-9999999999999999999                       # no -n
  shuf -i 1-9999999999999999999 -n 9999999999999999999

Both aborted (exit 134) with a hashbrown "Hash table capacity overflow"
panic or an allocator abort, where GNU prints "memory exhausted" and
exits 1.

Reserve fallibly with try_reserve (mirroring the Vec branch) and map
the failure to a clean error, so an unsatisfiable request errors like
GNU instead of crashing. A small --head-count still works unchanged.

Closes #12500.

* shuf: assert full stderr (incl. program prefix) in memory-exhausted tests

Per review on #12501: switch the two memory-exhausted regression tests
from stderr_contains to stderr_only("shuf: memory exhausted\n") so they
verify the whole stderr, including the `shuf:` program-name prefix.
This commit is contained in:
Wei Li
2026-06-01 15:50:35 +10:00
committed by GitHub
parent 7f0053075e
commit bcdd134352
5 changed files with 41 additions and 8 deletions
+1
View File
@@ -26,3 +26,4 @@ shuf-error-no-lines-to-repeat = no lines to repeat
shuf-error-start-exceeds-end = start exceeds end
shuf-error-missing-dash = missing '-'
shuf-error-write-failed = write failed
shuf-error-memory-exhausted = memory exhausted
+1
View File
@@ -23,3 +23,4 @@ shuf-error-no-lines-to-repeat = aucune ligne à répéter
shuf-error-start-exceeds-end = le début dépasse la fin
shuf-error-missing-dash = '-' manquant
shuf-error-write-failed = échec de l'écriture
shuf-error-memory-exhausted = mémoire épuisée
+13 -7
View File
@@ -6,7 +6,8 @@
use rustc_hash::FxHashMap;
use std::ops::RangeInclusive;
use uucore::error::UResult;
use uucore::error::{UResult, USimpleError};
use uucore::translate;
use crate::WrappedRng;
@@ -53,7 +54,7 @@ impl<'a> NonrepeatingIterator<'a> {
range: RangeInclusive<u64>,
rng: &'a mut WrappedRng,
head_count: Option<usize>,
) -> Self {
) -> UResult<Self> {
// Save RAM usage with shuf -i 1-huge_number -n small_number
const TOO_LARGE_VEC_SIZE: usize = 16_777_216;
let range_len = range.size_hint().0;
@@ -63,13 +64,18 @@ impl<'a> NonrepeatingIterator<'a> {
Values::Full(items)
} else {
const MAX_CAPACITY: usize = 128; // todo: optimize this
// `capacity` is the requested output count; with no --head-count it
// defaults to the whole range, which can be up to usize::MAX.
// Reserve fallibly so an unsatisfiable request errors cleanly
// instead of panicking in hashbrown (capacity overflow) or aborting
// in the allocator — mirroring the `try_reserve` on the Vec branch.
let capacity = head_count.unwrap_or(MAX_CAPACITY).min(range_len);
Values::Sparse(
range,
FxHashMap::with_capacity_and_hasher(capacity, rustc_hash::FxBuildHasher),
)
let mut map = FxHashMap::with_hasher(rustc_hash::FxBuildHasher);
map.try_reserve(capacity)
.map_err(|_| USimpleError::new(1, translate!("shuf-error-memory-exhausted")))?;
Values::Sparse(range, map)
};
NonrepeatingIterator { rng, values }
Ok(NonrepeatingIterator { rng, values })
}
fn produce(&mut self) -> UResult<u64> {
+1 -1
View File
@@ -358,7 +358,7 @@ impl Shufable for RangeInclusive<u64> {
amount: u64,
) -> UResult<impl Iterator<Item = UResult<Self::Item>>> {
let amount = usize::try_from(amount).unwrap_or(usize::MAX);
Ok(NonrepeatingIterator::new(self.clone(), rng, Some(amount)).take(amount))
Ok(NonrepeatingIterator::new(self.clone(), rng, Some(amount))?.take(amount))
}
}
+25
View File
@@ -234,6 +234,31 @@ fn test_range_permute_no_overflow_0_max() {
assert_eq!(result_seq.len(), 1, "Miscounted output length!");
}
#[test]
fn test_range_full_huge_no_head_count_memory_exhausted() {
// Repro for #12500: `shuf -i 1-huge` with no --head-count used to abort
// (hashbrown "Hash table capacity overflow" panic, or an allocator abort)
// because the sparse iterator reserved a map sized to the whole range.
// It must now fail cleanly, like GNU.
let upper_bound = usize::MAX;
new_ucmd!()
.arg(format!("-i1-{upper_bound}"))
.fails_with_code(1)
.stderr_only("shuf: memory exhausted\n");
}
#[test]
fn test_range_huge_head_count_memory_exhausted() {
// Repro for #12500: a large --head-count is just as unsatisfiable as no
// --head-count; both used to abort. Must now fail cleanly.
let upper_bound = usize::MAX;
new_ucmd!()
.arg(format!("-n{upper_bound}"))
.arg(format!("-i1-{upper_bound}"))
.fails_with_code(1)
.stderr_only("shuf: memory exhausted\n");
}
#[test]
fn test_very_high_range_full() {
let input_seq = vec![