Fuzz a Vulnerable C Parser

Use libFuzzer and AddressSanitizer to find and fix a C parser overflow.

Introduction

30 Second Summary

Software can look reliable when it receives exactly the input its author expected. The real test begins when an unexpected input reaches a hidden limit.

In this project, you will build a small C parser for structured label records. You will use libFuzzer with AddressSanitizer to take a hidden stack buffer overflow from discovery to a verified fix.

What You'll Build

You'll demo a parser that prints admin for a valid record, rejects a 16-byte label, and survives the exact input that once crashed it.

By the end of this project, you'll have:

  • A runnable parser that prints admin from LBL:admin.
  • A reproducible crash artifact stored at artifacts/crash-input for replaying the same stack overflow on demand.
  • A hardened parser that rejects a 16-byte label. A regression fuzzing run that finishes without an AddressSanitizer report.
  • Secret Mission: Use a libFuzzer dictionary to reach the structured LBL: input path without a seed file.

Are there any prerequisites?

You need Ubuntu or Debian Linux with permission to run sudo. Basic familiarity with reading C code helps, but no prior fuzzing experience is required.

Before We Start

Before the hands-on work begins, lock in the purpose of this controlled local vulnerability-research exercise. You are building a fuzzer-driven C parser lab where reproducing a crash preserves evidence before you fix the root cause.

Install the Instrumented Toolchain

A normal C compiler run can let invalid memory writes pass unnoticed. AddressSanitizer makes those failures visible when the instrumented program runs.

This step gives you Clang 22.1.8 plus its matching libFuzzer runtime. A minimal fuzz entry point proves the compiler can link the sanitizer runtime before you create any parser files.

In this step, get ready to:
  • Install the version-matched compiler toolchain from the official package source.
  • Verify the toolchain with a minimal fuzz target.
  • Create the lab workspace in Visual Studio Code.
Check your Clang version

APT manages the compiler packages on Ubuntu or Debian. GNU Wget retrieves the official installer that configures the apt.llvm.org package source.

  • Check your current Clang 22 state by running this command:
clang-22 --version

What does this command check?

The command asks the versioned Clang executable to report its installed release. Your result determines which installation path you use below.

These installation commands change system packages. Your local account may request authorization before APT continues.

✔️ I see Clang 22.1.8

Your clang-22 command already resolves to Clang 22.1.8. Refresh the official source before installing the matching runtime packages.

  • Install the LLVM installer prerequisites by running these commands:
sudo apt-get update
sudo apt-get install -y lsb-release wget software-properties-common gnupg

What do these prerequisites do?

  • The first command refreshes APT's package index.
  • The second command installs the distribution detection tools used by the installer.
  • It also installs the download tools required for the signed package source.

Expect the next installation to take a few minutes while the compiler packages download.

  • Configure the official LLVM 22 source by running these commands:
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 22
sudo apt-get install libclang-rt-22-dev
sudo apt-get install libfuzzer-22-dev

What does the installer add?

  • The wget command downloads the official LLVM installer.
  • The sudo ./llvm.sh 22 command configures the version 22 package source.
  • The libclang-rt-22-dev package supplies the matching AddressSanitizer runtime.
  • The libfuzzer-22-dev package supplies the matching fuzzing runtime.

The terminal returns to its prompt after every selected package operation finishes.

ⓧ I see an older version

Your current result does not match the pinned Clang 22.1.8 toolchain. Use the version 22 installer path below.

  • Install the LLVM installer prerequisites by running these commands:
sudo apt-get update
sudo apt-get install -y lsb-release wget software-properties-common gnupg

What do these prerequisites do?

  • The first command refreshes APT's package index.
  • The second command installs the distribution detection tools used by the installer.
  • It also installs the download tools required for the signed package source.

Expect the next installation to take a few minutes while the compiler packages download.

  • Install Clang 22 with its matching runtimes by running these commands:
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 22
sudo apt-get install libclang-rt-22-dev
sudo apt-get install libfuzzer-22-dev

What does the installer add?

  • The wget command downloads the official LLVM installer.
  • The sudo ./llvm.sh 22 command configures the version 22 package source.
  • The libclang-rt-22-dev package supplies the matching AddressSanitizer runtime.
  • The libfuzzer-22-dev package supplies the matching fuzzing runtime.

