Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ absolution/
│ └── <test_name>/
│ ├── <file>.c # Test input
│ ├── <file>.c.in # Optional input invariant
│ ├── <file>.c.offsets # Optional golden input layout
│ └── <file>.c.zon # Golden output
├── scripts/
│ ├── integration.zig # Integration test runner (zig run scripts/integration.zig)
Expand Down Expand Up @@ -82,6 +83,8 @@ C code emission:
- `emitSampler`: Generates `sample_invariant()` function
- `emitChecker`: Generates `check_invariant()` function
- `emitEntrypoint`: Generates `LLVMFuzzerTestOneInput()`
- `computeInputLayout`: Maps each global to its byte offset in the fuzzer input,
exported as C constants and a sidecar file when `--offsets` is given

## Development Workflow

Expand Down Expand Up @@ -130,6 +133,10 @@ zig run scripts/integration.zig -- foo # Run only tests matching "foo"
zig run scripts/integration.zig
```

To also pin the input layout, add a `<file>.c.offsets` golden. The runner always
requests offsets but only compares that file where it exists; `gen-golden.sh`
refreshes it under the same rule.

## Architecture Notes

### Parsing pipeline
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Absolution lets you specify an invariant for a program’s global state and fuzz
4. Emit `fuzzer.c` with sampling, invariant checking, and libFuzzer entrypoint.
5. Emit a symbol redefinition file for `objcopy` (handles `static` globals across translation units).
6. Write an optional seed file sized to the required random bytes.
7. Optionally export the input layout — the byte offset of every sampled global — as constants in the generated C and as a sidecar file (see [USAGE.md](USAGE.md)).

## Requirements

Expand All @@ -34,6 +35,7 @@ zig build -Doptimize=ReleaseFast
--out fuzzer.c \
--redef fuzzer.redef \
--seed fuzzer.seed \
--offsets fuzzer.offsets \
-- -I path/to/include -DMY_DEFINE=42

# Compile targets, apply objcopy, link, and run
Expand Down
57 changes: 57 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ OPTIONS:
-i, --invariant <str> Optional invariant file (.zon).
-z, --zon <str> Optional: export parsed module to .zon format.
-s, --seed <str> Optional seed file path (default: fuzzer.seed).
-m, --offsets <str> Optional input layout output path; also exports the
layout as constants in the generated C.
-e, --entry <str> Optional harness function name (default: AbsolutionTestOneInput).

<str>... C compiler flags after '--' (e.g. -I path -DFOO -fshort-enums).
Expand Down Expand Up @@ -138,6 +140,61 @@ mkdir -p corpus && cp fuzzer.seed corpus/
For CMake projects, `absolution_add_fuzzer()` handles all of this automatically.
See the [example/protocol_parser/](example/protocol_parser/) directory.

## Exporting the Input Layout

The generated sampler reads the fuzzer input from front to back, giving every
global a fixed byte offset. `--offsets` publishes those offsets so a harness or
a corpus tool can address a specific global without reading the generated
source.

Two exports are produced together:

**Constants in the generated C**, so code linked against the fuzzer can read the
layout it was actually built with:

```c
const size_t absolution_globals_size = 27; /* total sampled prefix */
const size_t absolution_offset_input_value = 0; /* per global */
const size_t absolution_offset_handlers = 4;
```

Declare what you need and let the linker supply the value; keep the declaration
weak if the harness must also build without absolution:

```c
extern const size_t absolution_globals_size __attribute__((weak));
extern const size_t absolution_offset_input_value __attribute__((weak));
```

**A sidecar file** at the requested path, for build scripts and corpus
generators:

```
# absolution fuzzer input layout
# global <offset> <bytes> <static:0|1> <offset_symbol> <name> <source_file>
version 1
globals_size 27
global 0 4 0 absolution_offset_input_value input_value targets.c
global 4 16 0 absolution_offset_handlers handlers targets.c
global 20 7 1 absolution_offset_targets_c_cache cache targets.c
```

`offset` is relative to the start of the input, `bytes` is how many input bytes
the global consumes, and `source_file` comes last because it may contain spaces.
A global constrained to a single value consumes zero bytes, so it shares the
offset of whatever follows it.

A `static` global is exported under the mangled name it receives after
`objcopy` renaming (see the `.redef` file), because that is the name it is
reachable by once linked.

Offsets describe one generation run: constraining a field changes how many bytes
it consumes and shifts everything after it. Re-read the export after each build
rather than caching values.

For CMake projects `absolution_add_fuzzer()` always writes the sidecar and
exposes its path as `${NAME}_OFFSETS_FILE`.

## Invariant Language

Invariants constrain the domains of global fields. They are written in Zig's `.zon` format.
Expand Down
13 changes: 11 additions & 2 deletions cmake/AbsolutionFuzzer.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
# linked normally.
# SANITIZERS — sanitizer list (default: fuzzer,address).
#
# Generated artifacts include an input layout export (fuzzer.offsets, plus
# matching constants in the generated C) giving the byte offset of every sampled
# global; see USAGE.md.
#
# Created targets:
# ${NAME}_objs — OBJECT library containing compiled target sources.
# ${NAME}_generate — Custom target that runs absolution CLI to produce artifacts.
Expand All @@ -42,6 +46,7 @@
# ${NAME}_SEED_FILE — Path to the generated seed file.
# ${NAME}_FUZZER_C — Path to the generated fuzzer.c file.
# ${NAME}_REDEF_FILE — Path to the generated .redef file.
# ${NAME}_OFFSETS_FILE — Path to the generated input layout sidecar.
# ${NAME}_GENERATE_TARGET — Name of the generate target.
# ${NAME}_REDEF_TARGET — Name of the redef target.

Expand Down Expand Up @@ -180,6 +185,7 @@ function(absolution_add_fuzzer)
set(_FUZZER_C "${_FUZZ_DIR}/fuzzer.c")
set(_REDEF_FILE "${_FUZZ_DIR}/fuzzer.redef")
set(_SEED_FILE "${_FUZZ_DIR}/fuzzer.seed")
set(_OFFSETS_FILE "${_FUZZ_DIR}/fuzzer.offsets")
set(_OBJ_LIST "${_FUZZ_DIR}/objfiles.txt")
set(_FLAGS_FILE "${_FUZZ_DIR}/absolution_flags.rsp")
set(_TARGETS_FILE "${_FUZZ_DIR}/absolution_targets.txt")
Expand Down Expand Up @@ -275,13 +281,14 @@ $<JOIN:$<TARGET_PROPERTY:${_OBJ_LIB},COMPILE_OPTIONS>,\n>
set(_GENERATE_TARGET "${FUZZ_NAME}_generate")

add_custom_command(
OUTPUT "${_FUZZER_C}" "${_REDEF_FILE}" "${_SEED_FILE}"
OUTPUT "${_FUZZER_C}" "${_REDEF_FILE}" "${_SEED_FILE}" "${_OFFSETS_FILE}"
COMMAND "${CMAKE_COMMAND}"
"-DABSOLUTION=${ABSOLUTION_EXECUTABLE}"
"-DTARGETS_FILE=${_TARGETS_FILE}"
"-DOUT_C=${_FUZZER_C}"
"-DREDEF=${_REDEF_FILE}"
"-DSEED=${_SEED_FILE}"
"-DOFFSETS=${_OFFSETS_FILE}"
"-DENTRY=${FUZZ_ENTRY}"
"-DINVARIANT=${_abs_inv}"
"-DFLAGS_FILE=${_FLAGS_FILE}"
Expand All @@ -293,13 +300,14 @@ $<JOIN:$<TARGET_PROPERTY:${_OBJ_LIB},COMPILE_OPTIONS>,\n>
VERBATIM
)
add_custom_target(${_GENERATE_TARGET}
DEPENDS "${_FUZZER_C}" "${_REDEF_FILE}" "${_SEED_FILE}"
DEPENDS "${_FUZZER_C}" "${_REDEF_FILE}" "${_SEED_FILE}" "${_OFFSETS_FILE}"
)

set_target_properties(${_GENERATE_TARGET} PROPERTIES
ABSOLUTION_FUZZER_C "${_FUZZER_C}"
ABSOLUTION_REDEF "${_REDEF_FILE}"
ABSOLUTION_SEED "${_SEED_FILE}"
ABSOLUTION_OFFSETS "${_OFFSETS_FILE}"
)

add_dependencies(${_GENERATE_TARGET} ${_OBJ_LIB})
Expand Down Expand Up @@ -383,6 +391,7 @@ $<JOIN:$<TARGET_PROPERTY:${_OBJ_LIB},COMPILE_OPTIONS>,\n>
set(${FUZZ_NAME}_SEED_FILE "${_SEED_FILE}" PARENT_SCOPE)
set(${FUZZ_NAME}_FUZZER_C "${_FUZZER_C}" PARENT_SCOPE)
set(${FUZZ_NAME}_REDEF_FILE "${_REDEF_FILE}" PARENT_SCOPE)
set(${FUZZ_NAME}_OFFSETS_FILE "${_OFFSETS_FILE}" PARENT_SCOPE)
set(${FUZZ_NAME}_GENERATE_TARGET "${_GENERATE_TARGET}" PARENT_SCOPE)
set(${FUZZ_NAME}_REDEF_TARGET "${_REDEF_TARGET}" PARENT_SCOPE)
endfunction()
4 changes: 4 additions & 0 deletions cmake/RunAbsolution.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# SEED — output .seed path
# ENTRY — entry function name
# INVARIANT — (optional) invariant file path
# OFFSETS — (optional) input layout sidecar output path
# FLAGS_FILE — one compiler flag per line (-Ipath, -DFOO, -std=c99 …)
# WORK_DIR — CMAKE_SOURCE_DIR (absolution resolves targets relative to this)

Expand All @@ -28,6 +29,9 @@ list(APPEND _cmd
if(INVARIANT)
list(APPEND _cmd -i "${INVARIANT}")
endif()
if(OFFSETS)
list(APPEND _cmd -m "${OFFSETS}")
endif()
list(APPEND _cmd --)
foreach(_f ${_flags})
list(APPEND _cmd "${_f}")
Expand Down
14 changes: 12 additions & 2 deletions scripts/gen-golden.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
# ./scripts/gen-golden.sh tests/aroccbug
#
# This script finds all .c files in the given test directory and generates
# corresponding .zon golden files by running absolution.
# corresponding .zon golden files by running absolution. A .offsets golden is
# refreshed only when the test already ships one, matching the integration
# runner, which compares that sidecar only where it exists.

set -euo pipefail

Expand Down Expand Up @@ -47,6 +49,7 @@ trap "rm -rf $TMPDIR" EXIT
for C_FILE in $C_FILES; do
BASENAME=$(basename "$C_FILE")
ZON_FILE="${TEST_DIR}/${BASENAME}.zon"
OFFSETS_FILE="${TEST_DIR}/${BASENAME}.offsets"
FLAGS_FILE="${TEST_DIR}/${BASENAME}.flags"

echo "Generating golden file for: $C_FILE"
Expand Down Expand Up @@ -75,20 +78,27 @@ for C_FILE in $C_FILES; do
--zon "$TMPDIR/${BASENAME}.zon" \
--out "$TMPDIR/${BASENAME}.fuzzer.c" \
--redef "$TMPDIR/${BASENAME}.redef.txt" \
--offsets "$TMPDIR/${BASENAME}.offsets" \
-- "${EXTRA_ARGS[@]}"
else
"$ABSOLUTION" \
--targets "$C_FILE" \
"${INVARIANT_ARGS[@]}" \
--zon "$TMPDIR/${BASENAME}.zon" \
--out "$TMPDIR/${BASENAME}.fuzzer.c" \
--redef "$TMPDIR/${BASENAME}.redef.txt"
--redef "$TMPDIR/${BASENAME}.redef.txt" \
--offsets "$TMPDIR/${BASENAME}.offsets"
fi

# Copy the generated .zon to the test directory
cp "$TMPDIR/${BASENAME}.zon" "$ZON_FILE"

echo " -> Created: $ZON_FILE"

if [ -f "$OFFSETS_FILE" ]; then
cp "$TMPDIR/${BASENAME}.offsets" "$OFFSETS_FILE"
echo " -> Updated: $OFFSETS_FILE"
fi
done

echo "Done!"
29 changes: 27 additions & 2 deletions scripts/integration.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! Integration tests for absolution.
//!
//! Finds .c test files under tests/, builds absolution once, then for each test:
//! 1. Runs absolution to produce .zon and fuzzer.c
//! 1. Runs absolution to produce .zon, fuzzer.c and the input offsets sidecar
//! 2. Compiles the generated fuzzer.c with `zig cc`
//! 3. Compares the .zon output against a golden file
//! 4. Compares the offsets sidecar against a golden file, when one exists
//!
//! Run with: zig run scripts/integration.zig
//!
Expand Down Expand Up @@ -107,6 +108,8 @@ const TestCase = struct {
flags: []const []const u8 = &.{},
targets: []const []const u8 = &.{},
invariant_path: ?[]const u8 = null,
/// Golden input-layout sidecar; only compared when the fixture ships one.
offsets_golden_path: ?[]const u8 = null,
};

// -----------------------------------------------------------------------
Expand Down Expand Up @@ -205,6 +208,10 @@ fn discoverTests(arena: std.mem.Allocator, io: std.Io, cases: *std.ArrayList(Tes
const inv_path = try std.fmt.allocPrint(arena, "{s}.in", .{c_path});
const invariant_path: ?[]const u8 = if (fileExists(cwd, io, inv_path)) inv_path else null;

// .offsets sidecar (golden input layout)
const offsets_path = try std.fmt.allocPrint(arena, "{s}.offsets", .{c_path});
const offsets_golden_path: ?[]const u8 = if (fileExists(cwd, io, offsets_path)) offsets_path else null;

try cases.append(arena, .{
.c_path = c_path,
.golden_path = golden_path,
Expand All @@ -213,6 +220,7 @@ fn discoverTests(arena: std.mem.Allocator, io: std.Io, cases: *std.ArrayList(Tes
.flags = flags,
.targets = targets,
.invariant_path = invariant_path,
.offsets_golden_path = offsets_golden_path,
});
}
}
Expand All @@ -234,6 +242,7 @@ fn runOneTest(
const out_zon = try std.fmt.allocPrint(arena, "{s}/out.zon", .{test_dir});
const out_fuzzer = try std.fmt.allocPrint(arena, "{s}/fuzzer.c", .{test_dir});
const out_redef = try std.fmt.allocPrint(arena, "{s}/redef.txt", .{test_dir});
const out_offsets = try std.fmt.allocPrint(arena, "{s}/fuzzer.offsets", .{test_dir});
const out_obj = try std.fmt.allocPrint(arena, "{s}/fuzzer.o", .{test_dir});

// -- Build absolution argv --
Expand All @@ -245,7 +254,9 @@ fn runOneTest(
if (tc.invariant_path) |inv| {
try argv.appendSlice(arena, &.{ "-i", inv });
}
try argv.appendSlice(arena, &.{ "--zon", out_zon, "--out", out_fuzzer, "--redef", out_redef });
// Offsets are always requested so every fixture also checks that the
// exported layout constants compile.
try argv.appendSlice(arena, &.{ "--zon", out_zon, "--out", out_fuzzer, "--redef", out_redef, "--offsets", out_offsets });
if (tc.flags.len > 0) {
try argv.append(arena, "--");
try argv.appendSlice(arena, tc.flags);
Expand All @@ -268,6 +279,20 @@ fn runOneTest(
std.debug.print(" Actual output: {s}\n", .{out_zon});
return error.GoldenMismatch;
}

// 4. Offsets golden comparison
if (tc.offsets_golden_path) |offsets_golden| {
const actual_offsets = try std.Io.Dir.cwd().readFileAlloc(io, out_offsets, gpa, .limited(10 * 1024 * 1024));
defer gpa.free(actual_offsets);
const expected_offsets = try std.Io.Dir.cwd().readFileAlloc(io, offsets_golden, gpa, .limited(10 * 1024 * 1024));
defer gpa.free(expected_offsets);

if (!std.mem.eql(u8, actual_offsets, expected_offsets)) {
std.debug.print(" Offsets mismatch: expected {s}\n", .{offsets_golden});
std.debug.print(" Actual output: {s}\n", .{out_offsets});
return error.OffsetsMismatch;
}
}
}

// -----------------------------------------------------------------------
Expand Down
Loading