CVE-2026-38753: BusyBox awk replacement-string use-after-free
Summary
CVE-2026-38753 is a heap use-after-free in BusyBox awk. The substitution built-ins retain a raw pointer to the replacement text, then evaluate the regular-expression expression. That evaluation can modify AWK variables and free the allocation behind the retained pointer. awk_sub() subsequently passes the dangling pointer to strlen() and uses it while constructing the replacement result.
The CNA describes the impact as denial of service in BusyBox v1.38.0.
The bug is scored as CVSS 7.5 (High) by CISA-ADP.
Affected versions
BusyBox builds containing the awk applet are affected from version 1.00 through 1.38.0, inclusive. The latest tested development commit after the 1.38.0 release, ec0c5cc142f1, is also affected. No fixed release is available.
Root cause
exec_builtin() evaluates selected builtin arguments into temporary var objects. When an argument is needed as a string, it caches the pointer returned by getvar_s() in as[]:
av[i] = evaluate(an[i], TMPVAR(i));
if (isr & 0x08000000)
as[i] = getvar_s(av[i]);
/* ... some codes ... */
setvar_i(res, awk_sub(an[0], as[1], 1, av[2], av[2]));
For sub() and gsub(), as[1] is a borrowed pointer into AWK variable storage; ownership is not transferred to awk_sub(). Inside awk_sub(), the vulnerable order is:
regex = as_regex(rn, &sreg);
sp = getvar_s(src ? src : intvar[F0]);
replen = strlen(repl);
as_regex() may evaluate a non-literal regular-expression expression. The reproducer drives a side-effecting evaluation chain through evaluate(), copyvar(), handle_special(), setvar_i(), and clrvar(). clrvar() calls free(v->string) on the same allocation referenced by repl. Control then returns to awk_sub(), where strlen(repl) reads the freed region.
The bug is therefore not a generic regex-library lifetime problem. It is a BusyBox AWK ownership error: a borrowed replacement pointer is kept across interpreter work that is allowed to invalidate it.
Evidence
A small reproducer for the lifetime error is:
busybox awk 'BEGIN { r="1"; s="1"; sub(r="x", r, s); print s }'
On AddressSanitizer builds of Git tag 1_37_0 and the official 1.38.0 source tarball, this produced a heap-use-after-free in awk_sub(): as_regex() freed the string still referenced by repl, and the subsequent strlen(repl) read it. The tarball’s editors/awk.c is byte-for-byte identical to the file at the 1.38.0 release commit, fc71374dfccd.
On an -O0 -g3 AddressSanitizer build of commit ec0c5cc142f1, the saved reproducer produced:
ERROR: AddressSanitizer: heap-use-after-free
READ of size 3
#0 strlen
#1 awk_sub editors/awk.c:2720
#2 exec_builtin editors/awk.c:3031
freed by:
#1 clrvar editors/awk.c:1002
#2 setvar_i editors/awk.c:1052
#3 handle_special editors/awk.c:2248
#4 copyvar editors/awk.c:1133
#13 as_regex editors/awk.c:1988
previously allocated by:
#1 xstrdup libbb/xfuncs_printf.c:84
#2 getvar_s editors/awk.c:1076
#3 exec_builtin editors/awk.c:2886
I also stopped the program in GDB. At awk_sub() entry, repl pointed to a two-byte allocation containing "1". A conditional breakpoint in clrvar() then showed v->string equal to that exact address while the call stack was beneath as_regex(). When execution returned to line 2720, repl still held the same address, but its first bytes had changed from the original string to allocator data:
awk_sub entry: repl=0x...09d0 value=1
before clrvar free: v->string=0x...09d0
before strlen: repl=0x...09d0 bytes=12 03 00 00 ...
That debugger sequence proves the lifetime transition directly: valid borrowed string, free during regex evaluation, then reuse of the unchanged pointer.
Fix
My submitted patch takes ownership of a stable copy before the side-effecting call:
repl_copy = xstrdup(repl);
regex = as_regex(rn, &sreg);
repl = repl_copy;
/* substitution loop */
free(repl_copy);
I applied that patch to the current upstream source and reran the original reproducer under AddressSanitizer. It exited with status 0 and no sanitizer finding.
The patch was submitted on 2026-06-16 and is waiting for review.
Exploit
The attacker must be able to supply an AWK program that reaches the side-effecting substitution expression. The demonstrated primitive is a read from freed heap storage. The replacement loop can continue consuming those stale bytes, but I did not demonstrate a controlled memory disclosure, write primitive, instruction-pointer control, or code execution.
To separate sanitizer detection from ordinary runtime behavior, I ran the reproducer on a non-sanitized x86-64 glibc build with MALLOC_PERTURB_ values 0, 85, 170, and 255 and with the tcache disabled. All four runs exited with status 0 and no visible output. Thus the UAF is confirmed, but this reproducer does not establish a reliable non-sanitized crash.
Mitigation
As of 2026-07-16 there is no merged upstream fix. Please do not execute untrusted AWK programs with an affected BusyBox awk. Integrators can backport the public patch that copies the replacement string before as_regex() and should validate it with a sanitizer build.
References
- CVE.org: CVE-2026-38753
- NVD: CVE-2026-38753
- BusyBox patch:
awk: fix use-after-free in awk_sub() - BusyBox AWK source introduction:
545106f8db3e - BusyBox 1.37.0 tag commit
- BusyBox 1.38.0 release announcement and changelog
- BusyBox 1.38.0 source tarball
- BusyBox 1.38.0 release commit:
fc71374dfccd - BusyBox upstream head used for re-verification