26 Years. Two Struct Fields.
glibc wordexp() Memory Bug
A POSIX function that never properly initialized memory when reusing buffers. The fix? Reset two struct fields. The twist? The feature never worked anyway.

WRDE_REUSE + WRDE_APPEND flags used together -> we_wordc not reset to zero -> Wrong offset write in we_wordv array -> Uninitialized pointers left in array -> Crash on dereference or wordfree()
Technical Overview
I found a memory initialization flaw in glibc's wordexp() function through fuzzing with libFuzzer and AddressSanitizer. The bug has existed since glibc 2.0 - 26 years of code that never properly handled a specific flag combination.
The vulnerability triggers when wordexp() is called with both WRDE_REUSE and WRDE_APPEND flags. The function fails to reset the we_wordc field, causing it to write results at the wrong offset in the word vector array.
What is wordexp()?
wordexp() is a POSIX function that performs shell-like word expansion:
- Tilde expansion (
~user->/home/user) - Variable expansion (
$HOME->/home/user) - Command substitution (when not disabled)
- Glob patterns (
*.txt)
The WRDE_REUSE flag tells it to reuse memory from a previous call. WRDE_APPEND tells it to append new results to existing ones.
The Twist
Here's the interesting part: according to the glibc maintainers, this flag combination never worked correctly. From the advisory:
"Calls to wordexp using WRDE_REUSE and WRDE_APPEND have never worked correctly"
So while the bug is real and spans 26 years, any application that tried to use this feature would have broken immediately. The practical impact is minimal because nobody could have successfully depended on this broken codepath.
The advisory explicitly states: "There is no known application impact for this CVE." The feature was broken from day one.
How The Bug Works
First wordexp() Call
Process input, populate we_wordc and we_wordv
The Root Cause
When WRDE_REUSE is set, wordexp() is supposed to reuse the existing wordexp_t structure. But it never resets we_wordc to zero before starting.
With WRDE_APPEND, it tries to append new words starting at index we_wordc. But after wordfree(), that index points past the reallocated array bounds, or into uninitialized memory.
The fix is trivial - reset we_wordc and we_wordv when WRDE_REUSE is set:
if (flags & WRDE_REUSE) {
pwordexp->we_wordc = 0;
pwordexp->we_wordv = NULL;
}That's it. Two lines. 26 years.
The Vulnerable Code
Here's the minimal trigger that crashes glibc 2.0 through 2.42:
/*
* minimal_trigger.c - CVE-2025-15281 PoC
* Build: gcc -fsanitize=address -o poc minimal_trigger.c
*/
#include <string.h>
#include <wordexp.h>
int main(void) {
char trigger[] = "\xf3\xf3\xf3\x00" "cccz]\"";
wordexp_t p = {0};
int flags = WRDE_NOCMD | WRDE_APPEND;
wordexp(trigger, &p, flags);
wordfree(&p);
memset(&p, 0, sizeof(p));
wordexp(trigger, &p, flags);
wordexp(trigger + 1, &p, flags | WRDE_REUSE); /* CRASH */
wordfree(&p);
return 0;
}What Happens
Without sanitizers:
free(): invalid pointer
Aborted (core dumped)With AddressSanitizer:
==PID==ERROR: AddressSanitizer: SEGV on unknown address
The signal is caused by a READ memory access.
#0 in wordexpThe uninitialized pointers in we_wordv contain garbage values that crash when accessed.
Impact Analysis
CVSS Score: 7.5 HIGH
| Metric | Value | Meaning |
|---|---|---|
| Attack Vector | Network | Can be triggered remotely |
| Attack Complexity | Low | No special conditions needed |
| Privileges Required | None | No authentication needed |
| User Interaction | None | No user action required |
| Confidentiality | None | No data leak |
| Integrity | None | No data modification |
| Availability | High | Process crash/DoS |
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
This is a Denial of Service vulnerability. The crash is reliable, but there's no path to code execution or data exfiltration. The CVSS reflects this - Confidentiality and Integrity impacts are both None.
Real-World Impact: Minimal
The glibc maintainers explicitly noted:
"There is no known application impact for this CVE, and the feature is generally non-functional"
The WRDE_REUSE + WRDE_APPEND combination never worked correctly. Any application that tried to use it would have discovered the bug immediately through failures. This is a case where a 26-year-old bug had no real-world impact because the broken code path was unusable.
Who's Actually Affected?
Realistically: probably nobody. The conditions required are:
- Application uses wordexp() with untrusted input
- Application uses both WRDE_REUSE and WRDE_APPEND flags
- Application somehow worked despite this combination being broken
That's a narrow intersection.
Mitigation
Update glibc to 2.43 (or backported patch)
The fix is committed and backported to versions 2.31-2.42. Update when your distribution releases the patch.
Low urgency for most environments
Given zero known affected applications and the flag combination never working, this is a low-priority update for most environments.
Avoid wordexp() with untrusted input
General best practice regardless of this CVE. Use glob() for pattern matching, or implement custom expansion.
Don't use WRDE_REUSE + WRDE_APPEND together
This combination was always broken. If you're somehow using it, your code was never working correctly anyway.
Timeline
| Date | Event |
|---|---|
| December 29, 2025 | Vulnerability discovered via fuzzing |
| January 20, 2026 | Reported to glibc maintainers |
| January 20, 2026 | CVE-2025-15281 assigned |
| January 20, 2026 | Fix committed to master (80cc58ea) |
| January 26, 2026 | Test coverage added |
| glibc 2.43 | Official release with fix |
The Fix
Commit 80cc58ea2de214f85b0a1d902a3b668ad2ecb302: "Reset wordexp_t fields with WRDE_REUSE"
The patch simply resets the struct fields when WRDE_REUSE is specified, ensuring consistent state regardless of previous calls.
The Real Story
This CVE is an interesting case study in vulnerability severity vs. real-world impact.
By the numbers, it looks concerning: CVSS 7.5 HIGH, 26 years of affected code, every Linux system running glibc. But dig deeper and you find a bug in code that never worked - a feature so broken that nobody could have used it successfully.
It's a reminder that fuzzing finds edge cases that humans miss, but also that not every bug with a CVE and a scary-looking CVSS score is actually exploitable in practice. The flag combination was DOA from day one.
How many "features" in your dependencies have been broken since day one but never noticed because nobody actually uses them?
