Skip to content

Range::from_sparse densifies without bound: a 2 KB well-formed xlsx aborts the process #693

Description

@krickert

Range::from_sparse densifies to rows * cols with no bound (src/lib.rs:958-961):

let len = cols.saturating_mul(rows);
let mut v = vec![T::default(); len];

saturating_mul prevents the arithmetic overflow, but the allocation still runs at whatever size the corners imply. Two cells far apart is enough, and the failure is an abort, not a panic.

Repro

A 2,341 byte package. Both cells are legal: XFD1048576 is the last addressable cell in the grid, so nothing here is malformed.

xl/worksheets/sheet1.xml:

<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<dimension ref="A1:XFD1048576"/>
<sheetData>
<row r="1"><c r="A1"><v>1</v></c></row>
<row r="1048576"><c r="XFD1048576"><v>2</v></c></row>
</sheetData>
</worksheet>
let mut wb: Xlsx<_> = open_workbook(&path)?;
let name = wb.sheet_names()[0].clone();
let r = wb.worksheet_range(&name)?;   // never returns

Run under a 4 GB cap so it fails fast rather than thrashing the machine (calamine 0.36.0, release):

$ ( ulimit -v 4000000; ./repro corners.xlsx; echo "exit=$?" )
memory allocation of 549755813888 bytes failed
Aborted (core dumped)
exit=134

1,048,576 x 16,384 = 17,179,869,184 cells, and size_of::<Data>() is 32, so 549.7 GB. On a machine with enough overcommit it will try to clone-fill every one of those pages instead.

The part that makes this different from a large-allocation bug

Allocation failure calls handle_alloc_error, which aborts. It does not unwind. So a library consumer cannot defend against it on stable Rust:

  • catch_unwind does not catch it
  • running the parse on its own thread does not help; the whole process goes
  • exit 134 is SIGABRT, visible above

The only stable-Rust defence is to not call the function. That is workable for cell values, because worksheet_cells_reader avoids from_sparse entirely. It is not workable for formulas: worksheet_formula goes through from_sparse at src/xlsx/mod.rs:2617 and has no incremental equivalent, so a service that reads formulas from uploaded files has no way to protect itself.

This is the same failure mode as #444 ("memory allocation of 2199023255552 bytes failed"), #550 and #433, which were addressed in other paths.

Prior art

#463 raised the from_sparse OOM and proposed a fix. Reading the thread, it was not turned down: you replied positively and asked for a clarifying comment, sftse added it and said it was ready, and the PR then went quiet and was self-closed nine months later. Worth noting that sftse's stated workaround was set_alloc_error_hook, which is unstable, which is consistent with there being no way to handle this on stable.

I did not want to simply reopen that PR without checking whether you want the same approach.

Possible directions

  1. Return Err when len crosses a threshold, so the caller sees a normal error instead of losing the process.
  2. Allocate fallibly (try_reserve plus resize) and map failure to an error.
  3. Bound by density rather than absolute size, so a genuinely large dense sheet still loads but a two-cell sheet spanning the grid does not.

Any of the three would fix it from a consumer's point of view. Happy to send a PR for whichever you prefer.

Found while building the gRPC service in #688. Related: #684, which is the same class of attacker-influenced allocation on a different code path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions