mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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![
|
||||
|
||||
Reference in New Issue
Block a user