Summary
This does not require a modded client or a launcher exploit. It starts with an ordinary server resource pack, the usual acceptance prompt, and a bitmap-font PNG.I found and reproduced two separate bugs in the official Minecraft Java Edition 1.21.8 Windows client. Both are reachable through server-supplied bitmap fonts decoded by the native
stb_image library. One bug crashes the process. The other copies uninitialized native stack memory into pixels. I also built a proof of concept showing that a server can learn information about those pixels without the client ever sending the image back.TL;DR
- Bug 1 (crash). A large 16-bit PNG wraps STB's output-size calculation. STB allocates about 190 KiB, then runs a conversion that expects more than 4 GiB of output space. The result is a sequential native heap overwrite. Both recorded official-client runs crashed in
lwjgl_stb.dll. - Bug 2 (disclosure). A 355-byte indexed PNG declares a one-entry palette but refers to higher palette indices. STB expands those indices through an uninitialized 1 KiB stack palette. This copies up to 1,020 bytes of native stack data into RGBA pixels, which Minecraft then uploads into a bitmap-font atlas.
- Why it matters. The leaked pixels contained live code pointers into
jemalloc.dll,jvm.dllandlwjgl_stb.dll. A text shader supplied by the same pack can turn tests against those pixels into latency differences that the server can measure through normal Ping/Pong traffic. - Limits. I did not demonstrate an arbitrary write, instruction-pointer control or RCE. I also did not dump complete pointers through server-side timing alone.
- Status. Neither supplied test case triggers its bug in 26.3 Pre-Release 1, released September 1, 2026. I reported both bugs privately on August 16, 2026, but received no response or acknowledgement after the report was forwarded.
Test setup
I reproduced everything below on a stock, unmodified client.
| Component | Version |
|---|---|
| Minecraft | Java Edition 1.21.8, official client |
| Java | Microsoft OpenJDK 21.0.7+6 |
| LWJGL STB | 3.3.3, stb_image 2.28 |
| OS | Windows 11 x64 |
Code:
Client JAR: EA74E9C5E92D01F95F3D39196DDB734E19A077A58B4C433C34109487D600276F
lwjgl_stb.dll: DE03F38F8C7471B4924AA8BDF8724C181D1A7C20428DBCE2ED301877E1D387E4
jemalloc.dll: 9ABF0095066EBAB37B78968E11370A8078313E48CB5BE8EDA01F67623C6A6248
Required user interaction: the player has to accept the server resource pack. If packs are already enabled for that server, there is no prompt at all. Once the pack is accepted, the resource reload reaches the vulnerable code without any further input.
Bug 1: the image that needs 4 GiB and gets 190 KiB
The crash path is straightforward.
Code:
server resource-pack push
-> accepted pack download
-> resource reload
-> bitmap font provider
-> NativeImage.read(RGBA, InputStream)
-> STBImage.stbi_load_from_memory(..., req_comp = 4)
-> undersized allocation
-> out-of-bounds writes
The PNG is 23,171 x 23,171 pixels and uses 16-bit grayscale samples. Minecraft requests RGBA output, so the decoder needs:
Code:
23171 * 23171 * 4 * 2 = 4,295,161,928 bytes = 0x1_0002F848
That value does not fit in 32 bits. The calculation wraps modulo 2^32, leaving this allocation size:
Code:
0x0002F848 = 194,632 bytes
The mismatch is the bug. The allocation uses the wrapped value, while the conversion loop still walks the full image dimensions. It runs past a buffer of about 190 KiB while attempting to produce more than four gigabytes of output.
Each output pixel contains three copies of an attacker-chosen 16-bit grayscale sample followed by a fixed
0xffff alpha value. The resulting primitive is a partially controlled sequential native heap overwrite. It does not provide an arbitrary target address, but it crashed the process in both recorded runs.Both of my official-client runs crashed on worker threads at the same native write access violation inside the 16-bit conversion loop:
Code:
lwjgl_stb.dll + 0x5f27
The byte-identical payload also worked when I placed it in an otherwise ordinary resource pack.
Bug 2: 355 bytes of PNG, 1,020 bytes of your stack
This bug is independent of the overflow, and I consider it the more interesting of the two.
The input is a 16 x 16 indexed PNG that is only 355 bytes long. Its structure and checksums are well formed, but its pixel data refers to palette entries that were never declared.
It contains one palette entry in
PLTE. The image data uses indices 0 through 255.STB stores its expanded RGBA palette in a 1,024-byte object on the native stack. It initializes only the entries supplied by
PLTE, then expands the image without rejecting indices beyond the declared palette length.With only entry zero initialized, the remaining 1,020 bytes of stack data can be copied into the decoded pixels. STB returns the decode as successful, and Minecraft uploads the result into the bitmap-font atlas.
That upload is what makes this more than a local uninitialized-read bug. The leaked bytes remain available as rendered texture data that other pack-controlled rendering code can sample.
Pointers in the decoded pixels
My first full scan found three pointer-shaped values in the decoded image:
Code:
0x00007ff9311f311e = jemalloc.dll + 0x3311e
0x00007ff9311f3573 = jemalloc.dll + 0x33573
0x00007ff9311f399e = jemalloc.dll + 0x3399e
All three resolve to the same base address:
Code:
jemalloc.dll = 0x00007ff9311c0000
That gave me a useful sanity check. Three separate leaked values all landed inside the same loaded module, rather than just looking vaguely like pointers.
The stack contents changed on a second resource reload, as expected. That run exposed six code pointers into
jvm.dll and one into lwjgl_stb.dll elsewhere in the same 256-pixel image. Every value resolved inside the matching module loaded by that client process.The bytes were not fixed decoder padding or a predictable fill pattern. They came from live native process state and included ASLR-relevant addresses.
Making the leak visible to the server
The next question was whether the server could learn anything about those pixels without the client sending the image back.
It can. Resource packs can replace the text shader, which gives the pack an indirect route to those pixels.
The shader samples a selected glyph channel, compares it with a threshold from the probe logic, and chooses either a bounded fast path or a bounded slow path. The server displays the probe text and measures normal Ping/Pong latency. This turns a yes-or-no test against a leaked pixel into a timing bit.
I first calibrated the setup with controlled holdout values:
Code:
recovery predicate: fast 2/840 long RTTs, slow 392/420 long RTTs
canonical predicate: fast 1/840 long RTTs, slow 393/420 long RTTs
cutoff: RTT > 90 ms
acquisition errors: 0
Individual RTTs overlapped, but the acquisition logic still made zero classification errors in the calibration run. I then moved the probe to the live texture. It checked all 128 non-overlapping pixel pairs and flagged 14 candidates whose upper bytes matched the canonical form of Windows user-mode pointers. It then tested nearby lower bytes against a fixed set of known return-address signatures.
I did not recover complete pointers from server-side timing alone. Those values came from full local scans of the decoded pixels, which I checked against the process module map. The timing test demonstrates a narrower point: the server can query predicates over the same leaked native stack bytes without ever receiving the texture. That is the end-to-end result shown here.
Why this works at all
The full chain depends on three trust-boundary failures lining up:
- A server-controlled resource pack passes attacker-controlled image data to a native decoder.
- That decoder has an output-size overflow and fails to validate indexed-palette references.
- Minecraft makes decoded bitmap-font pixels available to pack-controlled rendering, including a replaceable text shader.
The crash needs only the first two conditions. The remotely observable disclosure needs all three.
None of this requires a modified client. The only security boundary visible to the player is the normal resource-pack acceptance prompt.
Impact
Demonstrated:
- Network-originated native heap corruption after resource-pack acceptance
- Termination of the official game process in both recorded runs
- Disclosure of native process memory through bitmap-font pixels
- ASLR-relevant code pointers into loaded native modules
- A server-observable timing oracle over predicates derived from leaked pixels
Not demonstrated: arbitrary write control, instruction-pointer control, remote code execution.
I am not calling this RCE. I got heap corruption in both recorded runs, but not control of the write target or the instruction pointer. Those are different claims.
For the end-to-end scenario demonstrated here, I used this CVSS v3.1 vector:
Code:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:H = 7.1 High
If resource packs are already enabled for the server, there is no new acceptance prompt. Treating that case as
UI:N produces a score of 8.2 High. Since the crash and disclosure are independent bugs, they can also be scored separately.The relevant weakness classes are
CWE-787 for the out-of-bounds write, plus CWE-125 and CWE-908 for the uninitialized-memory disclosure.Timeline and disclosure
| Date | Event |
|---|---|
| 2026-08-16 | Reported privately to Mojang through a community manager, who forwarded it to the relevant team |
| -- | No response or acknowledgement received after the report was forwarded |
| 2026-09-01 | 26.3 Pre-Release 1 shipped with a validation check that blocks the issue. The change was not mentioned in the public changelog |
| Today | Public writeup |
Neither supplied test case triggers its bug on 26.3 Pre-Release 1. That is enough for me to treat both client-side paths as fixed in that build, while the tested stable client remains affected.
I would still have preferred a direct response from Mojang, especially when a server resource pack can trigger native memory corruption and disclosure. I held publication until I had tested the current pre-release myself.