|
|
Message-ID:
<DS4PR01MB994453E5B8286AE374178A1EA6EA6A2@DS4PR01MB994453.prod.exchangelabs.com>
Date: Mon, 2 Nov 2026 23:48:32 +0000
From: "Puranam, Ananta Srikar" <a.puranam@....edu>
To: "musl@...ts.openwall.com" <musl@...ts.openwall.com>
CC: "West, Emily" <emily.west1@....edu>, "Priya, Swarn" <swarn.priya@....edu>,
"Gao, Peng" <penggao@...edu>,
"Ammanaghatta Shivakumar, Basavesh"
<basavesh.shivakumar@....edu>
Subject: ldso: map_library does not validate e_phentsize
Hi,
I'm Ananta Srikar Puranam, a PhD student at Washington State University. I'm reporting this bug on behalf of my team: Emily West, Dr. Swarn Priya, Dr. Peng Gao and Dr. Basavesh Ammanaghatta Shivakumar.
map_library in ldso/dynlink.c never checks e_phentsize against sizeof(Phdr). It sizes the header read with e_phentsize * e_phnum (line 711 at 7813e911) and steps through the table by e_phentsize (line 727, also 764 and 825), but reads every entry as a full Phdr.
With e_phentsize set to 28 on x86_64, musl reads e_phnum records 28 bytes apart: only the first half of the real table, interleaved with bogus records that start in the middle of real ones. If PT_TLS is in the unread half, dlopen succeeds without TLS, dlsym of a TLS symbol returns NULL without setting dlerror, and the library crashes when it touches its own TLS. If PT_LOAD or PT_DYNAMIC is in the unread half, dlopen fails with "Exec format error".
glibc rejects the same file with "ELF file's phentsize not the expected size". Rejecting it with ENOEXEC when e_phentsize != sizeof(Phdr) seems like the expected behavior here.
Reproducer (musl 1.2.5, GNU ld 2.46, x86_64):
cat > lib.c <<'EOF'
__thread int tv = 42;
int get(void) { return tv; }
EOF
cat > main.c <<'EOF'
#include <dlfcn.h>
#include <stdio.h>
int main(int argc, char **argv) {
setvbuf(stdout, 0, _IONBF, 0);
void *h = dlopen(argv[1], RTLD_NOW);
printf("dlopen: %s\n", h ? "ok" : dlerror());
if (!h) return 1;
int *p = dlsym(h, "tv");
printf("dlsym(tv): %p (%s)\n", (void *)p,
p ? "" : (dlerror() ? "error" : "no dlerror"));
int (*get)(void) = (int (*)(void))dlsym(h, "get");
if (get) printf("get() = %d\n", get());
return 0;
}
EOF
musl-gcc -shared -fPIC -Wl,-z,noseparate-code -o good.so lib.c
musl-gcc -o main main.c
cp good.so bad.so
printf '\034' | dd of=bad.so bs=1 seek=54 conv=notrunc # 56 -> 28
./main ./good.so
./main ./bad.so
Output:
$ ./main ./good.so
dlopen: ok
dlsym(tv): 0x79fbaf627e30 ()
get() = 42
$ ./main ./bad.so
dlopen: ok
dlsym(tv): 0 (no dlerror)
Segmentation fault
good.so has 6 program headers (LOAD, LOAD, DYNAMIC, TLS, GNU_STACK, GNU_RELRO). With a 28-byte stride musl reads offsets 64, 92, ..., 204 and never reaches the TLS header at offset 232.
I ran this reproducer on 1.2.5. Our fuzzer's test file shows the same NULL dlsym on master at b1efda5b, and ldso/dynlink.c has not changed since then.
This is malformed input only; I'm not claiming a security issue.
The bug was found by an ELF loader fuzzer we are developing. I reproduced it by hand. This report was drafted with help from an AI assistant (Claude), and I reviewed and checked it before sending.
Thanks,
Srikar
Powered by blists - more mailing lists
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.