CVE-2022-41838
A code execution vulnerability exists in the DDS scanline parsing functionality of OpenImageIO Project OpenImageIO v2.4.4.2. A specially-crafted .dds can lead to a heap buffer overflow. An attacker can provide a malicious file to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
OpenImageIO Project OpenImageIO v2.4.4.2
OpenImageIO - https://github.com/OpenImageIO/oiio
9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-122 - Heap-based Buffer Overflow
OpenImageIO is an image processing library with easy-to-use interfaces and a sizable number of supported image formats. Useful for conversion and processing and even image comparison, this library is utilized by 3D-processing software from AliceVision (including Meshroom), as well as Blender for reading Photoshop .psd files.
The DirectDraw Surface file format (.dds) is another one of the file formats that libOpenImageIO can handle. It’s primarily used for DirectX and can contain a large number of textures, both compressed and uncompressed. When using libOpenImageIO to interact with .dds files, the same basic work flow occurs, in that we create a generic ImageInput object and then call ReadImage()
on our input file. After a certain amount of generic ImageInput
files, we end up hitting the DDSInput
object’s more specific handlers. Let’s quickly examine the structure of a .dds file before hitting code:
/// DDS file header.
typedef struct {
uint32_t fourCC; ///< file four-character code
uint32_t size; ///< structure size, must be 124
uint32_t flags; ///< flags to indicate valid fields
uint32_t height; ///< image height
uint32_t width; ///< image width
uint32_t pitch; ///< bytes per scanline (uncmp.)/total byte size (cmp.)
uint32_t depth; ///< image depth (for 3D textures)
uint32_t mipmaps; ///< number of mipmaps
uint32_t unused0[11];
dds_pixformat fmt; ///< pixel format
dds_caps caps; ///< DirectDraw Surface caps
uint32_t unused1;
} dds_header;
The header is 0x80 bytes long, read directly from our input file. The dds_pixformat
and dds_caps
structs are given below:
/// DDS pixel format structure.
///
typedef struct {
uint32_t size; ///< structure size, must be 32
uint32_t flags; ///< flags to indicate valid fields
uint32_t fourCC; ///< compression four-character code
uint32_t bpp; ///< bits per pixel
uint32_t masks[4]; ///< bitmasks for the r,g,b,a channels
} dds_pixformat;
/// DDS caps structure.
///
typedef struct {
uint32_t flags1;
uint32_t flags2;
uint32_t flags3;
uint32_t flags4;
} dds_caps;
When ImageSpec->open(filename)
is called by whatever code is utilizing libOpenImageIO, we eventually end up at the DDSInput::open()
function for .dds file-specific handling:
bool
DDSInput::open(const std::string& name, ImageSpec& newspec)
{
m_filename = name;
if (!ioproxy_use_or_open(name))
return false;
static_assert(sizeof(dds_header) == 128, "dds header size does not match");
if (!ioread(&m_dds, sizeof(m_dds), 1))
return false;
// [...]
// sanity checks - valid 4CC, correct struct sizes and flags which should
// be always present, regardless of the image type, size etc., also check
// for impossible flag combinations
if (m_dds.fourCC != DDS_MAKE4CC('D', 'D', 'S', ' ') || m_dds.size != 124
|| m_dds.fmt.size != 32 || !(m_dds.caps.flags1 & DDS_CAPS1_TEXTURE)
|| !(m_dds.flags & DDS_CAPS) || !(m_dds.flags & DDS_PIXELFORMAT)
|| (m_dds.caps.flags2 & DDS_CAPS2_VOLUME
&& !(m_dds.caps.flags1 & DDS_CAPS1_COMPLEX
&& m_dds.flags & DDS_DEPTH))
|| (m_dds.caps.flags2 & DDS_CAPS2_CUBEMAP
&& !(m_dds.caps.flags1 & DDS_CAPS1_COMPLEX))) {
errorf("Invalid DDS header, possibly corrupt file");
return false;
}
// make sure all dimensions are > 0 and that we have at least one channel
// (for uncompressed images)
if (!(m_dds.flags & DDS_WIDTH) || !m_dds.width
|| !(m_dds.flags & DDS_HEIGHT) || !m_dds.height
|| ((m_dds.flags & DDS_DEPTH) && !m_dds.depth)
|| (!(m_dds.fmt.flags & DDS_PF_FOURCC)
&& !(m_dds.fmt.flags
& (DDS_PF_RGB | DDS_PF_LUMINANCE | DDS_PF_ALPHA
| DDS_PF_ALPHAONLY)))) {
errorf("Image with no data");
return false;
}
At [1], we read in the 0x80 bytes for our struct dds_header m_dds
object variable. Following that, assorted validation of the file occurs. Continuing on:
bool
DDSInput::open(const std::string& name, ImageSpec& newspec)
{
// [...]
// validate the pixel format
if (m_dds.fmt.flags & DDS_PF_FOURCC) { // [2]
// [...]
}
// [...]
// determine the number of channels we have
if (m_compression != Compression::None) {
m_nchans = GetChannelCount(m_compression,
m_dds.fmt.flags & DDS_PF_NORMAL);
} else {
m_nchans = ((m_dds.fmt.flags & (DDS_PF_LUMINANCE | DDS_PF_ALPHAONLY))
? 1
: 3)
+ ((m_dds.fmt.flags & DDS_PF_ALPHA) ? 1 : 0);
// also calculate bytes per pixel and the bit shifts
m_Bpp = (m_dds.fmt.bpp + 7) >> 3; // [3]
if (!(m_dds.fmt.flags & DDS_PF_LUMINANCE)) {
for (int i = 0; i < 4; ++i)
calc_shifts(m_dds.fmt.masks[i], m_BitCounts[i],
m_RightShifts[i]);
}
}
Assuming that we don’t have the DDS_PF_FOURCC
flag set [2], our ImageSpec’s compression is set to Compression::None
. This brings us to the code path at [3] and causes our int m_Bpp
to be set to a value controlled directly from the input file. Still continuing in DDSInput::open()
:
bool
DDSInput::open(const std::string& name, ImageSpec& newspec){
// [...]
// fix depth, pitch and mipmaps for later use, if needed
if (!(m_dds.fmt.flags & DDS_PF_FOURCC && m_dds.flags & DDS_PITCH))
m_dds.pitch = m_dds.width * m_Bpp;
if (!(m_dds.caps.flags2 & DDS_CAPS2_VOLUME))
m_dds.depth = 1;
if (!(m_dds.flags & DDS_MIPMAPCOUNT))
m_dds.mipmaps = 1;
// count cube map faces
if (m_dds.caps.flags2 & DDS_CAPS2_CUBEMAP) {
m_nfaces = 0;
for (int flag = DDS_CAPS2_CUBEMAP_POSITIVEX;
flag <= DDS_CAPS2_CUBEMAP_NEGATIVEZ; flag <<= 1) {
if (m_dds.caps.flags2 & flag)
m_nfaces++;
}
} else
m_nfaces = 1;
seek_subimage(0, 0); // [4]
newspec = spec();
return true;
}
Again, more setting of object variables, but we enter an important call to seek_subimage(0, 0);
at [4]:
bool
DDSInput::seek_subimage(int subimage, int miplevel)
{
if (subimage != 0)
return false;
// early out
if (subimage == current_subimage() && miplevel == current_miplevel()) {
return true;
}
// [...]
// for cube maps, the seek will be performed when reading a tile instead
unsigned int w = 0, h = 0, d = 0;
TypeDesc::BASETYPE basetype = GetBaseType(m_compression);
Aside from more initialization, there is an important call to GetBaseType()
at [5], which opaquely tells the generic ImageSpec
object how many bytes a given pixel is. The function itself is rather simple:
static TypeDesc::BASETYPE
GetBaseType(Compression cmp)
{
if (cmp == Compression::BC6HU || cmp == Compression::BC6HS)
return TypeDesc::HALF;
return TypeDesc::UINT8;
}
For the Compression::BC6HU
and Compression::BC6HS
types, each pixel is two bytes, and for the rest only one byte. As such, continuing with our assumption of having Compression::None
, our TypeDesc is TypeDesc::UINT8
and each pixel is one byte. With that all covered, we can now examine how libOpenImageIO reads in the input scanline data from our .dds file:
bool
DDSInput::read_native_scanline(int subimage, int miplevel, int y, int z,
void* data)
{
lock_guard lock(*this);
if (!seek_subimage(subimage, miplevel))
return false;
// don't proceed if a cube map - use tiles then instead
if (m_dds.caps.flags2 & DDS_CAPS2_CUBEMAP)
return false;
if (m_buf.empty())
readimg_scanlines(); // [6]
size_t size = spec().scanline_bytes();
memcpy(data, &m_buf[0] + z * m_spec.height * size + y * size, size);
return true;
}
Assuming we’re dealing with scanlines, the first .dds-specific function we hit is DDSInput::read_native_scanline
. On first call to this function, we end up hitting the branch at [6], since our object’s m_buf
vector has not been initialized yet. To procede into readimg_scanlines
:
bool
DDSInput::readimg_scanlines()
{
//std::cerr << "[dds] readimg: " << ftell() << "\n";
// resize destination buffer
m_buf.resize(m_spec.scanline_bytes() * m_spec.height * m_spec.depth // [7]
/*/ (1 << m_miplevel)*/);
return internal_readimg(&m_buf[0], m_spec.width, m_spec.height, // [8]
m_spec.depth);
}
The overall flow of the function is rather simple, we allocate our m_buf
vector at [7] and then read in that many bytes at [8]. Figuring out how many bytes are allocated at [7] is rather annoying, but m_spec.height
and m_spec.depth
are read in directly from our dds_header
from the first 0x80 bytes of the file; m_spec.scanline_bytes
follows a few more function calls:
imagesize_t
ImageSpec::scanline_bytes(bool native) const noexcept
{
if (width < 0)
return 0;
return clamped_mult64((imagesize_t)width, (imagesize_t)pixel_bytes(native)); // [9]
}
At [9], the ImageSpec::width
variable corresponds to the dds_header.width
from our file. This value is multiplied against the return value of pixel_bytes(false)
:
size_t
ImageSpec::pixel_bytes(bool native) const noexcept
{
if (nchannels < 0)
return 0;
if (!native || channelformats.empty())
return clamped_mult32((size_t)nchannels, channel_bytes()); // [10]
At [10], we see the ImageSpec::nchannels
value being multiplied against channel_bytes()
. Still going with the Compression::None
assumption, nchannels
is set via the dds_header.fmt.flags
:
bool
DDSInput::open(const std::string& name, ImageSpec& newspec)
{
// [...]
m_nchans = ((m_dds.fmt.flags & (DDS_PF_LUMINANCE | DDS_PF_ALPHAONLY))
? 1
: 3)
+ ((m_dds.fmt.flags & DDS_PF_ALPHA) ? 1 : 0);
// [...]
Which brings us back to the channel_bytes
function:
OpenImageIO_v2_4::ImageSpec::channel_bytes (this=0x7ffff7fbbb20) at oiio-2.4.4.2/src/include/OpenImageIO/imageio.h:367
367 size_t channel_bytes() const noexcept { return format.size(); }
To stop from going deeper into defining macros, format.size()
is given by the TypeDesc
of our object. Since we’re dealing with Compression::None
, we have a DDSInput.basetype == TypeDesc::UINT8;
, and the size of our format is 0x1. Finally, putting all of these subsequent function calls together, we end up with our m_buf
vector at [7] having a size of m_spec.height * m_spec.dept * m_spec.width * m_spec.nchannels * format.size()
, with format.size()
only able to be 0x1 or 0x2 and m_spec.nchannels
able to only be 0x1 through 0x4. The rest of the values are controlled directly by the input file. Thus, let us see how the call to internal_readimg(&m_buf[0], m_spec.width, m_spec.height, m_spec.depth)
behaves at [8]:
bool
DDSInput::internal_readimg(unsigned char* dst, int w, int h, int d)
{
if (m_compression != Compression::None) {
// [...]
} else {
// uncompressed image // [11]
// HACK: shortcut for luminance
if (m_dds.fmt.flags & DDS_PF_LUMINANCE) {
return ioread(dst, w * m_Bpp, h); // [12]
}
std::vector<uint8_t> tmp(w * m_Bpp);
for (int z = 0; z < d; z++) {
for (int y = 0; y < h; y++) {
if (!ioread(tmp.data(), w, m_Bpp))
return false;
size_t k = (z * h * w + y * w) * m_spec.nchannels;
for (int x = 0; x < w; x++, k += m_spec.nchannels) {
uint32_t pixel = 0;
OIIO_DASSERT(tmp.size() >= size_t(x * m_Bpp + m_Bpp));
memcpy(&pixel, tmp.data() + x * m_Bpp, m_Bpp);
for (int ch = 0; ch < m_spec.nchannels; ++ch) {
dst[k + ch]
= bit_range_convert((pixel & m_dds.fmt.masks[ch])
>> m_RightShifts[ch],
m_BitCounts[ch], 8);
}
}
}
}
}
return true;
}
Since we’re again dealing with Compression::None
, we hit the branch at [11]. Assuming we have the DDS_PF_LUMINANCE
flag set in our dds_header
, we hit the call to ioread
at [12], which results in our m_buf
of size w * m_bpp * h
, or m_spec.width * m_bpp * m_spec.height
. If we look again at the size of m_buf
however, we see an issue:
m_spec.height * m_spec.depth * m_spec.width * m_spec.nchannels * format.size()
Since m_spec.height
and m_spec.width
are in both the size of the allocation and subsequent read, they don’t matter too much. They end up being m_bpp
versus m_spec.depth * m_spec.nchannels * format.size()
. Unfortunately for the library, there are no restrictions or validation of m_bpp
versus the others. As such, if m_spec.depth * m_spec.nchannels * format.size()
is 0x1 (which it can be), and our m_bpp
is 0x2, we end up reading twice the size of the m_buf
buffer, resulting in a heap overflow and subsequent code execution.
=================================================================
==409483==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60c000000c00 at pc 0x5555555ca52e bp 0x7fffffff9410 sp 0x7fffffff8be0
WRITE of size 134 at 0x60c000000c00 thread T0
[Detaching after fork from child process 409487]
#0 0x5555555ca52d in fread (/oiio/fuzzing_release/fuzz_oiio.bin+0x7652d) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#1 0x7fffeb62030a in OpenImageIO_v2_4::Filesystem::IOFile::read(void*, unsigned long) /oiio/oiio-2.4.4.2/src/libutil/filesystem.cpp:1161:16
#2 0x7ffff2d52901 in OpenImageIO_v2_4::ImageInput::ioread(void*, unsigned long, unsigned long) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageinput.cpp:1220:25
#3 0x7ffff3787c67 in OpenImageIO_v2_4::DDSInput::internal_readimg(unsigned char*, int, int, int) /oiio/oiio-2.4.4.2/src/dds.imageio/ddsinput.cpp:754:20
#4 0x7ffff378e6e6 in OpenImageIO_v2_4::DDSInput::readimg_scanlines() /oiio/oiio-2.4.4.2/src/dds.imageio/ddsinput.cpp:790:12
#5 0x7ffff379039e in OpenImageIO_v2_4::DDSInput::read_native_scanline(int, int, int, int, void*) /oiio/oiio-2.4.4.2/src/dds.imageio/ddsinput.cpp:829:9
#6 0x7ffff2d2b0c8 in OpenImageIO_v2_4::ImageInput::read_native_scanlines(int, int, int, int, int, void*) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageinput.cpp:399:19
#7 0x7ffff2d2bdfd in OpenImageIO_v2_4::ImageInput::read_native_scanlines(int, int, int, int, int, int, int, void*) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageinput.cpp:420:16
#8 0x7ffff2d27880 in OpenImageIO_v2_4::ImageInput::read_scanlines(int, int, int, int, int, int, int, OpenImageIO_v2_4::TypeDesc, void*, long, long) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageinput.cpp:336:15
#9 0x7ffff2d498d9 in OpenImageIO_v2_4::ImageInput::read_image(int, int, int, int, OpenImageIO_v2_4::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageinput.cpp:967:23
#10 0x55555566fa75 in LLVMFuzzerTestOneInput /oiio/fuzzing_release/./oiio_harness.cpp:90:18
#11 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing_release/fuzz_oiio.bin+0x414e3) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#12 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing_release/fuzz_oiio.bin+0x2b25f) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#13 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing_release/fuzz_oiio.bin+0x30fb6) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#14 0x5555555aedd2 in main (/oiio/fuzzing_release/fuzz_oiio.bin+0x5add2) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#15 0x7fffec2d5d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#16 0x7fffec2d5e3f in __libc_start_main csu/../csu/libc-start.c:392:3
#17 0x555555579b24 in _start (/oiio/fuzzing_release/fuzz_oiio.bin+0x25b24) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
0x60c000000c00 is located 0 bytes to the right of 128-byte region [0x60c000000b80,0x60c000000c00)
allocated by thread T0 here:
#0 0x55555566c90d in operator new(unsigned long) (/oiio/fuzzing_release/fuzz_oiio.bin+0x11890d) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#1 0x7ffff13d2411 in __gnu_cxx::new_allocator<unsigned char>::allocate(unsigned long, void const*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/new_allocator.h:127:27
#2 0x7ffff13d2293 in std::allocator_traits<std::allocator<unsigned char> >::allocate(std::allocator<unsigned char>&, unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/alloc_traits.h:464:20
#3 0x7ffff13d0d2b in std::_Vector_base<unsigned char, std::allocator<unsigned char> >::_M_allocate(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:346:20
#4 0x7ffff13ce381 in std::vector<unsigned char, std::allocator<unsigned char> >::_M_default_append(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/vector.tcc:635:34
#5 0x7ffff13cc26c in std::vector<unsigned char, std::allocator<unsigned char> >::resize(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:940:4
#6 0x7ffff378de4e in OpenImageIO_v2_4::DDSInput::readimg_scanlines() /oiio/oiio-2.4.4.2/src/dds.imageio/ddsinput.cpp:787:11
#7 0x7ffff379039e in OpenImageIO_v2_4::DDSInput::read_native_scanline(int, int, int, int, void*) /oiio/oiio-2.4.4.2/src/dds.imageio/ddsinput.cpp:829:9
#8 0x7ffff2d2b0c8 in OpenImageIO_v2_4::ImageInput::read_native_scanlines(int, int, int, int, int, void*) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageinput.cpp:399:19
#9 0x7ffff2d2bdfd in OpenImageIO_v2_4::ImageInput::read_native_scanlines(int, int, int, int, int, int, int, void*) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageinput.cpp:420:16
#10 0x7ffff2d27880 in OpenImageIO_v2_4::ImageInput::read_scanlines(int, int, int, int, int, int, int, OpenImageIO_v2_4::TypeDesc, void*, long, long) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageinput.cpp:336:15
#11 0x7ffff2d498d9 in OpenImageIO_v2_4::ImageInput::read_image(int, int, int, int, OpenImageIO_v2_4::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageinput.cpp:967:23
#12 0x55555566fa75 in LLVMFuzzerTestOneInput /oiio/fuzzing_release/./oiio_harness.cpp:90:18
#13 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing_release/fuzz_oiio.bin+0x414e3) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#14 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing_release/fuzz_oiio.bin+0x2b25f) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#15 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing_release/fuzz_oiio.bin+0x30fb6) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#16 0x5555555aedd2 in main (/oiio/fuzzing_release/fuzz_oiio.bin+0x5add2) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396)
#17 0x7fffec2d5d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
SUMMARY: AddressSanitizer: heap-buffer-overflow (/oiio/fuzzing_release/fuzz_oiio.bin+0x7652d) (BuildId: f09352d3bef5f556105d0e82941fa30a26694396) in fread
Shadow bytes around the buggy address:
0x0c187fff8130: fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa fa
0x0c187fff8140: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c187fff8150: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x0c187fff8160: 00 00 00 00 00 00 00 00 fa fa fa fa fa fa fa fa
0x0c187fff8170: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c187fff8180:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c187fff8190: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c187fff81a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c187fff81b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c187fff81c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c187fff81d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==409483==ABORTING
[Thread 0x7fffe65f9640 (LWP 409486) exited]
[Inferior 1 (process 409483) exited with code 01]
2022-10-19 - Initial Vendor Contact
2022-10-20 - Vendor Disclosure
2022-11-01 - Vendor Patch Release
2022-12-22 - Public Release
Discovered by Lilith >_> of Cisco Talos.