The terminal returns to its prompt after every selected package operation finishes.

ⓧ Command not found

Clang 22 is not installed yet. The official installer adds the compiler before APT installs both matching runtime packages.

  • Install the LLVM installer prerequisites by running these commands:
sudo apt-get update
sudo apt-get install -y lsb-release wget software-properties-common gnupg

What do these prerequisites do?

  • The first command refreshes APT's package index.
  • The second command installs the distribution detection tools used by the installer.
  • It also installs the download tools required for the signed package source.

Expect the next installation to take a few minutes while the compiler packages download.

  • Install Clang 22 with its matching runtimes by running these commands:
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 22
sudo apt-get install libclang-rt-22-dev
sudo apt-get install libfuzzer-22-dev

What does the installer add?

  • The wget command downloads the official LLVM installer.
  • The sudo ./llvm.sh 22 command configures the version 22 package source.
  • The libclang-rt-22-dev package supplies the matching AddressSanitizer runtime.
  • The libfuzzer-22-dev package supplies the matching fuzzing runtime.

The terminal returns to its prompt after every selected package operation finishes.

Toolchain installation stopped?

  • Confirm your machine is running Ubuntu or Debian.
  • Restore your internet connection if a package download stops.
  • Repeat the command blocks in your selected tab after resolving the failed download.

Help me troubleshoot the LLVM 22 installation.

Compile the toolchain check

A libFuzzer target exposes a function named LLVMFuzzerTestOneInput. The fuzzing engine passes a byte pointer plus its size into that function.

This temporary target ignores those bytes because its only job is to test the linker. A successful build proves Clang can combine libFuzzer with AddressSanitizer.

Before you run this check, do you expect the compiler to link both instrumentation runtimes into one executable?

  • Create the temporary fuzz entry point and test the toolchain by running these commands:
clang-22 --version
printf '#include <stddef.h>\n#include <stdint.h>\nint LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { (void)data; (void)size; return 0; }\n' > toolcheck.c
clang-22 -fsanitize=fuzzer,address toolcheck.c -o toolcheck
test -x toolcheck && echo 'fuzzer toolchain ready'

What does this check prove?

  • The first command confirms which Clang 22 release is active.
  • The printf command writes a minimal valid fuzz entry point to toolcheck.c.
  • The sanitizer build flag links the fuzzing engine with memory-error detection.
  • The final test prints its message only when toolcheck exists as an executable.

That is the toolchain hurdle cleared. You will see the Clang version followed by fuzzer toolchain ready with no linker failure.

Missing the ready message?

  • Check that your selected installation tab completed every package command.
  • Confirm the compiler command includes -fsanitize=fuzzer,address exactly as shown.
  • Repeat the toolchain check after the missing package finishes installing.

Help me fix the toolchain check.

Create and open the lab workspace

The temporary target has served its purpose. Removing it leaves ~/c-fuzz-lab ready for the parser files you create next.

  • Remove the temporary files and create the lab workspace by running these commands:
rm toolcheck toolcheck.c
mkdir -p ~/c-fuzz-lab

What do these commands leave behind?

The first command removes the temporary source file plus its executable. The second command creates ~/c-fuzz-lab if the folder does not already exist.

Before you check the workspace, which folder name do you expect to see in the Explorer sidebar?

  • Switch back to Visual Studio Code.
  • Select File from the top menu.
  • Select Open Folder....
  • Choose the c-fuzz-lab folder inside your home folder.
  • Select Select Folder.
  • Select Yes, I trust the authors if the Workspace Trust dialog appears.

You will see c-fuzz-lab at the top of the Explorer sidebar. The empty workspace confirms the temporary check files are gone.

Can't find the workspace?

  • Return to your home folder in the folder picker.
  • Look for c-fuzz-lab without a leading dot.
  • Repeat the workspace creation command if the folder is absent.

Help me open the lab workspace.

Your instrumented C toolchain can now compile fuzz targets with memory-error detection. Next up, you will build the parser whose normal input hides an unsafe boundary.

Build the Vulnerable Parser

Your instrumented toolchain is ready. The next layer is a small C parser that accepts records beginning with LBL:.

A normal record can make an unchecked memory write look safe. This step establishes that misleading happy path under AddressSanitizer.

In this step, get ready to:
  • Define the parser's input size and output size contract.
  • Implement a parser with an unchecked label copy.
  • Compile the demo with AddressSanitizer and parse a valid record.
Define the parser contract

A parser needs to know how many bytes it can read. It also needs the capacity of the destination that receives its result.

  • Create parser.h inside c-fuzz-lab using the file control in the Visual Studio Code Explorer sidebar.
  • Add the parser interface to parser.h by pasting this code:
#ifndef PARSER_H
#define PARSER_H

#include <stddef.h>
#include <stdint.h>

enum {
    LABEL_PREFIX_SIZE = 4,
    LABEL_CAPACITY = 16
};

int parse_label(const uint8_t *data, size_t size, char *out, size_t out_size);

#endif

What does this interface define?

  • The LABEL_PREFIX_SIZE constant records the four bytes in LBL:.
  • The LABEL_CAPACITY constant sets the local label buffer to 16 bytes.
  • The parse_label() interface receives a source pointer with its size.
  • The same interface receives a destination pointer with its capacity.
  • Save parser.h.
  • Confirm that parser.h appears inside c-fuzz-lab in the Explorer sidebar.

Seeing a header error later?

Check that the include guard uses PARSER_H in all three locations. Confirm that both size parameters use size_t.

Compare every declaration with the reference tab at the end of this step.

Help me check my parser header.

Implement the vulnerable parser

The implementation keeps a label in a 16-byte local array. This first version validates the record shape without checking whether the payload fits that array.

The implementation is split into two short chunks. Together they form one complete parse_label() function.

  • Create parser.c inside c-fuzz-lab using the file control in the Explorer sidebar.
  • Start the parser implementation in parser.c by pasting this code:
#include "parser.h"

int parse_label(const uint8_t *data, size_t size, char *out, size_t out_size) {
    char label[LABEL_CAPACITY];
    size_t label_size;
    size_t index;

    if (!data || !out || out_size == 0) {
        return -1;
    }

    if (size < LABEL_PREFIX_SIZE) {
        return -1;
    }

    if (data[0] != (uint8_t)'L' ||
        data[1] != (uint8_t)'B' ||
        data[2] != (uint8_t)'L' ||
        data[3] != (uint8_t)':') {
        return -1;
    }

    label_size = size - LABEL_PREFIX_SIZE;

What does this code check?

  • The label array reserves 16 bytes on the stack.
  • The first condition rejects missing pointers or an unusable output buffer.
  • The second condition rejects inputs shorter than the required prefix.
  • The prefix condition accepts only inputs beginning with LBL:.
  • The label_size calculation measures every byte after that prefix.
  • Complete the same parse_label() function by pasting this code directly below the label_size assignment:

    for (index = 0; index < label_size; index++) {
        label[index] = (char)data[LABEL_PREFIX_SIZE + index];
    }
    label[label_size] = '\0';

    for (index = 0; index <= label_size; index++) {
        out[index] = label[index];
    }

    return 0;
}

Where is the hidden risk?

The first loop copies the payload into label. The next write adds the null terminator required by a C string.

This version has no label_size bounds guard before those writes. A short payload fits cleanly while a longer payload can cross the local array boundary.

The final loop copies the completed string into the caller's output buffer.

  • Save parser.c.
  • Confirm that the file ends with return 0; followed by the function's closing brace.

Seeing incomplete code?

Make sure the second chunk sits inside parse_label(). It belongs directly below the label_size assignment.

Check that each loop uses the existing index variable. Confirm that the null terminator is written as '\0'.

Help me assemble the parser function.

Run the valid record

The demo supplies one well-formed record whose five-byte label fits inside the local array. A successful result shows that the parser's happy path behaves normally.

  • Create demo.c inside c-fuzz-lab using the file control in the Explorer sidebar.
  • Add the valid-record demo to demo.c by pasting this code:
#include "parser.h"

#include <stdint.h>
#include <stdio.h>

int main(void) {
    static const uint8_t valid[] = "LBL:admin";
    char output[LABEL_CAPACITY];

    if (parse_label(valid, sizeof(valid) - 1, output, sizeof(output)) != 0) {
        puts("valid label rejected");
        return 1;
    }
    puts(output);

    return 0;
}

What does this demo prove?

  • The valid byte array contains the required prefix followed by admin.
  • The input size excludes the string's automatic null terminator.
  • The output array uses the same 16-byte capacity as the parser's local array.
  • The final puts() call prints the parsed label after a successful return.
  • Save demo.c.

Before you run the demo, what do you expect a short valid label to print?

  • Build the AddressSanitizer-instrumented demo and run it from the Visual Studio Code terminal by entering:
clang-22 -g -O1 -fsanitize=address -fno-omit-frame-pointer parser.c demo.c -o demo
./demo

What do these commands do?

The first command compiles parser.c with demo.c. The sanitizer instrumentation monitors the resulting executable for invalid memory access.

The second command runs the new demo executable.

You should see admin with no AddressSanitizer report. That clean result confirms the happy path while leaving the unchecked payload length in place.

Demo does not print admin?

Confirm that the terminal is still inside ~/c-fuzz-lab. Check that all three source files appear in the Visual Studio Code Explorer sidebar.

If compilation fails near parse_label(), compare the function braces with the full reference below.

Help me compile the parser demo.

✔️ Awesome, I've got everything!

Great work. Your vulnerable parser now handles the valid LBL:admin record under AddressSanitizer.

ⓧ I'd like to double check the full code

parser.h
#ifndef PARSER_H
#define PARSER_H

#include <stddef.h>
#include <stdint.h>

enum {
    LABEL_PREFIX_SIZE = 4,
    LABEL_CAPACITY = 16
};

int parse_label(const uint8_t *data, size_t size, char *out, size_t out_size);

#endif
parser.c
#include "parser.h"

int parse_label(const uint8_t *data, size_t size, char *out, size_t out_size) {
    char label[LABEL_CAPACITY];
    size_t label_size;
    size_t index;

    if (!data || !out || out_size == 0) {
        return -1;
    }

    if (size < LABEL_PREFIX_SIZE) {
        return -1;
    }

    if (data[0] != (uint8_t)'L' ||
        data[1] != (uint8_t)'B' ||
        data[2] != (uint8_t)'L' ||
        data[3] != (uint8_t)':') {
        return -1;
    }

    label_size = size - LABEL_PREFIX_SIZE;

    for (index = 0; index < label_size; index++) {
        label[index] = (char)data[LABEL_PREFIX_SIZE + index];
    }
    label[label_size] = '\0';

    for (index = 0; index <= label_size; index++) {
        out[index] = label[index];
    }

    return 0;
}
demo.c
#include "parser.h"

#include <stdint.h>
#include <stdio.h>

int main(void) {
    static const uint8_t valid[] = "LBL:admin";
    char output[LABEL_CAPACITY];

    if (parse_label(valid, sizeof(valid) - 1, output, sizeof(output)) != 0) {
        puts("valid label rejected");
        return 1;
    }
    puts(output);

    return 0;
}

Your parser now looks healthy when it receives a short label. Next, you will let generated inputs probe the unchecked length and preserve the failure they uncover.

Reproduce the Overflow

Your instrumented demo now parses LBL:admin. It prints admin without an AddressSanitizer report.

That success concealed an unchecked stack write inside parse_label(). A libFuzzer harness explores label lengths that the happy-path demo never reached.

In this step, get ready to:
  • Create a fuzz target that passes arbitrary byte arrays to parse_label().
  • Compile the vulnerable parser with sanitizer instrumentation.
  • Run a bounded fuzzing campaign that preserves the crashing input.
Create the fuzz target

A fuzz target is a narrow entry point that accepts generated bytes. This harness sends every input to the parser without printing output or stopping the campaign.

  • Create fuzz_parser.c inside c-fuzz-lab from the file list in Visual Studio Code.
  • Define the harness in fuzz_parser.c by adding this code:
#include "parser.h"

#include <stddef.h>
#include <stdint.h>

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    char output[LABEL_CAPACITY];

    (void)parse_label(data, size, output, sizeof(output));
    return 0;
}

What does this harness do?

  • The LLVMFuzzerTestOneInput() entry point receives one generated byte array per test.
  • The output array gives parse_label() the same 16-byte destination used by the demo.
  • The discarded return value keeps invalid records from ending the fuzzing campaign.
  • Save fuzz_parser.c.
  • Confirm fuzz_parser.c appears alongside parser.c in the Visual Studio Code file list.

Good progress. Your harness now routes every generated byte array through the vulnerable parser.

