Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
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.