Skip to content

Cell reference parsing overflows u32 unchecked; MAX_ROWS/MAX_COLUMNS are warnings only #694

Description

@krickert

get_row_and_optional_column (src/xlsx/mod.rs:2825-2860) accumulates both components with unchecked u32 arithmetic:

c @ b'A'..=b'Z' => col = col * 26 + (c - b'A') as u32 + 1,   // :2834
...
c @ b'0'..=b'9' => row = row * 10 + (c - b'0') as u32,        // :2850

Neither is bounded, and the MAX_ROWS / MAX_COLUMNS checks in get_dimension (src/xlsx/mod.rs:2791-2796) are warn! only, so an out-of-range value is logged and then returned to the caller anyway.

Three separate outcomes, all from ~2 KB packages with a single real cell. calamine 0.36.0.

1. In range for u32, far out of range for a spreadsheet

<dimension ref="A1:ZZZZZZ1"/>:

start=(0, 0) end=(0, 321272405)    (MAX_COLUMNS = 16384)

Same in debug and release. The warn! fires, then the value is handed back from dimensions(). That is roughly 19,600 times the grid width, and dimensions() is the natural pre-allocation hint for the incremental readers, so a caller that sizes a row buffer from it commits about 10 GB.

2. Column overflow

<dimension ref="A1:ZZZZZZZ1"/> (seven letters, 26^7 > u32::MAX):

debug:   thread 'main' panicked at src/xlsx/mod.rs:2834:38:
         attempt to multiply with overflow
release: start=(0, 0) end=(0, 4058115285)

3. Row overflow, and it is reachable from a cell reference

<dimension ref="A1:A99999999999"/> panics the same way at :2850. More importantly the same parser handles <c r="...">, so it is reachable while iterating cells rather than only when reading the declaration:

<dimension ref="A1:A5"/>
<sheetData>
<row r="1"><c r="A1"><v>1</v></c></row>
<row r="99999999999"><c r="A99999999999"><v>2</v></c></row>
</sheetData>
let mut r = wb.worksheet_cells_reader(&name)?;
while let Some(c) = r.next_cell()? { println!("{:?}", c.get_position()); }
debug:   cell at (0, 0)
         thread 'main' panicked at src/xlsx/mod.rs:2850:38:
         attempt to multiply with overflow
release: cell at (0, 0)
         cell at (1215752190, 0)

In release the reader reports a cell at row 1,215,752,190, which is not a position that appears anywhere in the file.

Why it compounds

A fabricated position like that is exactly what Range::from_sparse turns into an unbounded densification, so this feeds directly into #693: the two together mean a small file can produce a cell position no writer wrote, and then an allocation sized from it. Bounding the parse would close one of the two ways to reach that.

For what it is worth, Reference::validate (src/xlsx/mod.rs:3122-3145) already does exactly the right check with XlsxError::ColumnNumberOverflow and RowNumberOverflow, but it is only applied to formula references, not to <dimension> or <c r>.

Suggested fix

Use checked arithmetic in the accumulation loops and return XlsxError::ColumnNumberOverflow / RowNumberOverflow on overflow or on exceeding the existing constants, reusing the variants validate already defines. That turns all three cases above into a normal Result the caller can handle, instead of a debug panic, a wrapped value, or a silently out-of-range one.

Found while building the gRPC service in #688. Related: #693 (unbounded densification, which this can feed) and #692 (unchecked subtraction on a reversed ref). Happy to send a PR.

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