Skip to content

# seq -f %a aborts (out-of-memory) on a large-magnitude exponent that GNU formats fine #13222

Description

@leeewee

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-2000000000000000000750000000000000024 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions