Text size
CVE-2025-15281January 20268 min read

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.

CVSS Score
7.5
Years Vulnerable
26
Affected Versions
2.0-2.42
Status
Patched
TL;DR

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()

1

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.

Real-World Impact

The advisory explicitly states: "There is no known application impact for this CVE." The feature was broken from day one.

2

How The Bug Works

Step 1 of 6

First wordexp() Call

Application

Process input, populate we_wordc and we_wordv

we_wordc = N, we_wordv allocated with N entries

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:

c
if (flags & WRDE_REUSE) {
    pwordexp->we_wordc = 0;
    pwordexp->we_wordv = NULL;
}

That's it. Two lines. 26 years.

3

The Vulnerable Code

Here's the minimal trigger that crashes glibc 2.0 through 2.42:

c
/*
* 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 wordexp

The uninitialized pointers in we_wordv contain garbage values that crash when accessed.

4

Impact Analysis

CVSS Score: 7.5 HIGH

MetricValueMeaning
Attack VectorNetworkCan be triggered remotely
Attack ComplexityLowNo special conditions needed
Privileges RequiredNoneNo authentication needed
User InteractionNoneNo user action required
ConfidentialityNoneNo data leak
IntegrityNoneNo data modification
AvailabilityHighProcess crash/DoS

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

DoS Only

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:

  1. Application uses wordexp() with untrusted input
  2. Application uses both WRDE_REUSE and WRDE_APPEND flags
  3. Application somehow worked despite this combination being broken

That's a narrow intersection.

5

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

DateEvent
December 29, 2025Vulnerability discovered via fuzzing
January 20, 2026Reported to glibc maintainers
January 20, 2026CVE-2025-15281 assigned
January 20, 2026Fix committed to master (80cc58ea)
January 26, 2026Test coverage added
glibc 2.43Official 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?

VS
Vitaly Simonovich
Security Researcher