# `seq -f %a` aborts (out-of-memory) on a large-magnitude exponent that GNU formats fine
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 24.1k
- Forks
- 2k
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 365
Description
Summary
seq -f %a / seq --format=%a (hexadecimal-float conversion) aborts with an out-of-memory error when an argument has a large-magnitude decimal exponent, even though the value is perfectly valid and GNU formats it without issue.
The crash is in formatting, not parsing — uutils parses the value fine (other conversions work), but the %a formatter builds a binary big-integer whose size is proportional to the exponent magnitude. A huge exponent makes it try to allocate exabytes, so the allocator fails and the process aborts (exit 134).
Steps to reproduce
$ seq -f %a 1E-1000000000000000000 1
memory allocation of 375000000000000024 bytes failed
Aborted (core dumped)
$ echo $?
134
--format=%a behaves identically. (1E-1000000000000000000 is a valid, tiny number — effectively 0.) The size that fails scales with the exponent magnitude, so a larger exponent fails larger (e.g. 1E-2000000000000000000 → 750000000000000024 bytes failed).
Expected behavior
GNU seq parses and formats the same value without crashing — it emulates a long double (≈15-bit exponent), so it never builds an oversized representation:
$ seq -f %a 1E-1000000000000000000 1
0x0p+0
0x8p-3
$ echo $?
0
Root cause
format_float_hexadecimal (in uucore/src/lib/features/format/num_format.rs) converts frac10 * 10^exp10 into frac2 * 2^exp2 by shifting a num-bigint value left by a margin derived from the exponent:
let exp10 = -p; // p = the value's decimal exponent
// ...
// Negative exponent: shift left by `margin`, then divide by 5^-exp10
let margin =
((max_precision + 1) as i64 * 4 - frac10.bits() as i64).max(0) + -exp10 * 3 + 1;
(
(frac10 << margin) / 5.to_bigint().unwrap().pow(-exp10 as u32), // <- allocates ~margin bits
exp10 - margin,
)
margin grows linearly with -exp10 and is never bounded. For exp10 = -10¹⁸, margin ≈ 3 × 10¹⁸ bits, so frac10 << margin asks num-bigint to build an integer of that many bits. The shift allocates a digit buffer sized to margin / 64 64-bit words — the failing allocation is Vec::with_capacity inside num-bigint:
num_format.rs:627 (frac10 << margin) / ...
-> num-bigint-0.4.6/src/biguint/shift.rs:12 biguint_shl (digits = margin / 64)
-> num-bigint-0.4.6/src/biguint/shift.rs:30 Vec::with_capacity(len) <-- OOM here
margin / 64 ≈ 4.7 × 10¹⁶ words × 8 bytes ≈ 3.75 × 10¹⁷ bytes — exactly the 375000000000000024 bytes the allocator reports before aborting. The function's own comment already flags the hazard:
TODO: this is most accurate, but frac2 will grow a lot for large precision or
exponent, and formatting will get very slow.
GNU caps the exponent to what a long double can represent, so it formats the value cheaply (0x0p+0) instead of materializing a giant bignum.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in uucore/src/lib/features/format/num_format.rs at format_float_hexadecimal, then reproduce the issue with seq -f %a 1E-1000000000000000000 1. Trace how the decimal exponent determines the bigint shift and add regression coverage showing that huge exponents no longer abort while producing the expected hexadecimal-float output.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli, performance
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100