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
14 changes: 11 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ absolution/
│ ├── include_paths.zig # Include path discovery (zig cc compatibility)
│ ├── seed.zig # initial seed generation
│ └── cgen/
│ ├── ir.zig # Core data structures (Domain, Field, Global)
│ ├── ir.zig # Core data structures (Domain, Field, Global)
│ ├── builder.zig # File writing utilities and type re-exports
│ └── emit.zig # C code emission (sampler, checker, entrypoint)
│ ├── emit.zig # C code emission (sampler, checker, entrypoint)
│ └── prefixheader.zig # Public state-prefix header generation
├── tests/ # Integration test cases
│ └── <test_name>/
│ ├── <file>.c # Test input
Expand Down Expand Up @@ -78,11 +79,18 @@ Core data structures:
### `cgen/emit.zig`

C code emission:
- `writeFuzzerC`: Writes includes, extern declarations, redef file, sampler, checker, and entrypoint
- `writeFuzzerC`: Coordinates header generation and writes includes, extern declarations, redef file, sampler, checker, and entrypoint
- `emitSampler`: Generates `sample_invariant()` function
- `emitChecker`: Generates `check_invariant()` function
- `emitEntrypoint`: Generates `LLVMFuzzerTestOneInput()`

### `cgen/prefixheader.zig`

State-prefix header emission:
- Derives the header path from the generated C output path
- Emits `AbsolutionStatePrefix` and `ABSOLUTION_STATE_PREFIX_SIZE`
- Preserves sampler ordering for global and field dimensions

## Development Workflow

### Building
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Absolution lets you specify an invariant for a program’s global state and fuzz
1. Parse globals from your C translation unit(s) using [aro](https://github.com/Vexu/aro).
2. Build flattened globals containing fields, padding, and domains.
3. Optionally apply a `.zon` invariant to constrain field values (per-element `.values` / `.pointers`, or whole-field blobs with `.whole_values` on array-shaped fields; see [USAGE.md](USAGE.md)).
4. Emit `fuzzer.c` with sampling, invariant checking, and libFuzzer entrypoint.
4. Emit `fuzzer.c` with sampling, invariant checking, and libFuzzer entrypoint,
plus `fuzzer.h` describing the encoded state prefix for custom mutators.
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.

Expand Down
55 changes: 53 additions & 2 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,60 @@ Example for `Config configs[10]` with `int values[5]` (struct size 8 bytes):
}}
```

## Generated Code
## Generated Output

The generated `fuzzer.c` contains:
Absolution generates the following C interface:

### State-prefix header (`fuzzer.h` by default)

Absolution also generates a header beside the requested C output, using the
same basename (`--out path/foo.c` produces `path/foo.h`). It exposes:

- `ABSOLUTION_STATE_PREFIX_SIZE`, the number of bytes reserved for state.
- The packed `AbsolutionStatePrefix` type describing those bytes.

The type contains one nested member per sampled global. Unconstrained fields
use a `_bytes` member containing their object representation. Constrained
value, whole-value, and pointer domains use one-byte `_selector` members.
Fields fixed to a single value and padding consume no input bytes and are
omitted. Selectors are reduced modulo the number of allowed candidates by the
generated sampler.

This allows a libFuzzer custom mutator to handle state and entrypoint input
separately:

```c
#include "fuzzer.h"
#include <string.h>

extern size_t LLVMFuzzerMutate(
uint8_t *data, size_t size, size_t max_size
);

size_t LLVMFuzzerCustomMutator(
uint8_t *data, size_t size, size_t max_size, unsigned seed
) {
if (max_size < ABSOLUTION_STATE_PREFIX_SIZE)
return 0;
if (size < ABSOLUTION_STATE_PREFIX_SIZE) {
memset(data + size, 0, ABSOLUTION_STATE_PREFIX_SIZE - size);
size = ABSOLUTION_STATE_PREFIX_SIZE;
}

AbsolutionStatePrefix *state = (AbsolutionStatePrefix *)data;
/* Mutate state->global_<index>_<name>... */

uint8_t *input = data + ABSOLUTION_STATE_PREFIX_SIZE;
size_t input_size = size - ABSOLUTION_STATE_PREFIX_SIZE;
input_size = LLVMFuzzerMutate(
input,
input_size,
max_size - ABSOLUTION_STATE_PREFIX_SIZE
);

return ABSOLUTION_STATE_PREFIX_SIZE + input_size;
}
```

### `sample_invariant(data, size)`

Expand Down
9 changes: 7 additions & 2 deletions cmake/AbsolutionFuzzer.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
# Exported variables (PARENT_SCOPE):
# ${NAME}_SEED_FILE — Path to the generated seed file.
# ${NAME}_FUZZER_C — Path to the generated fuzzer.c file.
# ${NAME}_STATE_HEADER — Path to the generated state-prefix header.
# ${NAME}_REDEF_FILE — Path to the generated .redef file.
# ${NAME}_GENERATE_TARGET — Name of the generate target.
# ${NAME}_REDEF_TARGET — Name of the redef target.
Expand Down Expand Up @@ -178,6 +179,7 @@ function(absolution_add_fuzzer)
file(MAKE_DIRECTORY "${_FUZZ_DIR}")

set(_FUZZER_C "${_FUZZ_DIR}/fuzzer.c")
set(_STATE_HEADER "${_FUZZ_DIR}/fuzzer.h")
set(_REDEF_FILE "${_FUZZ_DIR}/fuzzer.redef")
set(_SEED_FILE "${_FUZZ_DIR}/fuzzer.seed")
set(_OBJ_LIST "${_FUZZ_DIR}/objfiles.txt")
Expand Down Expand Up @@ -275,7 +277,7 @@ $<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}" "${_STATE_HEADER}" "${_REDEF_FILE}" "${_SEED_FILE}"
COMMAND "${CMAKE_COMMAND}"
"-DABSOLUTION=${ABSOLUTION_EXECUTABLE}"
"-DTARGETS_FILE=${_TARGETS_FILE}"
Expand All @@ -293,11 +295,12 @@ $<JOIN:$<TARGET_PROPERTY:${_OBJ_LIB},COMPILE_OPTIONS>,\n>
VERBATIM
)
add_custom_target(${_GENERATE_TARGET}
DEPENDS "${_FUZZER_C}" "${_REDEF_FILE}" "${_SEED_FILE}"
DEPENDS "${_FUZZER_C}" "${_STATE_HEADER}" "${_REDEF_FILE}" "${_SEED_FILE}"
)

set_target_properties(${_GENERATE_TARGET} PROPERTIES
ABSOLUTION_FUZZER_C "${_FUZZER_C}"
ABSOLUTION_STATE_HEADER "${_STATE_HEADER}"
ABSOLUTION_REDEF "${_REDEF_FILE}"
ABSOLUTION_SEED "${_SEED_FILE}"
)
Expand Down Expand Up @@ -340,6 +343,7 @@ $<JOIN:$<TARGET_PROPERTY:${_OBJ_LIB},COMPILE_OPTIONS>,\n>

# ── Step 4: Link into the fuzzer executable ───────────────────────────────
add_executable(${FUZZ_NAME} "${_FUZZER_C}")
target_include_directories(${FUZZ_NAME} PRIVATE "${_FUZZ_DIR}")

if(FUZZ_HARNESS)
target_sources(${FUZZ_NAME} PRIVATE "${FUZZ_HARNESS}")
Expand Down Expand Up @@ -382,6 +386,7 @@ $<JOIN:$<TARGET_PROPERTY:${_OBJ_LIB},COMPILE_OPTIONS>,\n>
# ── Export ────────────────────────────────────────────────────────────────
set(${FUZZ_NAME}_SEED_FILE "${_SEED_FILE}" PARENT_SCOPE)
set(${FUZZ_NAME}_FUZZER_C "${_FUZZER_C}" PARENT_SCOPE)
set(${FUZZ_NAME}_STATE_HEADER "${_STATE_HEADER}" PARENT_SCOPE)
set(${FUZZ_NAME}_REDEF_FILE "${_REDEF_FILE}" PARENT_SCOPE)
set(${FUZZ_NAME}_GENERATE_TARGET "${_GENERATE_TARGET}" PARENT_SCOPE)
set(${FUZZ_NAME}_REDEF_TARGET "${_REDEF_TARGET}" PARENT_SCOPE)
Expand Down
55 changes: 53 additions & 2 deletions scripts/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
//! Finds .c test files under tests/, builds absolution once, then for each test:
//! 1. Runs absolution to produce .zon and fuzzer.c
//! 2. Compiles the generated fuzzer.c with `zig cc`
//! 3. Compares the .zon output against a golden file
//! 3. Compiles a custom mutator against the generated state-prefix header
//! 4. Runs an optional runtime sidecar (`<target>.runtime.c`)
//! 5. Compares the .zon output against a golden file
//!
//! Run with: zig run scripts/integration.zig
//!
Expand Down Expand Up @@ -107,6 +109,7 @@ const TestCase = struct {
flags: []const []const u8 = &.{},
targets: []const []const u8 = &.{},
invariant_path: ?[]const u8 = null,
runtime_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;

// .runtime.c sidecar (compiled and run with the generated fuzzer)
const runtime_candidate = try std.fmt.allocPrint(arena, "{s}.runtime.c", .{c_path});
const runtime_path: ?[]const u8 = if (fileExists(cwd, io, runtime_candidate)) runtime_candidate 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,
.runtime_path = runtime_path,
});
}
}
Expand All @@ -235,6 +243,9 @@ fn runOneTest(
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_obj = try std.fmt.allocPrint(arena, "{s}/fuzzer.o", .{test_dir});
const mutator_c = try std.fmt.allocPrint(arena, "{s}/custom_mutator.c", .{test_dir});
const mutator_obj = try std.fmt.allocPrint(arena, "{s}/custom_mutator.o", .{test_dir});
const runtime_exe = try std.fmt.allocPrint(arena, "{s}/runtime_test", .{test_dir});

// -- Build absolution argv --
var argv: std.ArrayList([]const u8) = .empty;
Expand All @@ -257,7 +268,47 @@ fn runOneTest(
// 2. Compile generated fuzzer.c
try execCapture(gpa, io, &.{ "zig", "cc", "-c", out_fuzzer, "-o", out_obj, "-I", tc.dir_path });

// 3. Golden-file comparison
// 3. Compile a custom-mutator translation unit against the generated
// state-prefix API.
var mutator_file = try std.Io.Dir.cwd().createFile(io, mutator_c, .{ .truncate = true });
defer mutator_file.close(io);
try mutator_file.writeStreamingAll(io,
\\#include "fuzzer.h"
\\#include <stddef.h>
\\#include <stdint.h>
\\#include <string.h>
\\
\\size_t LLVMFuzzerCustomMutator(
\\ uint8_t *data, size_t size, size_t max_size, unsigned seed
\\) {
\\ (void)seed;
\\ if (max_size < ABSOLUTION_STATE_PREFIX_SIZE) return 0;
\\ if (size < ABSOLUTION_STATE_PREFIX_SIZE) {
\\ memset(data + size, 0, ABSOLUTION_STATE_PREFIX_SIZE - size);
\\ size = ABSOLUTION_STATE_PREFIX_SIZE;
\\ }
\\ AbsolutionStatePrefix *state = (AbsolutionStatePrefix *)data;
\\ (void)state;
\\ return size;
\\}
\\
);
try execCapture(gpa, io, &.{ "zig", "cc", "-c", mutator_c, "-o", mutator_obj, "-I", test_dir });

// 4. Compile and run a dedicated runtime sidecar, when present.
if (tc.runtime_path) |runtime_path| {
try execCapture(gpa, io, &.{
"zig", "cc",
out_fuzzer, tc.c_path,
runtime_path, "-o",
runtime_exe, "-I",
test_dir, "-I",
tc.dir_path,
});
try execCapture(gpa, io, &.{runtime_exe});
}

// 5. Golden-file comparison
const actual = try std.Io.Dir.cwd().readFileAlloc(io, out_zon, gpa, .limited(10 * 1024 * 1024));
defer gpa.free(actual);
const expected = try std.Io.Dir.cwd().readFileAlloc(io, tc.golden_path, gpa, .limited(10 * 1024 * 1024));
Expand Down
28 changes: 22 additions & 6 deletions src/cgen/emit.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const std = @import("std");
const Parser = @import("../Parser.zig");
const ir = @import("ir.zig");
const prefixheader = @import("prefixheader.zig");

fn writeIndent(io: std.Io, file: *std.Io.File, depth: usize) !void {
for (0..depth) |_| try file.writeStreamingAll(io, " ");
Expand Down Expand Up @@ -250,12 +251,23 @@ pub fn writeFuzzerC(
entry_name: []const u8,
func_symbols: []const []const u8,
) !void {
const header_path = try prefixheader.path(allocator, out_path);
defer allocator.free(header_path);
try prefixheader.write(allocator, io, globals, needed_bytes, header_path);

var file = try std.Io.Dir.cwd().createFile(io, out_path, .{ .truncate = true });
defer file.close(io);

var redef_file = try std.Io.Dir.cwd().createFile(io, redef_path, .{ .truncate = true });
defer redef_file.close(io);

const header_include = try std.fmt.allocPrint(
allocator,
"#include \"{s}\"\n",
.{std.fs.path.basename(header_path)},
);
defer allocator.free(header_include);
try file.writeStreamingAll(io, header_include);
try file.writeStreamingAll(io,
\\#include <assert.h>
\\#include <stdint.h>
Expand All @@ -269,10 +281,6 @@ pub fn writeFuzzerC(
defer allocator.free(fwd_decl);
try file.writeStreamingAll(io, fwd_decl);

const globals_size_define = try std.fmt.allocPrint(allocator, "#define ABSOLUTION_GLOBALS_SIZE {d}\n\n", .{needed_bytes});
defer allocator.free(globals_size_define);
try file.writeStreamingAll(io, globals_size_define);

for (func_symbols) |sym| {
const func_decl = try std.fmt.allocPrint(allocator, "extern void {s}(void);\n", .{sym});
defer allocator.free(func_decl);
Expand Down Expand Up @@ -317,7 +325,7 @@ fn emitSampler(allocator: std.mem.Allocator, io: std.Io, globals: []const Parser
var ptr_idx: usize = 0;
try file.writeStreamingAll(io, "ptrdiff_t sample_invariant(const uint8_t *data, size_t size) {\n");
try file.writeStreamingAll(io, " size_t off = 0;\n");
try file.writeStreamingAll(io, " const size_t needed = ABSOLUTION_GLOBALS_SIZE ;\n");
try file.writeStreamingAll(io, " const size_t needed = ABSOLUTION_STATE_PREFIX_SIZE;\n");
try file.writeStreamingAll(io, " if (size < needed) return -1;\n");

for (globals) |g| {
Expand Down Expand Up @@ -1501,6 +1509,8 @@ test "writeFuzzerC end-to-end produces valid output" {
defer alloc.free(dir_path);
const out_path = try std.fs.path.join(alloc, &.{ dir_path, "fuzzer.c" });
defer alloc.free(out_path);
const header_path = try std.fs.path.join(alloc, &.{ dir_path, "fuzzer.h" });
defer alloc.free(header_path);
const redef_path = try std.fs.path.join(alloc, &.{ dir_path, "fuzzer.redef" });
defer alloc.free(redef_path);

Expand Down Expand Up @@ -1532,9 +1542,15 @@ test "writeFuzzerC end-to-end produces valid output" {

var buf: [16384]u8 = undefined;
const out = try readTmpFile(&tmp, "fuzzer.c", &buf);
var header_buf: [16384]u8 = undefined;
const header = try readTmpFile(&tmp, "fuzzer.h", &header_buf);
try std.testing.expect(std.mem.indexOf(u8, out, "#include <assert.h>") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "#include \"fuzzer.h\"") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "int TestHarness(const uint8_t *data, size_t size)") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "#define ABSOLUTION_GLOBALS_SIZE 4") != null);
try std.testing.expect(std.mem.indexOf(u8, header, "#define ABSOLUTION_STATE_PREFIX_SIZE 4u") != null);
try std.testing.expect(std.mem.indexOf(u8, header, "} AbsolutionStatePrefix;") != null);
try std.testing.expect(std.mem.indexOf(u8, header, "global_0_g") != null);
try std.testing.expect(std.mem.indexOf(u8, header, "field_0_x_bytes[4]") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "uint8_t __attribute__((weak)) g[8]") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "sample_invariant") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "check_invariant") != null);
Expand Down
Loading