Seeing editor errors in the harness?

  • Check that the first line includes parser.h from the same c-fuzz-lab folder.
  • Compare the parameter types with const uint8_t *data plus size_t size.

Help me check my fuzz target.

✔️ Awesome, I've got everything!

  • Confirm fuzz_parser.c is saved before continuing.

ⓧ I'd like to double check the full code

#include "parser.h"

#include <stddef.h>
#include <stdint.h>

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    char output[LABEL_CAPACITY];

    (void)parse_label(data, size, output, sizeof(output));
    return 0;
}

What should match?

Compare the headers plus the complete LLVMFuzzerTestOneInput() function with your saved file. The function should call parse_label() once for every generated input.

Prepare the fuzzing campaign

A seed corpus gives libFuzzer a valid starting record. Mutations can then focus on the label bytes beyond the required LBL: prefix.

  • Prepare the vulnerable campaign by running these commands in the terminal from earlier:
clang-22 -g -O1 -fsanitize=fuzzer,address -fno-omit-frame-pointer parser.c fuzz_parser.c -o fuzz_parser_vulnerable
mkdir -p corpus artifacts
printf 'LBL:admin' > corpus/seed

What do these commands prepare?

  • The clang-22 command builds fuzz_parser_vulnerable with libFuzzer plus AddressSanitizer instrumentation.
  • The corpus directory stores useful inputs discovered during fuzzing.
  • The artifacts directory stores the input that triggers a failure.
  • The corpus/seed file begins the campaign with the valid record LBL:admin.
  • Select corpus/seed from the Visual Studio Code file list.
  • Confirm its contents read LBL:admin.

The vulnerable binary now has a structured starting input plus a dedicated location for the failure evidence.

Did the vulnerable target fail to build?

  • Check that your terminal remains inside ~/c-fuzz-lab.
  • Confirm parser.c plus fuzz_parser.c appear in the Visual Studio Code file list.
  • Check that the compiler command uses -fsanitize=fuzzer,address.

Help me build the vulnerable fuzz target.

Run the fuzzer and capture the crash

The seed starts inside the parser's structured input path. libFuzzer can now extend the label while AddressSanitizer watches every memory access.

The campaign can run for up to 20 seconds. A quiet moment during mutation is normal.

Before you run the campaign, consider whether the clean result will hold as labels grow toward 32 bytes.

  • Start the bounded campaign by running this command:
./fuzz_parser_vulnerable -max_len=32 -len_control=0 -max_total_time=20 -exact_artifact_path=artifacts/crash-input corpus

What did the fuzzer expose?

You'll see output containing ERROR: AddressSanitizer: stack-buffer-overflow. This failure is the intended shortfall.

The report names a stack buffer overflow caused by writing beyond label. AddressSanitizer stops at the first detected error.

libFuzzer stores the failing bytes as a crash artifact at artifacts/crash-input.

  • Scroll upward in the terminal to the AddressSanitizer summary.
  • Locate the first stack frame that names parse_label.
  • Confirm artifacts/crash-input appears in the Visual Studio Code file list.

The saved file preserves an LBL: record with a label at least 16 bytes long. Those bytes now make the vulnerable boundary deterministic.

Did the campaign finish without a crash?

  • Check that corpus/seed contains exactly LBL:admin.
  • Check that the campaign command includes -len_control=0.
  • Confirm parser.c still copies into label without a label-size guard.

Help me reproduce the parser overflow.

That hidden bug is now concrete. Next, you'll use the saved bytes to verify a local boundary fix.

Patch and Replay the Crash

Your libFuzzer run preserved the bytes that trigger a stack buffer overflow. AddressSanitizer now gives you a repeatable failure you can inspect.

A crash report remains a finding until you can replay its input. In this step, you'll fix the violated boundary where untrusted bytes enter the local array.

In this step, get ready to:
  • Trace the failed write with the saved crash artifact.
  • Add a boundary guard before the copy loops.
  • Prove the repair against the exact crashing bytes.
Trace the failed write

A saved crash artifact is a deterministic regression input. Replaying it through the retained vulnerable binary ties the sanitizer report to those exact bytes.

  • Replay the artifact with the retained vulnerable target by running this command:

Before you run this, consider whether the retained vulnerable binary will still stop on the saved bytes.

./fuzz_parser_vulnerable artifacts/crash-input

Why Does This Still Crash?

