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
- Return
Err when len crosses a threshold, so the caller sees a normal error instead of losing the process.
- Allocate fallibly (
try_reserve plus resize) and map failure to an error.
- 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.
Range::from_sparsedensifies torows * colswith no bound (src/lib.rs:958-961):saturating_mulprevents 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:
XFD1048576is the last addressable cell in the grid, so nothing here is malformed.xl/worksheets/sheet1.xml:Run under a 4 GB cap so it fails fast rather than thrashing the machine (calamine 0.36.0, release):
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_unwinddoes not catch itThe only stable-Rust defence is to not call the function. That is workable for cell values, because
worksheet_cells_readeravoidsfrom_sparseentirely. It is not workable for formulas:worksheet_formulagoes throughfrom_sparseatsrc/xlsx/mod.rs:2617and 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_sparseOOM 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 wasset_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
Errwhenlencrosses a threshold, so the caller sees a normal error instead of losing the process.try_reserveplusresize) and map failure to an error.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.