FForge
← Labsdebug-02Runtime debugging●●○○

Uninitialized / wild pointer (nondeterministic UB)

debug/02_uninit_wild_ptr.c3 steps
0/3 steps
evidence to leave with
Exit codes from 16 runs differ; deterministic under setarch -R; valgrind silent because the address is mapped
walk it, one step at a time
run this
$ clang -g -O0 02_uninit_wild_ptr.c -o bin/02_uninit_wild_ptr
$ for i in $(seq 1 16); do ./bin/02_uninit_wild_ptr; echo -n "$? "; done; echo
what it printed on the machine that wrote this lab
216 248 168 232 120 200 88 168 120 152 216 40 248 200 56 184
^ representative — YOURS WILL DIFFER, and that is exactly the bug. The
  values are all multiples of 8 because they are the low byte of a
  16-byte-aligned stack address. No 139 here: it never crashed.

FORCE the crash (deterministic SIGSEGV) and pin ASLR to prove causation:
run this
$ clang -g -O0 -DFORCE_CRASH 02_uninit_wild_ptr.c -o bin/02_force
$ for i in $(seq 1 8); do ./bin/02_force; echo -n "$? "; done; echo
what it printed on the machine that wrote this lab
139 139 139 139 139 139 139 139
run this
$ for i in $(seq 1 6); do setarch -R ./bin/02_uninit_wild_ptr; echo -n "$? "; done; echo
what it printed on the machine that wrote this lab
120 120 120 120 120 120
^ `setarch -R` disables ASLR: now the address is fixed, so the exit code
  is IDENTICAL every run. That proves the run-to-run variation was ASLR.
run this
$ gdb -q -batch -ex 'set debuginfod enabled off' \
what it printed on the machine that wrote this lab
      -ex 'break 132' -ex run -ex 'print p' \
      -ex 'print/x *(unsigned char*)p' -ex 'info locals' \
      ./bin/02_uninit_wild_ptr
Breakpoint 1, main () at 02_uninit_wild_ptr.c:132
132         unsigned char v = *(unsigned char *)p;  // <-- THE BUG
$1 = 0x7fffffffd718 "\030\327\377\377\377\177"
$2 = 0x18
p = 0x7fffffffd718 "\030\327\377\377\377\177"
v = 0 '\000'

Run that gdb line TWICE: `print p` shows the SAME address both times, because gdb sets `disable-randomization on` by default:

run this
$ gdb -q -batch -ex 'show disable-randomization' ./bin/02_uninit_wild_ptr
what it printed on the machine that wrote this lab
Disabling randomization of debuggee's virtual address space is on.
CONSEQUENCE: addresses are STABLE under gdb but DIFFER from a bare native
run (where ASLR is on). So "the address gdb shows" is not "the address
you crashed at natively". To reproduce the native layout under gdb:
    (gdb) set disable-randomization off
How to read the commands:
  break LINE       -> stop just before the deref so we can inspect p.
  print p          -> the wild pointer's value (a random stack address).
  print/x *(...)p  -> the byte we are about to return, in hex.
  info locals      -> shows p (and v, still garbage) in this frame.
run this
$ valgrind -q --track-origins=yes ./bin/02_uninit_wild_ptr
what it printed on the machine that wrote this lab
  ==56945== Use of uninitialised value of size 8
  ==56945==    at 0x400471: main (02_uninit_wild_ptr.c:132)
  ==56945==  Uninitialised value was created by a stack allocation
  ==56945==    at 0x4004A0: reader (02_uninit_wild_ptr.c:121)
  How to read it:
    "Use of uninitialised value of size 8" -> the 8-byte POINTER we deref
       is itself uninitialized. Note valgrind does NOT say "Invalid read":
       the ADDRESS is valid (mapped stack) — it only caught the bug through
       uninitialized-VALUE tracking. (META-LESSON #3 again, made concrete.)
    "Uninitialised value was created by a stack allocation at reader" ->
       --track-origins walked the value back to WHERE it was born. Without
       --track-origins you only learn THAT it's uninitialized, not where.

  On the forced build valgrind ALSO reports the bad address:
run this
$ valgrind -q ./bin/02_force
what it printed on the machine that wrote this lab
  ==56951== Use of uninitialised value of size 8
  ==56951==    at 0x40047F: main (02_uninit_wild_ptr.c:132)
  ==56951== Invalid read of size 1
  ==56951==    at 0x40047F: main (02_uninit_wild_ptr.c:132)
  ==56951==  Address 0x1ffffff708 is not stack'd, malloc'd or (recently) free'd

ROOT CAUSE vs CRASH SITE:
  The crash SITE (forced build) is the deref in main. The ROOT CAUSE is in
  reader(): `keep` is never initialized. valgrind's --track-origins points
  straight at reader() — the *birthplace* of the bad value — not at main
  where it was used. Trace the value backward to its origin, not forward to
  the crash.
category

UB / wild read (the headline meta-lesson bug)

difficulty

2 ------------------------------------------------------------

what's wrong

`reader()` returns a pointer variable `keep` that it never initialized. `main` dereferences it (`*(unsigned char*)p`). Reading an uninitialized variable is Undefined Behavior. Buggy line: the `unsigned char v = *(unsigned char*)p;`. WHY THIS PARTICULAR SHAPE (read this — it IS the lesson): A fresh program's stack pages are ZERO-FILLED by the kernel, so a naive `char *p; *p;` just reads 0 and behaves like a NULL deref (boring, deterministic). To show REAL uninitialized garbage I first "dirty" the stack: writer() leaves an ASLR-randomized address in a stack slot, then reader() — built with the SAME frame shape — reads that same slot WITHOUT initializing it. So `p` ends up holding a *real, valid, but random* stack address. That is the textbook wild pointer. SYMPTOM (this is the whole point — it is NONDETERMINISTIC): - Default build: it does NOT crash. `p` is a valid (mapped) stack address, so the read succeeds and the program's exit code is the byte it happened to read — which is a byte of an ASLR'd address, so it is DIFFERENT ALMOST EVERY RUN. (META-LESSON #3: a wild pointer that lands on valid memory passes silently — "no crash" != "no bug".) - Forced build (-DFORCE_CRASH): we push `p` 16 MiB past the stack top into an unmapped page, so it reliably SIGSEGVs (exit 139).

the fix

Initialize the pointer to something real before use: char *keep = &some_real_object; // in reader() or return a defined value. Never read an object before assigning it.

take it further

1) Run the default build under `gdb` with `set disable-randomization off` and confirm the printed `p` now changes between gdb runs. 2) Build with `-fsanitize=memory` (clang MSan) if available — it is purpose-built to catch exactly this "use of uninitialized value". ============================================================

read the source · debug/02_uninit_wild_ptr.c
/* (no headers needed — this bug is pure pointer plumbing) */ __attribute__((noinline)) static char *writer(void) { char *keep = (char *)&keep; /* leave an ASLR-randomized stack address... */ return keep; } __attribute__((noinline)) static char *reader(void) { char *keep; /* ROOT CAUSE: never initialized */ return keep; /* ...reads the slot writer() dirtied */ } int main(void) { writer(); char *p = reader(); /* p = a wild (uninitialized) pointer value */ #ifdef FORCE_CRASH p = p + (16 << 20); /* push 16 MiB past the stack -> unmapped */ #endif unsigned char v = *(unsigned char *)p; /* <-- THE BUG: deref wild pointer */ return v; }
◇
Finish the lab: write the evidence note

A lab is done when you can say what it showed on your machine — not when you have read it.