Redwood Fog
Challenge Information
Project: libavif
Type: delta
Harnesses: 8
Vulnerabilities: 1
AFC Challenge Performance
Number of Unique Vulnerabilities Discovered: #
Number of Teams with Scoring PoVs: 0
Number of Teams with Scoring Patches: 1
Number of Teams with Scoring Bundles: 0
Total Points Scored for this Challenge: 5.336123543908823
What design decisions were considered for this challenge?
Wanted to put a bug into some complicated functionality that would be difficult for a non-expert human to understand. Wanted to see if an LLM could spot the bug in a fairly large and math-heavy feature containing many memory manipulations.
Why this set of vulnerabilities?
This vulnerability is pretty difficult to solve, so it was put on its own.
Delta vs Full and why?
Being staged as a delta challenge should help an LLM spot the rather obvious bug in the code, the difficulty was to be in finding the crashing input.
Challenge Harnesses
- avif_fuzztest_dec@DecodeAvifTest.Decode
- avif_fuzztest_dec_incr@DecodeAvifFuzzTest.DecodeIncr
- avif_fuzztest_enc_dec@EncodeDecodeAvifFuzzTest.EncodeDecodeValid
- avif_fuzztest_enc_dec_anim@EncodeDecodeAvifFuzzTest.EncodeDecodeAnimation
- avif_fuzztest_enc_dec_incr@EncodeDecodeAvifFuzzTest.EncodeDecodeGridValid
- avif_fuzztest_properties@PropertiesAvifFuzzTest.EncodeDecode
- avif_fuzztest_read_image@ReadImageFuzzTest.ReadImageFile
- avif_fuzztest_yuvrgb@YuvRgbFuzzTest.Convert
Challenge Vulnerabilities
-
For a given pixel in the 2x2 box, the RGB value is calculated from the Y of the pixel and the UV of the box.
-
Calculate how far outside of the bounds you are in UV space, and then spill 1/3rd of that chroma (be it positive or negative) over to the 3 other pixels in the subsampling box, removing it from the current pixel.
-
Repeat until there is no more chroma to spill
chroma_spill
Vulnerability Information
Author: nemo
Harness: avif_fuzztest_yuvrgb@YuvRgbFuzzTest.Convert
CWE Classification: CWE-126
What functions and functionality is relevant?
The chroma upsampler in avifImageYUVAnyToRGBAnySlow
Why is this vulnerable?
Just a small out of bounds write, could be used to crash a program.
Is this a replay and/or is inspired by anything?
No.
What makes it interesting?
It presents a challenging fuzzing problem, the crashing input needs to be patterned very specifically across the rows and columns of the image. It should be possible for a sufficiently capable LLM to find the bug without finding the crashing input. It would then be possible for an extremely smart LLM to understand the image processing at play and to direct the fuzzer into creating at least a decent seed input for image fuzzing.
Additional Information
Adds a “spill” chroma upsampling option to libavif for YUV->RGB translation, the algorithm is described in “Towards Better Chroma Subsampling”, (SMPTE Motion Imaging Journal, Glenn Chan, 2007). The bug occurs only if the input image is crafted in such a way that the “spill” in all YUV420 2x2 pixel boxes is maximized, triggering the dampening which is missing a bounds check on the image and will crash for odd dimensions.
Some background
!!! Disclaimer: I am a reverse engineer not an image processing expert, this was done for the intention of adding a bug and has a wide range of issues
YUV420 is a color encoding format for images that uses chroma subsampling. For every pixel in the encoded image, a luminance value is saved; color information is stored in the U and V chroma channels, which have half of the resolution of the original image.
This “spill” algorithm adapted from “Towards Better Chroma Subsampling” works to take the YUV420 subsampled image and convert it back into RGB.
The algorithm takes advantage of out-of-gamut colors that are produced as a result of the YUV420->RGB translation. Because a monitor cannot render negative light values or values higher than the color depth, the RGB output must be clipped. This can create errors in cases such as the following example.
RGB -> Y + UV -> RGB
+------------+-------+ +-----+-----+ +--------------+ +-----+----------+
| bright red | black | | 1.0 | 0.0 | | | | red | dark red |
+------------+-------+ +-----+-----+ | reddish | +-----+----------+
| bright red | black | | 1.0 | 0.0 | | | | red | dark red |
+------------+-------+ +-----+-----+ +--------------+ +-----+----------+
Where the input 8-bit (R,G,B) values would be something like (255,0,0) and output (155,0,0) for red and (100,-25,-25) for dark red. This creates an appearance of a red color where there should be a black pixel. Additionally, the chroma ends up “missing” from the left side pixels, making the red appear less bright than it should.
The spill algorithm takes advantage of the fact that the luminance data is more “trustworthy” than the chroma data because it is stored at a higher resolution. This restricts the range of possible valid chroma values for a specific pixel; since we know that the data in the pixel was averaged, any out of bounds chroma must belong to a different pixel in the box.
The bug
A prior version of this algorithm I wrote had a bug that would cause the spill values to run away sometimes, i.e. the total chroma in the box would increase over iterations. To stopgap this I added a way to dampen the chroma in the box in the extreme case as a bad remediation. I have readded this dampening and lowered the threshold so that it is possible (but difficult) for an image to trigger the dampening functionality. The calculation of the subsampling box inside of this functionality does not do the bounds check and will cause an out of bounds write on an odd-dimensional image.
