Date Mar 31, 2026

Meta Bug Bounty — Fuzzing "netconsd" for Fun and Profit — Part 3

The final installment: generating meaningful fuzz corpora using real kernel messages and symbolic execution with Klee, minimizing test cases, then running a full AFL++ fleet with 20+ instances using different coverage strategies and custom mutators.

netconsd fuzzing part 3

Why Corpus Generation Matters

A fuzzer is only as good as its starting seeds. Good initial test cases dramatically improve code coverage by guiding the fuzzer toward structurally valid inputs that exercise deeper code paths — rather than spending all cycles on trivially invalid inputs.

Method 1: Real Kernel Messages from /dev/kmsg

The simplest and most authentic source of test cases is your own machine's kernel log stream:

# Capture real kernel messages as initial corpus sudo cat /dev/kmsg | tee testfile.txt

Each line represents a real netconsole message in the exact format netconsd expects. Some lines were stored individually; others were grouped together to exercise the fragment reassembly code that was the key to finding the heap overflow.

Method 2: Symbolic Execution with Klee

Klee is a symbolic execution engine that explores program paths mathematically, generating inputs that exercise different branches. Using Klee requires modifying the harness to use a symbolic input buffer:

What is Symbolic Execution?

Instead of running programs with concrete values, symbolic execution treats inputs as symbolic variables and explores multiple code paths simultaneously — automatically generating test cases that satisfy different branch conditions.

#include "ncrx.h" #include <klee/klee.h> int main(int argc, char **argv) { unsigned char buf[2048]; int len = sizeof(buf); klee_make_symbolic(buf, len, "ncrx_input"); // mark as symbolic struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); uint64_t now = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; struct ncrx *ncrx = ncrx_create(NULL); myProcess(buf, now, 0, ncrx, len); ncrx_destroy(ncrx); }

Running Klee in Docker

# Use the official Klee Docker image cd netconsd_ncrx_fuzz sudo docker run -v .:/netconsd --rm -ti --ulimit='stack=-1:-1' klee/klee # Build with WLLVM to generate LLVM bitcode wllvm -O0 -Xclang -disable-O0-optnone -o fuzzme_klee main_klee.c libncrx.c \ -L/home/klee/klee_build/lib -lkleeRuntest -I. \ -I/home/klee/klee_build/include -D__NO_STRING_INLINES \ -D_FORTIFY_SOURCE=0 -U__OPTIMIZE__ # Extract LLVM bitcode extract-bc fuzzme_klee -o fuzzme_klee.bc # Run Klee (Ctrl+C when satisfied) klee --libc=uclibc --posix-runtime ./fuzzme_klee.bc # Extract generated test cases ktest-tool --extract ncrx_input klee-last/test000026.ktest

Optimizing the Corpus

Collecting thousands of test cases from both /dev/kmsg and Klee creates lots of redundancy. Two AFL++ tools minimize this before fuzzing:

  • afl-cmin: Removes inputs that cover the same code paths (deduplication)
  • afl-tmin: Shrinks each individual test case to its minimal crashing/covering form
# Deduplicate the entire corpus afl-cmin -i ./all -o ./input -- ./fuzz-asan # Minimize each remaining test case afl-tmin -i testcase -o testcase_min -- ./fuzz-asan

The Full Fuzzing Setup

The Makefile compiles the harness with multiple coverage strategies for maximum path exploration:

CC=afl-clang-lto CC1=afl-clang-fast all: asan cmplog compcov ctx ngram asan: AFL_USE_ASAN=1 AFL_USE_UBSAN=1 $(CC) -o fuzz-asan main.c libncrx.c -I. cmplog: AFL_LLVM_CMPLOG=1 $(CC) -o fuzz-cmplog main.c libncrx.c -I. compcov: AFL_LLVM_LAF_ALL=1 $(CC) -o fuzz-compcov main.c libncrx.c -I. ctx: AFL_LLVM_INSTRUMENT=CTX $(CC1) -o fuzz-ctx main.c libncrx.c -I. ngram: AFL_LLVM_INSTRUMENT=NGRAM-8 $(CC1) -o fuzz-ngram main.c libncrx.c -I.

Running 20+ AFL++ Instances in tmux

# run.sh — executed inside tmux tmux new-window 'AFL_AUTORESUME=1 afl-fuzz -i in/ -o out -D -M main -m none -- ./fuzz-asan' tmux new-window 'AFL_AUTORESUME=1 afl-fuzz -i in/ -o out -D -S s2 -m none -- ./fuzz-cmplog' tmux new-window 'AFL_AUTORESUME=1 afl-fuzz -i in/ -o out -D -S s3 -m none -- ./fuzz-compcov' tmux new-window 'AFL_AUTORESUME=1 afl-fuzz -i in/ -o out -D -S s4 -m none -- ./fuzz-ctx' tmux new-window 'AFL_AUTORESUME=1 afl-fuzz -i in/ -o out -D -S s5 -m none -- ./fuzz-ngram' # Custom mutators for text-format awareness: tmux new-window 'AFL_AUTORESUME=1 AFL_CUSTOM_MUTATOR_LIBRARY=.../autotokens.so afl-fuzz -i in/ -o out -S s11 -m none -- ./fuzz-asan' tmux new-window 'AFL_AUTORESUME=1 AFL_CUSTOM_MUTATOR_LIBRARY=.../radamsa-mutator.so afl-fuzz -i in/ -o out -S s12 -m none -- ./fuzz-asan' tmux new-window 'AFL_AUTORESUME=1 AFL_CUSTOM_MUTATOR_LIBRARY=.../libfuzzer-mutator.so afl-fuzz -i in/ -o out -S s13 -m none -- ./fuzz-asan'

Conclusion & Results

After running the full fleet for an extended period, AFL++ discovered the heap overflow in netconsd's fragment reassembly code. Coverage analysis showed near-100% block coverage of the parser code — meaning the fuzzer exercised virtually every reachable code path.

  • The bug was caused by improper bounds checking in ncfrag offset arithmetic when assembling message fragments
  • The crash was reproducible against the real netconsd daemon, confirming exploitability
  • The vulnerability was reported to Meta and fixed
  • The complete fuzz harness source is available at: github.com/fadyosman/netconsd_ncrx_fuzz

Build Your Security Research Skills with ZINAD

ZINAD's training programs cover binary exploitation, fuzzing methodologies, and vulnerability research — the same techniques used in this series. Our experts also provide hands-on application security assessments and red team engagements.

https://ZINAD.net/support-page.html

Written by Fady Othman, Co-founder and Director of R&D at ZINAD.