CVE-2026-38754: BusyBox ash stale-IFS-region out-of-bounds read
Summary
CVE-2026-38754 is an out-of-bounds read in BusyBox ash argument expansion. An expansion error can escape through longjmp() after IFS split regions have been recorded but before expandarg() reaches its normal ifsfree() cleanup. A later expansion reuses the stack string area for shorter data, while ifsbreakup() still trusts the older offsets and reads beyond the current string’s valid storage.
The bug is scored as CVSS 7.5 (High) by CISA-ADP.
Affected versions
BusyBox builds containing the ash applet are affected from version 1.00 through 1.38.0, inclusive. Development commits after the 1.38.0 release through 5de0780eaf4e are also affected. Commit a448b6d5b21e and later commits are fixed, but no released version contains the fix yet.
Root cause
ash records the portions of an expanded string that are eligible for IFS splitting in a global linked list rooted at ifsfirst; ifslastp indicates that saved state is active. The regions contain integer offsets rather than owning the expanded string:
struct ifsregion {
struct ifsregion *next;
int begoff;
int endoff;
int nulonly;
};
On the normal path, expandarg() releases and clears this state at its out: label:
argstr(arg->narg.text, flag);
/* split the completed string */
ifsbreakup(p, &exparg);
out:
ifsfree();
The reproducer first records IFS regions and then raises an arithmetic expansion error inside argstr(). The exception uses longjmp(), so control never reaches the out: cleanup in that invocation. Before the fix, expansion-related handlers such as redirectsafe(), expandstr(), and the ERR-trap path in evaltree() could catch EXERROR and continue without clearing the IFS globals.
A later expansion stores a shorter string in the reusable stack-string area. ifsbreakup() then calculates p = string + ifsp->begoff and loops until string + ifsp->endoff, even though both offsets describe the older expansion. The stale logical boundary can extend past the current allocation.
Evidence
On AddressSanitizer builds, the input reproduced at Git tag 1_37_0 (be7d1b7b1701), in the official 1.38.0 source tarball, and at commit 5de0780eaf4e, immediately before the upstream fix. The tarball’s shell/ash.c is byte-for-byte identical to the file at the 1.38.0 release commit, fc71374dfccd. The 5de0780eaf4e run produced:
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
#0 ifsbreakup shell/ash.c:6463
#1 expandarg shell/ash.c:8410
#2 fill_arglist shell/ash.c:9189
#3 evalcommand shell/ash.c:10957
0x...02a0 is located 0 bytes after 544-byte region
[0x...0080, 0x...02a0)
GDB stopped immediately before the invalid dereference. The later string was "aEOF", but the active region still described offsets 51 through 58:
string="aEOF"
p - string = 52
ifsp->begoff = 51
ifsp->endoff = 58
ifslastp != NULL
Those values directly demonstrate the mismatch: offsets from a much longer prior expansion were being applied to a four-character string.
I then ran the same input on upstream commit ec0c5cc142f1, which contains the fix. GDB caught the expansion error with ifslastp non-NULL on entry to the new cleanup helper and showed both ifslastp and ifsfirst.next as NULL on return. The malformed script later exited 127 for an expected command not found, but it did not produce sanitizer findings.
Fix
The merged fix follows the root-cause approach used by dash: clear IFS state when an expansion-related handler catches EXERROR and elects to continue.
static void
restore_handler_expandarg(struct jmploc *savehandler, int err)
{
exception_handler = savehandler;
if (err) {
if (exception_type != EXERROR)
longjmp(exception_handler->loc, 1);
ifsfree();
}
}
redirectsafe(), expandstr(), and the relevant evaltree() handler now use this helper. Clearing the stale state at the point where normal control flow resumes prevents the next expansion from seeing obsolete offsets; it is stronger than merely clipping the later read.
BusyBox merged the patch on 2026-07-06 as a448b6d5b21e.
Exploit
The attacker needs to make an ash process evaluate crafted shell input that first leaves stale IFS state and then performs another expansion. The confirmed primitive is a one-byte-at-a-time out-of-bounds read during word splitting.
I ran the same input on a non-sanitized x86-64 glibc build with MALLOC_PERTURB_ values 0, 85, 170, and 255 and the tcache disabled. Each run completed without a memory fault and ended at the malformed script’s command not found path. I did not obtain a controlled information disclosure, write primitive, code execution, or privilege transition.
The related dash history discusses address disclosure from a similar stale-region bug, but that is not proof that this BusyBox reproducer leaks useful data. I therefore claim only the sanitizer-confirmed out-of-bounds read and the CNA’s denial-of-service impact.
Mitigation
Use a BusyBox tree that contains commit a448b6d5b21e, or backport that commit to the vendor’s BusyBox version.
References
- CVE.org: CVE-2026-38754
- NVD: CVE-2026-38754
- BusyBox v3 patch
- BusyBox source-history lower bound:
c470f4477ad7 - 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 - Last vulnerable commit:
5de0780eaf4e - Merged BusyBox fix:
a448b6d5b21e dashcleanup commit referenced by the BusyBox discussion