The retained binary still contains the unchecked copy loop. Replaying the artifact deliberately reaches the same invalid stack write.

The sanitizer stops the program when that write crosses the local array boundary. This failure confirms the artifact reproduces the original finding.

You'll see an AddressSanitizer report naming a stack buffer overflow. The command exits after the first detected memory violation.

  • Locate the first stack frame named parse_label in the report.
  • Match the reported source location to the write into label[index].

Where Does the Write Cross the Boundary?

The vulnerable parser calculates label_size from untrusted input. It enters the copy loop without comparing that size with char label[LABEL_CAPACITY].

A 16-byte label fills all 16 available slots. The null terminator needs one additional slot, so the next write crosses the stack buffer boundary.

Crash Not Reproduced?

Confirm the command uses fuzz_parser_vulnerable. The repaired target does not exist yet.

Confirm artifacts/crash-input remains inside ~/c-fuzz-lab.

Help me reproduce the saved parser crash.

Guard the local buffer

The parser must validate both destination capacities before copying any bytes. Rejecting a 16-byte local label also reserves space for its terminator.

  • In parser.c, locate this line:
label_size = size - LABEL_PREFIX_SIZE;

Why This Line Matters

This assignment measures every byte after the LBL: prefix. The resulting length controls both copy loops.

  • Insert the boundary guard immediately below that line:
label_size = size - LABEL_PREFIX_SIZE;

if (label_size >= sizeof(label) || label_size + 1 > out_size) {
    return -1;
}

What Does the Guard Protect?

  • The label_size >= sizeof(label) condition rejects labels that fill or exceed the local array.
  • The label_size + 1 > out_size condition protects the caller's output buffer.
  • Returning before the loops prevents every rejected payload from reaching a memory write.
  • Save parser.c.

✔️ Awesome, I've got everything!

The guard is in place. Both copy loops now run only when the label and its terminator fit inside each destination.

ⓧ I'd like to double check the full code

#include "parser.h"

int parse_label(const uint8_t *data, size_t size, char *out, size_t out_size) {
    char label[LABEL_CAPACITY];
    size_t label_size;
    size_t index;

    if (!data || !out || out_size == 0) {
        return -1;
    }

    if (size < LABEL_PREFIX_SIZE) {
        return -1;
    }

    if (data[0] != (uint8_t)'L' ||
        data[1] != (uint8_t)'B' ||
        data[2] != (uint8_t)'L' ||
        data[3] != (uint8_t)':') {
        return -1;
    }

    label_size = size - LABEL_PREFIX_SIZE;

    if (label_size >= sizeof(label) || label_size + 1 > out_size) {
        return -1;
    }

    for (index = 0; index < label_size; index++) {
        label[index] = (char)data[LABEL_PREFIX_SIZE + index];
    }
    label[label_size] = '\0';

    for (index = 0; index <= label_size; index++) {
        out[index] = label[index];
    }

    return 0;
}
Prove the boundary holds

The saved artifact now becomes a regression case for the repaired parser. Running the same bytes through a newly compiled target tests the fix without changing the input.

  • Build the repaired fuzz target by running this command:
clang-22 -g -O1 -fsanitize=fuzzer,address -fno-omit-frame-pointer parser.c fuzz_parser.c -o fuzz_parser

What Does This Build Do?

  • The command asks Clang to compile the repaired parser with its fuzz target.
  • The -fsanitize=fuzzer,address option links libFuzzer while preserving AddressSanitizer checks.
  • The output is a new executable named fuzz_parser.
  • The retained fuzz_parser_vulnerable executable remains available for comparison.

The compiler returns to the prompt without diagnostics. You now have a repaired fuzz_parser executable.

Build Did Not Complete?

Confirm you saved parser.c after inserting the complete guard. Missing braces can stop compilation.

Confirm the terminal remains inside ~/c-fuzz-lab, where parser.c and fuzz_parser.c already exist.

Help me compile the repaired fuzz target.

Before you replay the artifact, consider whether the exact bytes can still reach either copy loop.

  • Replay the saved artifact with the repaired target by running this command:
./fuzz_parser artifacts/crash-input

What Did the Replay Prove?

Passing a file path makes libFuzzer run that file as one test input. The command does not start a new fuzzing campaign.

The repaired parser checks the label length before either copy loop. The saved input now returns cleanly without an AddressSanitizer report.

