dwa_uncompress() addresses the three colour channels by a fixed slot
index derived from nb_channels, while decode_block() extracts the planes
through channel_offsets[]. Two things follow.

The first is a wrong image for an otherwise handled file. The RLE
section, which carries the alpha channel, is written to slot 0 whenever
nb_channels is at least 4. With four or more channels and no alpha, slot
0 is blue, so the blue plane receives the RLE data. An RGB image with a
depth pass and one further channel decodes with B at 0.5498 where the
file says 0.30, and 0.5498 is the value of that unrelated channel.
Whether this happens depends on whether the encoder put a channel into
the RLE section, so the same channel list decodes correctly or loses its
blue depending on the file. Patch 1 tests the actual condition: the
alpha channel has to be the one sitting in slot 0.

The second is a channel list that the slot arithmetic cannot describe at
all, for example an RGBA image carrying a depth pass. Those decode with
the colour planes shifted by one and one plane left as it was, and
decoding reports success. Patch 2 rejects them instead.

Neither is a memory safety issue: with the pixel type check from
0469d68ac in place the slot arithmetic stays inside the line stride, so
this goes to the list and not to ffmpeg-security.

Where this sits. 7896cc67 checks the lower end of the same assumption by
rejecting fewer than three channels; patch 2 is the upper end. 6e8cf037
gates the channel list in decode_header(), but its first branch passes as
soon as R, G and B are all present, so extra channels are never looked
at, which is why an RGBA image with a depth pass gets through. The
channel ordering assumption is also the second of the two named in the
public description of CVE-2025-59733, whose pixel type half was
addressed in 0469d68ac.

The test files come out of the OpenEXR reference implementation, so they
are what a normal encoder writes rather than something crafted by hand.
Every channel is a constant plane: R 0.10, G 0.20, B 0.30, A 0.40,
Y 0.20, and every further channel 0.55.

  import numpy as np, OpenEXR
  H = W = 64
  V = {"R": 0.10, "G": 0.20, "B": 0.30, "A": 0.40, "Y": 0.20}
  def w(path, names, comp=OpenEXR.DWAA_COMPRESSION, dt=np.float16):
      ch = {n: np.full((H, W), V.get(n, 0.55), dtype=dt) for n in names}
      with OpenEXR.File({"compression": comp,
                         "type": OpenEXR.scanlineimage}, ch) as f:
          f.write(path)

Decoded with

  ffmpeg -hide_banner -i <file> -f rawvideo -pix_fmt gbrpf16le out.raw -y

using gbrapf16le where the file has alpha, gbrpf32le for the float file
and grayf16le for the luma file, then averaged per plane. The EXR channel
list is sorted bytewise, so uppercase names come first; the offsets below
are what decode_header() ends up with.

  file                       channels                      before             
after
  c1  B,G,R                  B@0 G@2 R@4                   correct            
correct
  c2  A,B,G,R                A@0 B@2 G@4 R@6               correct            
correct
  c3  list of t1, but ZIP    A@0 B@2 G@4 R@6 depth@8       correct            
correct
  c4  B,G,R as float         B@0 G@4 R@8                   correct            
correct
  c5  AO,B,G,R               AO@0 B@2 G@4 R@6              correct            
correct
  c6  B,G,R,depth,fog        B@0 G@2 R@4 depth@6 fog@8     correct            
correct
  c7  B,G,R and three more   B@0 G@2 R@4 and three more    correct            
correct
  r1  B,G,R,depth,zz.A       B@0 G@2 R@4 depth@6 zz.A@8    B 0.5498           
correct
  t1  A,B,G,R,depth          A@0 B@2 G@4 R@6 depth@8       R 0 G .10 B .20    
refused
  t2  B,G,R,depth            B@0 G@2 R@4 depth@6           R .20 G .30 B 0    
refused
  t3  B,G,R,Z                B@0 G@2 R@4 Z@6               R .20 G .30 B 0    
refused
  t4  Y,P,Q                  P@0 Q@2 Y@4                   Y 0                
refused
  m1  no channel list        none                          Missing channel 
list, both
  m2  A,Y                    A@0 Y@2                       Gray DWA, both
  m3  B,G,R as uint32        B@0 G@4 R@8                   Zero ac_size, both

r1 is what patch 1 fixes: zz.A goes into the RLE section, and 0.5498 is
that channel, not blue. c5 to c7 are the cases a coarser check would
lose: they carry extra channels but keep B, G and R on the slots
dwa_uncompress() writes, and they decode correctly before and after. c3
and t1 hold the same channel list and differ only in the compression,
which places the second defect in dwa_uncompress() and not in the header
parsing. m1 to m3 are there so that the new check cannot quietly take
over a diagnostic that already exists: it runs only for three channels or
more, so Missing channel list and the Gray DWA path from 7896cc67 both
stay reachable.

Decoding t1 to t3 reports success. At the default log level the only exr
message is a warning naming the extra channel, Unsupported channel depth
for t1 and t2 and Multiple channels with index 2 for t3. Neither says
that the colour planes are wrong.

FATE is 5869 tests with 0 failures, the same before and after the series,
and the sorted list of test names is identical. 74 of those are exr tests
and none of them uses DWA compression, so no existing test changes state.
I could not add one because the samples live in the separate fate-suite.
make check does not run at this HEAD, devprogs has no rule, so the above
is make fate. Built with gcc 15 on x86_64.

Nexory (2):
  avcodec/exr: Write the DWA alpha slot only when alpha occupies it
  avcodec/exr: Check the DWA channel layout

 libavcodec/exr.c | 26 +++++++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)
_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to