Conversation
| // The APV spec has no (k & 31) here: for a valid bitstream k | ||
| // stays below 32 so the mask is a no-op. It exists only to | ||
| // keep the shift count in [0,31] for a malformed stream that | ||
| // drives k past 31, which would otherwise be shift-count UB. | ||
| // Such a stream is rejected by the (k < 32) check after the | ||
| // loop, so the masked value is never used. | ||
| // | ||
| // The mask is used instead of an in-loop (k < 32) branch on | ||
| // purpose: this is the hot coefficient-decoding loop, and an | ||
| // extra conditional here would add a mispredictable branch and | ||
| // slow down decoding of every symbol. Masking is branch-free. | ||
| symbol += 1u << (k & 31); | ||
| oapv_assert_rv(k < 30, -1); /* prevent too large (impossible) k value */ | ||
| symbol += 1u << k; |
There was a problem hiding this comment.
The in-loop change cannot be accepted. The original symbol += 1u << (k & 31); was implemented this way on purpose, and the comment removed by this PR explains why.
This loop runs for every prefix bit in coefficient decoding, which is the hottest path of the decoder. An oapv_assert_rv(k < 30, -1) inside it adds a branch per bit and slows down decoding. The (k & 31) mask gives the same safety without a branch:
- For a valid bitstream
kstays below 32, so the mask is a no-op and the decoded value is the same as in the APV spec. - For a malformed bitstream it keeps the shift count in
[0, 31], so there is no shift-count UB. Such a symbol is then rejected by thekcheck after the loop.
So the mask and its comment should be kept, and the in-loop assert should not be added.
This part of the code has been reported repeatedly, most likely by coding agents that flag the missing in-loop check. To prevent this, comments explaining why the check is intentionally left out of these loops and how a malformed bitstream is rejected have been added in #297.
There was a problem hiding this comment.
We have reverted the in-loop change in this PR so that the PR only includes the oapv_assert_rv(k < 30, -1) and return (int)symbol; changes that we agree on.
However, to share the context on why the in-loop symbol += 1u << (k & 31); was flagged: while u32 wraparound is well-defined in standard C, Android builds libopenapv with sanitize: { integer_overflow: true } in Android.bp, which enables Clang's -fsanitize=unsigned-integer-overflow in addition to signed-integer-overflow (treating unsigned wraparound as a fatal abort in sanitized/fuzzing builds).
On a malformed bitstream with 32+ zero bits in the prefix, k reaches 31 inside the while (1) loop and symbol += 1u << (k & 31) wraps u32 (2147483680 + 2147483648), triggering a UBSan abort before the post-loop k < 30 check is reached:
src/oapv_vlc.c:780:24: runtime error: unsigned integer overflow: 2147483680 + 2147483648 cannot be represented in type 'unsigned int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior src/oapv_vlc.c:780:24
If you are open to avoiding that u32 wraparound without adding any branch to the hot loop, dec_vlc_read() can use the same structure as dec_vlc_read_kparam0() and dec_vlc_read_1bit_read() — doing only k++; inside the loop and adding the closed-form geometric series sum after oapv_assert_rv(k < 30, -1):
- When
parse_exp_golomb == 1(initial prefix bits01with initial parameterk0),(2u << k0) + sum_{j=k0..k-1} (1u << j) = (1u << k0) + (1u << k). - Setting
symbol = 1u << k;whenparse_exp_golomb == 1(orsymbol = (u32)(1 + flag) << k;whenflag == 0and onlysymbol = 1u << k;on theparse_exp_golombpath), keeping onlyk++;in the loop, and computingsymbol += 1u << k;afteroapv_assert_rv(k < 30, -1)requires zero in-loop branch, removes the per-bit shift/add from the loop body, and never wrapsu32.
| oapv_assert_rv(k >= 0 && k < 30, -1); | ||
|
|
There was a problem hiding this comment.
The oapv_assert_rv(k >= 0 && k < 30, -1) check at the entry of dec_vlc_read() is not needed either. k does not come from the bitstream. It is always a decoder-side value set by OAPV_KPARAM_* or KPARAM_*(), so it is always in [0, OAPV_KPARAM_DC_MAX] (0 to 5). The check can never fail, and it would only add a branch to every call on the coefficient decoding path.
There was a problem hiding this comment.
Agreed — removed the entry oapv_assert_rv(k >= 0 && k < 30, -1) check.
| } | ||
|
|
||
| oapv_assert_rv(k < 32, -1); /* prevent too large (impossible) k value */ | ||
| oapv_assert_rv(k < 30, -1); /* prevent too large (impossible) k value */ |
There was a problem hiding this comment.
This change is reasonable.
| bs->leftbits -= k; | ||
| } | ||
| return symbol; | ||
| return (int)symbol; |
Signed-off-by: Fyodor Kyslov <kyslov@google.com>
8f8531a to
fb56c3f
Compare
Summary
Bounds the post-loop exp-Golomb prefix check
k < 30and explicitly castsreturn (int)symbol;indec_vlc_read()(src/oapv_vlc.c) sosymbolcannot exceedINT_MAXor return a negativeintwhen adding thek-bit suffix.Testing
ctestunit/conformance tests pass with AddressSanitizer and UndefinedBehaviorSanitizer.