fix(encoding): bounds-check the length prefix in VariableFullZipDecoder::unzip - #8138
Conversation
There was a problem hiding this comment.
Gate recommendation: request changes. Removing the unchecked read is the right safety direction, but malformed file bytes should cross the decoder’s existing Result boundary as a contextual corrupt-file error instead of becoming a library panic. Make parse_length/unzip/new fallible and assert the returned error so the regression test distinguishes this fix from the vulnerable base.
| /// bounds checked this read up to 8 bytes out of a 4 byte allocation, which a | ||
| /// release build did not catch because the only guard was a debug_assert!. | ||
| #[test] | ||
| #[should_panic] |
There was a problem hiding this comment.
Malformed file input still unwinds through bounds-checked indexing instead of returning a decoder error, and this #[should_panic] test does not regress the release-only bug. On the observed head, CARGO_TARGET_DIR=/home/agent/tmp/pr8138-target-xlDJUZ cargo test -p lance-encoding variable_full_zip_ -- --nocapture passed 2/2 while this case panicked at data[..8]; expected behavior is a contextual Error::CorruptFile. In a detached base checkout with only this helper/test hunk applied, CARGO_TARGET_DIR=/home/agent/tmp/pr8138-base-target-fMi9J5 cargo test -p lance-encoding variable_full_zip_ -- --nocapture also passed 2/2 because the vulnerable debug_assert! supplied the expected panic. create_decoder already returns Result, so please propagate a contextual Error::corrupt_file_named through parse_length/unzip/new and assert its variant and message, preferably for both supported prefix widths.
parse_length read the length prefix out of the page buffer with get_unchecked, guarded only by a debug_assert!. There is no [profile.release] override in the workspace Cargo.toml, so debug-assertions defaults to false in release and that assertion is not present in the published wheels. The loop it sits in continues on while !databuf.is_empty(), so it enters the body with as little as one byte remaining, and parse_length then reads up to eight. A page whose item walk ends with a partial trailing item therefore read past the end of the buffer. Malformed file bytes now cross the decoder's existing Result boundary rather than becoming a panic. parse_length, unzip and new are fallible and a truncated prefix returns a contextual Error::corrupt_file_named naming the prefix width and how many bytes actually remained. create_decoder already returned Result, so this propagates with a single ? at the one production call site. Tests assert the error variant and message for both supported prefix widths. They cannot compile against the unpatched base, where new returns Self rather than Result, so they cannot pass on vulnerable code. Verified on x86-64: before this change a release build with -Zsanitizer=address reports heap-buffer-overflow, READ of size 8, three bytes past a four byte allocation; after it there is no sanitizer error. The well-formed control is unaffected in both. The crate suite is 521 passing.
fca1459 to
5ea6228
Compare
|
Both points are right and I have taken both. Thank you for actually running the test against a base checkout rather than reading it, because that is what caught the real problem. The Malformed bytes now cross the
On the regression test discriminatingI owe you a precise answer rather than a claim. The replacement asserts the error variant and the message text, across both supported prefix widths (a 3 byte buffer for a 32 bit prefix, a 4 byte buffer for a 64 bit prefix): let err = decode_variable_full_zip(vec![0xAA; buf_len], bits)
.expect_err("a truncated length prefix must not decode");
assert!(matches!(err, Error::CorruptFile { .. }), ...);
assert!(err.to_string().contains("truncated length prefix"), ...);On the vulnerable base this test cannot compile, which is a stronger form of discrimination than failing at runtime. I checked it the way you did, applying only the test hunk to a detached base checkout:
If you would prefer a test that also demonstrates the underlying read on the base, that has to be a sanitizer run rather than a unit test, since on the base the failure is undefined behaviour rather than a value. For reference, that is how the original report was produced: a release build with Verification
Happy to adjust the error message wording or the naming if you would like something that matches your conventions more closely. |
There was a problem hiding this comment.
Gate recommendation: approve. The new revision removes the unchecked prefix read while preserving valid 32- and 64-bit decoding, propagates truncated prefixes as contextual corrupt-file errors through the existing loader boundary, and replaces the panic-based test with typed regression coverage that distinguishes the vulnerable implementation.
Xuanwo
left a comment
There was a problem hiding this comment.
Thank you @professor-moody for working on this!
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Backport the corruption-safety fixes from: - #8138: #8138 - #8144: #8144 The v3.0 stable line still reaches the vulnerable VariableFullZip length-prefix parser and unchecked variable-width offset paths through the default reader. Release-specific adaptation: use the v3.0 error-construction and synchronous reader APIs; retain one native truncated-prefix regression plus native V2.1/V2.2/V2.3 reader coverage, without main-only async/lazy/RLE scaffolding. Validation: - cargo fmt --all: passed - Targeted lance-encoding/lance-file corruption regression tests: passed (1 + 12 + 1 + 1 + 5 + 6 tests) - cargo clippy --all --tests --benches -- -D warnings: passed - No manifests, lockfiles, Python extras, or workflows were changed on this branch. - The current linux-build failure is the known ethnum/current-nightly E0512 tooling baseline; Python and cargo-deny failures are also baseline exceptions. The standalone create-rc workflow has no needs on these validation workflows, so no release artifact blocker was identified. - Keep nightly/dependency maintenance separate from this focused backport. --------- Co-authored-by: m00dy <professor.moody@pm.me>
Backport the corruption-safety fixes from: - #8138: #8138 - #8144: #8144 The v4.0 stable line still reaches the vulnerable VariableFullZip length-prefix parser and unchecked variable-width offset paths through the default reader. Release-specific adaptation: use the v4.0 error-construction and synchronous reader APIs; retain one native truncated-prefix regression plus native V2.1/V2.2/V2.3 reader coverage, with the release-specific reader conflict resolved minimally. Validation: - cargo fmt --all: passed - Targeted lance-encoding/lance-file corruption regression tests: passed (1 + 12 + 1 + 1 + 5 + 6 tests) - cargo clippy --all --tests --benches -- -D warnings: passed - Rust build, build-no-lock, linux-build, MSRV, and format checks: passed. - No manifests, lockfiles, Python extras, or workflows were changed on this branch. - Python and cargo-deny failures are baseline exceptions; the standalone create-rc workflow has no needs on these validation workflows, so no release artifact blocker was identified. - Keep Python/dependency maintenance separate from this focused backport. --------- Co-authored-by: m00dy <professor.moody@pm.me>
Backport the corruption-safety fixes from: - #8138: #8138 - #8144: #8144 The v5.0 stable line still reaches the vulnerable VariableFullZip length-prefix parser and unchecked variable-width offset paths through the default reader. Release-specific adaptation: use the v5.0 error-construction and synchronous reader APIs; retain one native truncated-prefix regression plus native V2.1/V2.2/V2.3 reader coverage, without main-only async/lazy/RLE scaffolding. Validation: - cargo fmt --all: passed - Targeted lance-encoding/lance-file corruption regression tests: passed (1 + 12 + 1 + 1 + 5 + 6 tests) - cargo clippy --all --tests --benches -- -D warnings: passed - Rust build-no-lock, MSRV, clippy, and format checks: passed; linux-build remains the known ethnum/current-nightly E0512 tooling baseline. - No manifests, lockfiles, Python extras, or workflows were changed on this branch. - Python and cargo-deny failures are baseline exceptions; the standalone create-rc workflow has no needs on these validation workflows, so no release artifact blocker was identified. - This branch retains the existing recovery history around the #8144 backport (duplicate application followed by revert); no history was rewritten, and the final tree contains the intended fix. - Keep nightly/dependency maintenance separate from this focused backport. --------- Co-authored-by: m00dy <professor.moody@pm.me>
Backport the corruption-safety fixes from: - #8138: #8138 - #8144: #8144 The v6.1 stable line still reaches the vulnerable VariableFullZip length-prefix parser and unchecked variable-width offset paths through the default reader. Release-specific adaptation: use the v6.1 error-construction and asynchronous reader APIs; retain one native truncated-prefix regression plus native V2.1/V2.2/V2.3 reader coverage, without main-only lazy/RLE scaffolding. Validation: - cargo fmt --all: passed - Targeted lance-encoding/lance-file corruption regression tests: passed (1 + 12 + 1 + 1 + 5 + 6 tests) - cargo clippy --all --tests --benches -- -D warnings: passed - Rust build, build-no-lock, linux-build, MSRV, clippy, and format checks: passed. - The license-header-checker install failure is an upstream dynamic-installer/tooling baseline; Python and cargo-deny failures are also baseline exceptions. The standalone create-rc workflow has no needs on these validation workflows, so no release artifact blocker was identified. - No manifests, lockfiles, Python extras, or workflows were changed on this branch. - Keep license/Python/dependency maintenance separate from this focused backport. --------- Co-authored-by: m00dy <professor.moody@pm.me>
Backport the corruption-safety fixes from: - #8138: #8138 - #8144: #8144 The v7.1 stable line still reaches the vulnerable VariableFullZip length-prefix parser and unchecked variable-width offset paths through the default reader. Release-specific adaptation: use the v7.1 error-construction and asynchronous reader APIs; retain one native truncated-prefix regression plus native V2.1/V2.2/V2.3 reader coverage, without main-only lazy/RLE scaffolding. Validation: - cargo fmt --all: passed - Targeted lance-encoding/lance-file corruption regression tests: passed (1 + 12 + 1 + 1 + 5 + 6 tests) - cargo clippy --all --tests --benches -- -D warnings: passed - Rust build, build-no-lock, linux-build, MSRV, clippy, and format checks: passed. - Python and cargo-deny failures are baseline exceptions; the standalone create-rc workflow has no needs on these validation workflows, so no release artifact blocker was identified. - No manifests, lockfiles, Python extras, or workflows were changed on this branch. - Keep Python/dependency maintenance separate from this focused backport. --------- Co-authored-by: m00dy <professor.moody@pm.me>
Backport the corruption-safety fixes from: - #8138: #8138 - #8144: #8144 The v8.0 stable line still reaches the vulnerable VariableFullZip length-prefix parser and unchecked variable-width offset paths through the default reader. Release-specific adaptation: use the v8.0 error-construction and asynchronous reader APIs; retain one native truncated-prefix regression plus native V2.1/V2.2/V2.3 reader coverage, without main-only lazy/RLE scaffolding. Validation: - cargo fmt --all: passed - Targeted lance-encoding/lance-file corruption regression tests: passed (1 + 12 + 1 + 1 + 5 + 6 tests) - cargo clippy --all --tests --benches -- -D warnings: passed - Rust build-no-lock, MSRV, clippy, and format checks: passed. The final linux-build log shows the untouched 'rust/lance-index/src/vector/utils.rs:313' test 'test_simple_index_nearest_centroid::case_2_f32' failed with '45 != 42' (642 passed, 1 failed); this is outside the backport diff. - Python and cargo-deny failures are baseline exceptions. The standalone create-rc workflow has no needs on these validation workflows, so no release artifact blocker was identified. - No manifests, lockfiles, Python extras, or workflows were changed on this branch. - Keep the independent lance-index/Python/dependency maintenance separate from this focused backport. --------- Co-authored-by: m00dy <professor.moody@pm.me>
Backport the corruption-safety fixes from: - #8138: #8138 - #8144: #8144 The v9.0 stable line still reaches the vulnerable VariableFullZip length-prefix parser and unchecked variable-width offset paths through the default reader. Release-specific adaptation: use the v9.0 error-construction and asynchronous reader APIs; retain one native truncated-prefix regression plus native V2.1/V2.2/V2.3 reader coverage, without main-only lazy/RLE scaffolding. Validation: - cargo fmt --all - Targeted lance-encoding/lance-file corruption regression tests: passed - cargo clippy --all --tests --benches -- -D warnings: passed --------- Co-authored-by: m00dy <professor.moody@pm.me>
parse_lengthreads the length prefix out of the page buffer withget_unchecked, and the only thing between it and the end of the buffer is adebug_assert!:There is no
[profile.release]override in the workspaceCargo.toml, sodebug-assertionsdefaults to false in release and that assertion is not present in the published wheels.The loop it sits in continues on
while !databuf.is_empty(), so it enters the body with as little as one byte remaining.parse_lengththen reads up to eight. A page whose item walk ends with a partial trailing item therefore reads past the end of the buffer.Reproduced on x86-64 with
-Zsanitizer=addresson a release build, driving the realVariableFullZipDecoder::new:A well-formed control buffer is clean in the same run.
The change
The payload read one line below the call site is already bounds checked and panics on malformed input:
So a truncated item already fails cleanly on the payload path. Only the length read was inconsistent. This makes the two match by using safe indexing in
parse_length, which lets theunsafeblock and thedebug_assert!both go away.On valid input the behaviour is unchanged. On a truncated trailing item the result is the same clean panic the payload path already produces, rather than an out-of-bounds read.
Tests
Two, per the contributing guide:
variable_full_zip_wellformed_length_prefixdecodes a well-formed prefixvariable_full_zip_truncated_length_prefix_is_rejectedis#[should_panic]and covers the case aboveBoth pass, and the crate's existing suite is unaffected (520 passing before and after).
Scope, stated honestly
I have not established that a
.lancefile produced by the writer can reach this state. Truncating a data file is rejected earlier by the I/O range check, and a sweep of in-place single-byte edits either read cleanly, were rejected by that same check, or panicked in safe code further along in decode. So I am not claiming this is reachable from a crafted dataset, and I am filing it as hardening rather than as a security report.The case for the change does not depend on that: an
unsaferead whose only guard is compiled out of release builds is worth removing on its own, particularly when the adjacent read of the same buffer is already checked.One unrelated observation
Not part of this change, and not something I have shown to be a bug, but it looked odd while reading. The length is read using
in_bits_per_lengthand the cursor is then advanced bybytes_per_offset, which comes fromout_bits_per_offset:Those are equal in the common case, so this may well be deliberate. Flagging it only in case the asymmetry is unintentional.