Source: NSB Cyber
Technical Breakdown
TL;DR
TL;DR: We dug into the technical breakdown of CVE-2026-83844 (Out of Bounds Read) affecting Microsoft Paint. From our testing, we found no evidence of a zero-click, listening-service, or wormable path (as several sources claimed). A user would need to explicitly ‘open’ the payload image in Microsoft Paint for this vulnerability to be triggered.
1. Background: where the untrusted data comes from
Modern Paint supports Content Credentials (C2PA) — cryptographically signed provenance metadata embedded in, or linked from, media files. When Paint ingests an image that carries C2PA data (or references a remote manifest over the network), it parses that manifest before anything is verified or displayed.
In the vulnerable build (11.2605.81.0), all of this parsing was handled by provenancesdk.dll, a Microsoft-authored C++ library (linked against OpenSSL via libcrypto-3-x64.dll). A C2PA manifest is a JUMBF box structure whose payloads are encoded in CBOR (RFC 8949) and wrapped in a COSE_Sign1 signature object. The SDK contains hand-written parsers for each layer:
Because a manifest's bytes are fully attacker-controlled and are parsed before trust is established, any memory-safety defect in these parsers is reachable by an attacker who can get a crafted manifest in front of the victim's Paint instance — including over the network.
2. The low-level readers are correct
The byte-level primitives in ByteHelpers.h are not the bug. Each performs a real runtime bounds check against a remaining byte counter and decrements it on success (addresses from the shipped provenancesdk.dll):
These are sound: they return an error (0x24 / 0xf) rather than reading past the buffer, and they correctly keep cursor and remaining in lockstep. The JUMBF box walk is likewise correctly bounded — ParseJUMBFBoxHeader (0x1801731f0) rejects the classic LBox < header_size underflow and validates box size against the remaining buffer with unsigned comparisons (JUMBFBoxBase::Deserialize 0x1801700f0, JUMBFSuperBox::DeserializePayload 0x180171750).
The defect is one layer up, in how the CBOR item decoder maintains that remaining counter for its caller.
3. Root cause: a stale remaining counter
CBORDataItem::CreateFromBuffer (0x180164b20, CBOR.cpp) decodes one CBOR item and is responsible for updating the caller's cursor (current read position) and remaining (bytes left in the buffer) as in/out parameters. It updates the cursor correctly but writes the original, un-decremented value back to remaining:
The decompiled store remaining = lVar6; (and at the instruction level, MOV [reg], R13 where R13 holds the entry value of remaining) is the entire defect. The consumed size is added to the cursor but never subtracted from remaining. The correct behavior is *remaining = lVar6 - lVar11;.
In isolation a single stale write is harmless — but CBOR containers are parsed in a loop.
4. Amplification: the container element loop
CBORArrayItem/CBORMapItem::DeserializePayload (0x180166a50) parses the elements of a CBOR array or map. It snapshots the cursor and remaining into locals once, then loops an attacker-controlled number of times, calling CreateFromBuffer on those locals each iteration:
Because CreateFromBuffer never shrinks local_remaining, the loop produces a diverging state:
local_cursor marches forward by each element's consumed size.
local_remaining stays pinned at the container's initial (full) value.
Once local_cursor reaches and passes the end of the real manifest buffer, every subsequent element decode still sees a remaining that claims plenty of bytes are left. The otherwise-correct bound checks in ReadBE/ReadBytes (sizeof(T) <= remaining, n <= remaining) are therefore evaluated against a stale, too-large value and pass, so the reads are issued at an out-of-bounds cursor — past the end of the heap allocation holding the manifest.
The attacker controls:
the array/map element count (count), i.e. how many times the loop over-reads;
each element's declared length (decoded by ReadArgument 0x180167980, a 1/2/4/8-byte value), i.e. how far past the end each read reaches and how many bytes are touched.
The indefinite-length container branch (selected by the container flag at object offset +0x28) is worse still: it has no count bound and loops until it encounters a CBOR 0xFF "break" byte, so the cursor can run arbitrarily far into out-of-bounds memory until such a byte happens to appear.
5. Reachability and impact
Confirmed parse path (observed live under a debugger, found below). The manifest is parsed when Paint ingests a crafted image; in the lab this was a PNG opened in Paint, whose embedded C2PA manifest (carried in a caBX chunk) is read through the ingredient path:
CBOR arrays/maps are intrinsic to a C2PA claim / assertion store / COSE_Sign1, so the loop is unavoidably on the manifest-parse path, and the read happens before signature verification — no valid certificate or signature is required to reach it.
On the "network" vector (tested): the crafted image arrives over the network in the way that matters for the rating — download, email, web — and the victim need only open/ingest it. We explicitly tested the two lower-interaction network paths on the vulnerable build, and both are negative:
Remote-manifest fetch — no. An image whose XMP dcterms:provenance referenced a remote manifest URL did not cause Paint to fetch it (a local listener logged zero requests on open). This matches the static read: provenancesdk.dll has no HTTP client (WinHTTP/WinINet/urlmon not imported; its only URLs are Microsoft PKI CRL/OCSP endpoints from cert validation).
Explorer preview/thumbnail — no. No image type (.png/.jpg/.jpeg/.paint) registers a shell thumbnail/preview handler that points at a Paint DLL — all route to the Windows system PhotoMetadataHandler.dll. Paint's provenancesdk.dll is not a shell handler, so previewing/thumbnailing a file cannot invoke the parser.
Net: the trigger requires the victim to open/ingest the crafted image in Paint. The CVSS AV:N is consistent with a client file-parser whose input is network-delivered, with user interaction required (UI:R); we found no evidence of a zero-click, listening-service, or wormable path. (An earlier draft over-stated a remote-fetch "beacon" vector; corrected here.)
The primitive is an attacker-sized out-of-bounds read from a heap buffer — an information-disclosure / crash primitive (crash observed live, §6). Combined with heap grooming and the adjacent-object control a structured parser like this affords, it is the basis for the remote code execution the vendor rating reflects (CVSS 8.1, network, unauthenticated). We demonstrated the read/crash primitive; we did not demonstrate code execution.
6. Dynamic confirmation (live reproduction)
The static root cause was reproduced on the vulnerable build (11.2605.81.0, isolated Windows 11 VM) under WinDbg cdb with full page-heap on mspaint.exe:
Reachability. Opening a crafted PNG (a valid 1×1 image plus a caBX chunk carrying the manifest) hit module+offset breakpoints in order: ParseJUMBFBoxHeader ×10, then CreateFromBuffer, then DeserializePayload — with a call stack of mspaint → ProvenanceHelper!InsertFileIngredientIfManifestExists → provenancesdk CBOR frames. The crafted image reaches the vulnerable loop.
The stale counter, observed. At the DeserializePayload entry, remaining was frozen at 0x4004 (16388) while the cursor advanced — exactly the defect; !heap -p -a showed the CBOR buffer is an ordinary heap allocation.
The fault. The element loop walked the cursor off the end of the buffer into an unmapped page:
The claim's CBOR was a definite-length array declaring 0x8000 elements over a payload of 0x4000 single-byte (0x00) elements: the array pre-sizes (allocatable), the real elements parse, then the loop keeps reading past the buffer because remaining never shrank, until an unmapped page faults. The faulting address varies per run (ASLR/heap layout) but reproduced across consecutive runs. See the reproduction bundle (CVE-2026-83844_repro/) for the trigger, generator, and the debugger transcript.
7. What the patch did
The fix shipped in 11.2609.401.0 is not a one-line *remaining = lVar6 - lVar11; correction. Microsoft replaced the entire hand-written C++ provenance stack with a memory-safe implementation:
Why the rewrite fixes the bug class, not just the instance:
In c2pa-rs, buffer traversal is expressed with Rust slices and iterators whose bounds are enforced by the language/standard library. The specific failure mode here — manually keeping a cursor and a remaining counter in sync and silently forgetting to decrement one of them — cannot be expressed the same way; an equivalent slip is a compile error or a safe panic, not an out-of-bounds read.
The CBOR/JUMBF/COSE decoding is now performed by a widely-used, independently-audited, memory-safe library rather than bespoke parsers, shrinking the attack surface to reviewed code.
The replacement is accompanied by an independent certificate-verification layer in ProvenanceHelper.dll, reducing reliance on the parser's own trust decisions.
Verification from the shipped binaries: the current 11.2609.401.0 provenance path (c2pa_c.dll + ProvenanceHelper.dll) shows the classic length-driven parser pitfalls either absent (Rust bounds enforcement) or correctly guarded, and dynamic testing of the patched build showed no beacon/parse activity on the networked remote-manifest path — consistent with the vulnerable code having been removed rather than merely patched in place.
Operational note: there was published confusion about the fixed version — Microsoft initially cited 11.2605.91.0, the Store served some users 11.2605.81.0, and the MSRC page ultimately lists 11.2609.401.0 as the fix. Administrators should confirm the installed Paint version directly (Get-AppxPackage Microsoft.Paint) rather than trust Store/auto-update status, and ensure it is >= 11.2609.401.0.
Analysis performed on the shipped x64 binaries (vulnerable 11.2605.81.0 extracted from the MSIX bundle; patched 11.2609.401.0 as installed) via static decompilation, then confirmed dynamically on an isolated VM running the vulnerable build (WinDbg cdb + full page-heap). Static addresses are image-relative (base 0x180000000) to each respective provenancesdk.dll / c2pa_c.dll; runtime addresses are given as provenancesdk!PROV_ValidationGetStatus+offset (nearest exported symbol). Findings were verified by first-hand decompilation of CBORDataItem::CreateFromBuffer (0x180164b20) and DeserializePayload (0x180166a50) and by a reproduced 0xC0000005 out-of-bounds read.
Want to stay on top of the latest breaches and incidents? Subscribe to our LinkedIn Newsletter, visit our Signals page every Friday and explore our latest Blogs.

