paramdict: reject negative array and string lengths - #6874
paramdict: reject negative array and string lengths#6874professor-moody wants to merge 2 commits into
Conversation
The array and string length fields are read from the model file as signed ints and used without a lower bound. A negative length makes Mat::create wrap its size arithmetic, so the allocation succeeds at a small size and the following read overflows it. Adds the lower bound at the three sites that were missing it, checks that the allocation succeeded before reading into it, and adds a regression test.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 975d4f056d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| // binary array, negative length | ||
| { | ||
| const int mem[] = {-23304, -4, 1, 2, 3, 4, -233}; |
There was a problem hiding this comment.
Encode binary fixtures as little-endian bytes
On big-endian builds, both added binary fixtures use the host-native representation of int, but load_param_bin() treats the stream as little-endian and byte-swaps each field under __BIG_ENDIAN__. The initial -23304 is therefore transformed into an unrelated out-of-range ID, so the loader returns an error before reaching the negative-length check; consequently, this test still passes if that check is removed. Use explicit little-endian byte arrays, as the earlier binary ParamDict tests do, so the intended branch is exercised on every supported endianness.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch, thanks. Fixed in 5f13051.
Both binary fixtures now use explicit little-endian byte arrays, matching the existing binary ParamDict tests, instead of a host-native int array. You were right that the old version would have passed even with the fix removed on a big-endian build: the leading -23304 was byte-swapped into an out-of-range id, so load_param_bin returned before it ever reached the length check.
I also re-checked the three cases against an unpatched tree to confirm the test really does fail without the fix. The text case returns 0 where it should return -1, and both binary cases abort before completing.
The two binary fixtures used a host-native int array. load_param_bin() byte-swaps each field under __BIG_ENDIAN__, so on a big-endian build the leading -23304 decoded to an unrelated out-of-range id and the loader returned before reaching the length check. The test would have passed even with the fix removed. Use explicit little-endian byte arrays, matching the existing binary ParamDict tests.
|
Two corrections to the mechanism I described in the PR body. The patch itself is unchanged and I believe it is correct; these are both cases where a reviewer who tries the stated behaviour will see something different from what I wrote. 1. The 2. The small-allocation explanation attributes the wrap to the wrong layer. I wrote that Two riders on that second point. The specific "76 byte region" in my ASan quote is branch dependent: it comes from the Neither correction changes the fix. Rejecting a negative length is right for all of these cases, including the silent-acceptance ones. |
The array and string length fields in ParamDict are read from the model file as signed ints and used without a lower bound.
In load_param_bin the array branch does:
With a negative len, Mat::create computes w * elemsize as a size_t, which wraps, so the aligned total also wraps and the allocation request comes out small enough to succeed. The result is a small block whose refcount pointer lands before the allocation, followed by a read of sizeof(float) * len bytes into it. Against a build with AddressSanitizer this shows up as a heap-buffer-overflow write of 4060 bytes into a 76 byte region, from an ordinary ncnn::Net::load_param_bin() call on a crafted .param.bin.
Three places need it:
This is the same shape as the id fix in 5a0288f, which changed
id >= NCNN_MAX_PARAM_COUNTtoid < 0 || id >= NCNN_MAX_PARAM_COUNTin both functions. The length fields in those same two functions kept an upper bound or no bound.The patch rejects negative lengths at all three sites, and also checks that the allocation actually succeeded before reading into it, since a large positive length can fail to allocate and leave a null data pointer.
Added test_paramdict_7 covering the three cases. It fails on current main and passes with the fix. Full test suite is 176/176 either way.