Strong result. The exact bytes that crashed the vulnerable binary are now rejected before any invalid write occurs.

The artifact remains at artifacts/crash-input for future regression runs.

Still Seeing a Sanitizer Report?

Confirm the replay command names fuzz_parser. The retained fuzz_parser_vulnerable binary should still reproduce the crash.

Compare the guard in parser.c with the full-code reference. It must appear before the first copy loop.

Help me debug the repaired parser replay.

Your saved crash is now a passing regression case. Next, you'll prove normal input still works before a fresh fuzzing campaign searches for other failures.

Run Regression Fuzzing

Your repaired parser now rejects the saved crash input without an AddressSanitizer report. That replay proves the boundary check handles one known failure.

A single replay covers only one input. You will confirm both visible parser outcomes before a new libFuzzer campaign explores other mutations.

In this step, get ready to:
  • Add an oversized record to demo.c as a visible regression case.
  • Prove both parser outcomes with the AddressSanitizer demo.
  • Run a bounded libFuzzer campaign against the accumulated corpus.
Add an oversized regression case

The demo.c file already proves that LBL:admin is accepted. The regression case adds a label that fills LABEL_CAPACITY and must be rejected to preserve null-terminator space.

  • Switch back to demo.c in Visual Studio Code.
  • Locate the declarations at the start of main().
  • Insert the oversized declaration so that the section matches:
    static const uint8_t valid[] = "LBL:admin";
    static const uint8_t oversized[] = "LBL:0123456789abcdef";
    char output[LABEL_CAPACITY];

What does this input test?

The text after LBL: fills all LABEL_CAPACITY bytes. A safe parser rejects it so the local array still has room for \0.

  • Find the puts(output) call in main().
  • Insert this rejection check directly below it:
    if (parse_label(oversized, sizeof(oversized) - 1, output, sizeof(output)) == 0) {
        puts("oversized label accepted");
        return 1;
    }
    puts("oversized label rejected");

How does the regression check work?

  • The second parse_label() call sends the full-capacity label through the repaired boundary check.
  • A zero return means the parser accepted the oversized record. The demo exits with 1 to expose that regression.
  • A rejected record reaches puts("oversized label rejected") and keeps the demo successful.
  • Save demo.c.

✔️ Awesome, I've got everything!

Your demo.c now checks the accepted path plus the full-capacity rejection path.

ⓧ I'd like to double check the full code

#include "parser.h"

#include <stdint.h>
#include <stdio.h>

int main(void) {
    static const uint8_t valid[] = "LBL:admin";
    static const uint8_t oversized[] = "LBL:0123456789abcdef";
    char output[LABEL_CAPACITY];

    if (parse_label(valid, sizeof(valid) - 1, output, sizeof(output)) != 0) {
        puts("valid label rejected");
        return 1;
    }
    puts(output);

    if (parse_label(oversized, sizeof(oversized) - 1, output, sizeof(output)) == 0) {
        puts("oversized label accepted");
        return 1;
    }
    puts("oversized label rejected");

    return 0;
}
Check visible parser behavior

The demo turns the parser contract into two visible outcomes. Valid input must still work after the security fix.

Before you compile the demo, predict which two messages confirm that both sides of the boundary behave correctly.

  • Build the updated demo and execute it by running these commands:
clang-22 -g -O1 -fsanitize=address -fno-omit-frame-pointer parser.c demo.c -o demo
./demo

What does this check prove?

  • The -fsanitize=address flag instruments memory accesses while the demo runs.
  • The ./demo command tests the valid record before testing the oversized record.

You will see admin first. You will see oversized label rejected on the next line.

That closes the visible regression check: normal input still works. The 16-byte label is rejected before either copy loop.

Demo output looks different?

  • Confirm that you saved the updated demo.c before compiling.
  • Check that parser.c still contains the label_size guard before both copy loops.

Help me diagnose my demo output.

Fuzz the repaired parser

The existing corpus gives regression fuzzing a valid structured starting point. libFuzzer mutates those inputs through the repaired parse_label() path.

Expect this campaign to run for up to 20 seconds. Status lines in the terminal show that libFuzzer is still working.

Before you run this check, predict whether the repaired target finds another sanitizer failure or reaches its time limit cleanly.

  • Run the bounded regression campaign against corpus by running:
