Systematic integer truncation hardening - #3373
krishna28238-arch wants to merge 7 commits into
Conversation
Eliminate the remaining cases where a value can be truncated by a narrowing integer conversion before or during validation, and add a compile-time guard so that this class of bugs cannot silently return. - avifDecoderData::sampleTransformNumInputImageItems was a uint8_t, so a crafted file with 259 'dimg' inputs to a 'sato' item wrapped the count to 3 and bypassed both the "at most 32 input items" format check and the implementation limit check, ending in a misleading error. It is now a uint32_t, and the internal avifImageApplyExpression() and avifImageApplyOperations() interfaces follow. - av1SequenceHeaderParse() computed the size of an OBU without a size field through an (int) cast of a size_t, which is an implementation-defined conversion for payloads larger than INT_MAX and may overflow. The computation is now done in unsigned 64-bit arithmetic. - The 'a1lx' writer silently truncated any layer size larger than 4 GiB to 32 bits. It now refuses to encode such layers. - The experimental MinimizedImageBox chroma sample position check compared a truncated value. It now compares the full enums. - GCC builds now compile the C code with -Wconversion (excluding sign and float conversions) so that any future implicit narrowing conversion is caught at compile time instead of silently wrapping. All remaining warnings are fixed with explicit conversions or casts. - New AvifDecodeTest.SampleTransformTooManyInputItems regression test.
The -Wconversion flag added to avif_enable_warnings also applied to the vendored libyuv sources that are compiled into avif_obj whenever AVIF_LIBYUV is disabled, which broke GCC -Werror builds with AVIF_LIBYUV=OFF (e.g. build-shared-local ubuntu libyuv OFF in CI): third_party/libyuv/source/scale.c, scale_common.c, scale_any.c and row_common.c contain benign uint32_t to uint16_t conversions. Exclude these trusted third-party sources from the guard with a per-file -Wno-conversion so that only libavif's own code is held to the standard.
…ng-cl, gradle download timeout on android-jni)
… from the GitHub release CDN on two windows jobs; download works again)
| data[0] = (uint8_t)((value >> 0) & 0xff); | ||
| data[1] = (uint8_t)((value >> 8) & 0xff); | ||
| data[2] = (uint8_t)((value >> 16) & 0xff); | ||
| data[3] = (uint8_t)((value >> 24) & 0xff); |
There was a problem hiding this comment.
Krishna: Thank you for your interest in libavif. I will need to decline most of the changes in this pull request.
For example, the casts added to this file are not necessary because the expressions in question are in the range of 0..0xff and fit in the uint8_t type. In this case we can choose to omit the explicit uint8_t casts for brevity.
There was a problem hiding this comment.
tell me the changes and i will change those
| return 0.0f; | ||
| } | ||
| return (float)f.n / f.d; | ||
| return (float)f.n / (float)f.d; |
There was a problem hiding this comment.
Here is another example of omitting unnecessary explicit casts. If the first operand is float, then the second operand is implicitly converted to float. So one explicit cast to float suffices. We take advantage of this rule to minimize the number of explicit casts and avoid visual clutter in the source code.
| // Convert extended SDR (where 1.0 is SDR white) to nits. | ||
| clli->maxCLL = (uint16_t)AVIF_CLAMP(avifRoundf(rgbMaxLinear * SDR_WHITE_NITS), 0.0f, (float)UINT16_MAX); | ||
| const float rgbAverageLinear = rgbSumLinear / ((size_t)width * height); | ||
| const float rgbAverageLinear = rgbSumLinear / (float)((size_t)width * height); |
There was a problem hiding this comment.
Similarly, since the rgbSumLinear variable is of the float type, we omit the explicit float cast on the denominator deliberately.
Per review feedback, all changes that existed only to silence -Wconversion are withdrawn: - CMakeLists.txt: back to -Wall -Wextra for GCC; the libyuv -Wno-conversion scoping goes away with the flag. - apps/shared/iccmaker.c, src/utils.c, src/reformat.c, src/reformat_libyuv.c, src/stream.c: revert to the original expressions; masked byte extraction provably fits the target type, so the explicit casts were unnecessary. - src/gainmap.c, src/read.c: revert the redundant float casts; when the first operand of an arithmetic expression is float, the other operand is implicitly converted, so one explicit cast (or none, when the first operand is already float) suffices. The actual truncation fixes and their regression test are unchanged: sato input item count uint8_t -> uint32_t, 64-bit OBU size computation with bound check, and a1lx layer size bound check. Verified: 0 warnings with the upstream GCC flags, ctest 51/51.
|
Thanks for the review! Commit 3258cc1 addresses the comments: While auditing every 16-bit field written by the encoder, I found one more silent truncation: the 'iref' reference_count field is 16-bit in both versions, so a 256x256 grid (65536 cells) cannot be represented. avifEncoderAddImageGrid() now rejects it with AVIF_RESULT_INVALID_IMAGE_GRID (new test GridApiTest.CellCountExceeding16BitReferenceCount). Happy to drop that change if you prefer to keep this PR minimal. |
|
No, it is not recent. The very first commit of libavif (444f051, Jan 2019) |
Systematic integer truncation hardening
Summary
This PR eliminates the remaining cases where a value derived from untrusted data can be truncated by a narrowing integer conversion before or during validation.
Motivating pattern (the class this PR removes): a count of 260 stored as uint8_t truncates to 4; a "count <= limit" check on the truncated value passes while a loop iterates 260 times — validation bypass by truncation/wrap-around.
The audit
A systematic sweep of the whole codebase:
Most sites are already guarded (see "Notable sites verified safe" below). Three instances of the pattern survived and are fixed here.
Fixes
avifDecoderData::sampleTransformNumInputImageItems (src/read.c) counted 'dimg' inputs of a 'sato' item into a uint8_t. A crafted file with 259 input items wrapped the counter to 3, bypassing both the "at most 32 input items" format check and the implementation limit check. Release builds then rejected the file with a misleading error (DECODE_SAMPLE_TRANSFORM_FAILED "not a supported image type" or INTERNAL_ERROR) instead of the intended clean failure; debug builds abort on the later invariant.
av1SequenceHeaderParse() (src/obu.c) computed the size of an OBU without a size field as (int)obus.size - 1 - obu_extension_flag. For payloads larger than INT_MAX this is an implementation-defined conversion with signed overflow on the way (UB; UBSan abort). The computation is now done in unsigned 64-bit arithmetic with an explicit bound check before the narrowing store.
3. a1lx layer sizes silently truncated to 32 bits
The 'a1lx' writer (src/write.c) stored each layer's payload size_t size into a uint32_t without a check, so a layer larger than 4 GiB would be written with a wrapped layer size, signaling wrong layer boundaries to decoders. It now refuses to encode such layers (AVIF_ASSERT_OR_RETURN(size <= UINT32_MAX)).
Changes withdrawn after review
An earlier revision of this PR also enabled -Wconversion for C sources on GCC and added the explicit casts needed to keep that build warning-free. Per review feedback, all of those changes are withdrawn in 20f4a67:
Only the three behavioral fixes above and the regression test remain.
Notable sites verified safe (no change needed)
Verification