./fuzz_parser -max_len=32 -len_control=0 -max_total_time=20 corpus

What does this campaign test?

  • The -max_len=32 option allows generated inputs up to 32 bytes.
  • The -len_control=0 option immediately explores lengths up to that limit.
  • The -max_total_time=20 option bounds the campaign at 20 seconds.
  • The corpus directory supplies the starting inputs for new mutations.

You will see DONE when the time limit is reached. No AddressSanitizer report should appear.

You have completed the crash-to-fix loop. The repaired parser preserves valid behavior while surviving continued mutation.

Fuzzing does not finish cleanly?

  • Confirm that you ran fuzz_parser instead of the retained fuzz_parser_vulnerable binary.
  • Return to ~/c-fuzz-lab if the campaign cannot find the corpus directory.
  • Confirm that corpus/seed still contains LBL:admin.

Help me troubleshoot the regression fuzzing run.

Secret mission

Reach Structured Inputs with a Dictionary

A seed file gave the fuzzer a direct route into the structured parser path. This time, you will remove that clue and use a dictionary token to rediscover the overflow from an empty corpus. The repaired parser will then prove that the new artifact is harmless.

Clean Up Your Resources

Clean Up Your Resources

This local lab has no ongoing charges or cloud resources. Choose whether to keep the workspace, pause your work, or delete it.

Resources you used:

  • Local workspace at ~/c-fuzz-lab, including the C source files and header.
  • Seed and generated libFuzzer corpora in corpus and fresh-corpus.
  • Local executables named demo, fuzz_parser, and fuzz_parser_vulnerable.
  • Reproducible crash artifacts at artifacts/crash-input and artifacts/dictionary-crash-input.
  • Dictionary file at labels.dict.

Why the vulnerable target stays local

The fuzz_parser_vulnerable executable intentionally contains the stack buffer overflow from the lab. It belongs in this controlled local workspace and should never receive untrusted production data.

Keep everything running

No action is needed. Choose this option if you want to keep experimenting with parser boundaries or new fuzzing inputs.

  • Keep the ~/c-fuzz-lab workspace in its current location.
  • Preserve both files in artifacts as regression inputs for the repaired parser.
  • Keep fuzz_parser_vulnerable inside the local lab for controlled comparisons only.
  • Leave the Clang toolchain installed for future C or C++ security work.

Pause - I'll come back to this later

The bounded libFuzzer runs have already exited. No background fuzzer is consuming processor time while the project is paused.

  • Save any open files in the ~/c-fuzz-lab workspace.
  • Close the ~/c-fuzz-lab workspace in Visual Studio Code.
  • Keep the workspace folder in your home folder for your next fuzzing session.

Delete - I don't want to use this again

Deleting the workspace is permanent. Every crash artifact inside it disappears with the source files.

  • Copy artifacts/crash-input elsewhere if you want to keep the seed-assisted crash input.
  • Copy artifacts/dictionary-crash-input elsewhere if you want to keep the dictionary-assisted crash input.
  • Move to your home folder and remove ~/c-fuzz-lab by running these commands:
cd ~
rm -rf ~/c-fuzz-lab

What does this cleanup remove?

The command deletes the entire ~/c-fuzz-lab folder. This includes the sources, corpora, executables, dictionary, and both crash artifacts.

Your Clang toolchain and apt.llvm.org package source remain installed. They live outside this workspace.

  • View your home folder with your operating system's file browser.

You should no longer see the c-fuzz-lab folder.

Nice Work!

Nice Work!

You did it! You built a C parser lab that uses libFuzzer to expose a stack buffer overflow. Your saved crash artifact now proves the repaired parser rejects the same dangerous input under AddressSanitizer.

You've learned how to:

  • Defined a bounded parser contract for structured byte input. Passed source size plus destination capacity into parse_label.
  • Built a focused libFuzzer harness for arbitrary byte arrays. Preserved the failing bytes in artifacts/crash-input for deterministic replay.
  • Added local bounds validation before the parser writes to its fixed-size label array. Confirmed valid labels still work during a fresh fuzzing regression run.
  • Secret Mission: Used a libFuzzer dictionary to reach the structured LBL: path from an empty corpus. Replayed artifacts/dictionary-crash-input against the repaired target without an AddressSanitizer report.

Ready to quiz yourself?