Skip to content
Merged
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
33 changes: 33 additions & 0 deletions .github/workflows/allocfault.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: allocfault

on:
workflow_dispatch:
push:
paths:
- "tools/allocfault/**"
- "scripts/run-original-suite.sh"
- ".github/workflows/allocfault.yml"

jobs:
smoke-interposer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Zig 0.16.0
run: |
curl -sL https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz | tar -xJ
echo "$(pwd)/zig-x86_64-linux-0.16.0" >> "$GITHUB_PATH"
- name: Build and probe interposer
run: |
export CACHE="$HOME/.cache/zig-tinyexpr"
# Short corpus via env override: run script but the script builds its own corpus.
# Probe only: compile interposer and assert counter advances on one expression.
WORK=.local/allocfault
mkdir -p "$WORK"
cc -shared -fPIC -O2 -ldl -pthread tools/allocfault/interposer.c -o "$WORK/liballocfault.so"
cc -O0 -g -I third_party/tinyexpr-c -c tools/allocfault/driver.c -o "$WORK/driver.o"
cc -O0 -g -I third_party/tinyexpr-c -c third_party/tinyexpr-c/tinyexpr.c -o "$WORK/te.o"
cc -O0 -g "$WORK/driver.o" "$WORK/te.o" -o "$WORK/driver-c" -ldl -lm
OUT="$(LD_PRELOAD=$WORK/liballocfault.so ALLOCFAULT_COUNT_ONLY=1 ALLOCFAULT_SILENT=1 $WORK/driver-c '1+2*3')"
echo "$OUT"
python3 -c 'import json,sys; j=json.loads(sys.argv[1]); assert j["allocs"]>0, j' "$OUT"
28 changes: 28 additions & 0 deletions .github/workflows/fuzz-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: fuzz-smoke

on:
workflow_dispatch:
push:
paths:
- "fuzz/**"
- "scripts/run-fuzz.sh"
- "scripts/run-metamorphic.sh"
- ".github/workflows/fuzz-smoke.yml"

jobs:
certified-short:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Zig 0.16.0
run: |
curl -sL https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz | tar -xJ
echo "$(pwd)/zig-x86_64-linux-0.16.0" >> "$GITHUB_PATH"
- name: 5s differential fuzz smoke
run: |
export CACHE="$HOME/.cache/zig-tinyexpr"
bash scripts/run-fuzz.sh 5 fuzz/logs/ci-smoke.txt ci-smoke
- name: Metamorphic smoke
run: |
export CACHE="$HOME/.cache/zig-tinyexpr"
bash scripts/run-metamorphic.sh
30 changes: 30 additions & 0 deletions .github/workflows/sanitizer-matrix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: sanitizer-matrix

on:
workflow_dispatch:
push:
paths:
- "scripts/run-sanitizer-matrix.sh"
- "scripts/run-original-suite.sh"
- "tools/zig_probe_stub.c"
- ".github/workflows/sanitizer-matrix.yml"

jobs:
matrix:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- name: Install Zig 0.16.0
run: |
curl -sL https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz | tar -xJ
echo "$(pwd)/zig-x86_64-linux-0.16.0" >> "$GITHUB_PATH"
- name: Run sanitizer matrix
run: |
export CACHE="$HOME/.cache/zig-tinyexpr"
bash scripts/run-sanitizer-matrix.sh
- name: Upload report
uses: actions/upload-artifact@v4
with:
name: sanitizers-md
path: docs/evidence/sanitizers.md
73 changes: 73 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,76 @@ Entries that only restate a diff will be deleted.
- Evidence: `scripts/run-enumerate.sh`,
`docs/evidence/fragments/enumeration.json` (`total` 11505, `divergences` 0
at string length 3 / AST depth 1).

## D021 — One malloc interposer for C and Zig ABI

- Constraint: Track G asks for allocator-misuse evidence; both implementations
must be failed at every allocation ordinal on a shared corpus.
- Options: separate Zig GPA fault injection; `ld --wrap=malloc`; `LD_PRELOAD`
over libc `malloc`/`calloc`/`realloc`/`free`.
- Choice: `tools/allocfault` LD_PRELOAD interposer for the main campaign. The
Zig static library allocates through libc `malloc` at the ABI boundary just
as the C original does, so **one interposer drives both**. ASan builds use a
preprocessor shim TU (`tinyexpr_instrumented.c`) because ASan owns `malloc`
and GNU `--wrap` did not redirect libc `malloc` on this host.
- Consequence: ordinal campaigns are directly comparable; ledger diffs are
meaningful; ASan ordinal runs are C-shim-only with Zig still on LD_PRELOAD.
- Evidence: `tools/allocfault/run.sh`,
`docs/evidence/fragments/allocfault.json` (271 ordinals, 0 crashes, ledger
identical on `1+2*3` with 11 events).

## D022 — Differential fuzz engines and ReleaseFast link

- Constraint: bonus needs 60 continuous seconds with zero divergences; Debug
Zig fuzzing is known-crashy; linking ReleaseSafe `.a` into a gcc main fails
on `__zig_probe_stack`.
- Options: Zig `std.testing` fuzz only; libFuzzer only; dual harness.
- Choice: in-process C-renamed + Zig harness at `fuzz/harness.c` with
structured generation, plus `fuzz/harness.zig` Smith metamorphic tests.
Build the Zig archive as ReleaseFast for C-linked drivers; provide
`tools/zig_probe_stub.c` when Debug/ReleaseSafe archives must link into gcc.
- Consequence: certified run publishes executions, not only wall-clock;
metamorphic transforms are a separate campaign.
- Evidence: `fuzz/log.txt` (48512558 execs / 60s / 0 divergences),
`docs/evidence/fragments/metamorphic.json` (45 cases / 0 failures).

## D023 — Sanitizer matrix and float flags

- Constraint: judges need a glanceable matrix across C sanitizers, Zig opt
modes, and flag configs; float contraction would silently break bit-exact
equivalence.
- Options: cite prompt-1 measurements; re-measure at HEAD.
- Choice: re-measure via `scripts/run-sanitizer-matrix.sh` at the current
commit; record Valgrind as `tool_missing` when absent; empirically check
`cc -O2 -###` for fast-math flags.
- Consequence: sixteen Zig suite cells (4 modes x 4 configs) plus C
UBSan/ASan cells are first-class evidence; float claim is measured.
- Evidence: `docs/evidence/sanitizers.md`,
`docs/evidence/fragments/sanitizers.json`.

## D024 — Signed-char ctype lead closed by trapping wrapper

- Constraint: UBSan on glibc did not diagnose `isalpha` on high-bit bytes;
editing `third_party/` is forbidden.
- Options: declare unverifiable; rely on standard citations alone; inject a
trapping ctype via compiler `-include`.
- Choice: ship `scripts/run-buglead-ctype.sh` with a trap header that aborts
on negative ctype arguments; feed bytes `0x80..0xFF` through `te_interp`.
- Consequence: Lead B is a confirmed UB surface with a one-line stderr
reproducer (`ctype_trap: isalpha(-128)`), ready for upstream filing.
- Evidence: `docs/findings.md` Lead B,
`docs/evidence/fragments/buglead-ctype.json`.

## D025 — Locale lead: correctness yes, hang no; Zig shares strtod

- Constraint: Lead C claimed a possible non-advancing `s->next` DoS under
`de_DE.UTF-8`; the port was expected to be locale-independent.
- Options: change Zig to locale-independent parsing now; keep strtod parity.
- Choice: keep `libm.strtod` for bit-exact parity; measure both under
`setlocale(LC_ALL, "de_DE.UTF-8")`; hard-timeout the suspected hang inputs.
- Consequence: `1.5` fails on both sides after setlocale (correctness bug in
the original, shared by the port); `.5` does not hang within 3s; smoke under
env-only LC_ALL is not a trigger because smoke never calls setlocale.
Equivalence for decimal literals is scoped to the C locale.
- Evidence: `docs/findings.md` Lead C,
`docs/evidence/fragments/buglead-locale.json`.
27 changes: 25 additions & 2 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ pub fn build(b: *std.Build) void {
const cross_step = b.step("cross", "Cross-compile matrix");
cross_step.dependOn(&cross_cmd.step);

_ = b.step("fuzz", "Prompt 4+: differential fuzz");
const fuzz_cmd = b.addSystemCommand(&.{ "bash", "scripts/run-fuzz.sh", "5", "fuzz/logs/fuzz-verify.txt", "verify" });
fuzz_cmd.setCwd(b.path("."));
const fuzz_step = b.step("fuzz", "Short differential fuzz tier");
fuzz_step.dependOn(&fuzz_cmd.step);

const cover_cmd = b.addSystemCommand(&.{ "bash", "scripts/run-coverage-c.sh" });
cover_cmd.setCwd(b.path("."));
const cover_step = b.step("cover", "lcov coverage of original C + evidence fragment");
Expand All @@ -146,6 +150,25 @@ pub fn build(b: *std.Build) void {
const enumerate_quick_step = b.step("enumerate-quick", "Short differential-enumeration tier");
enumerate_quick_step.dependOn(&enumerate_quick_cmd.step);

_ = b.step("alloc-fault", "Prompt 4+: malloc failure injection");
const alloc_cmd = b.addSystemCommand(&.{ "bash", "tools/allocfault/run.sh" });
alloc_cmd.setCwd(b.path("."));
const alloc_step = b.step("alloc-fault", "Malloc ordinal failure injection");
alloc_step.dependOn(&alloc_cmd.step);

// Wire anatomy fuzz/harness.zig as a ReleaseSafe test module.
const harness_mod = b.createModule(.{
.root_source_file = b.path("fuzz/harness.zig"),
.target = target,
.optimize = .ReleaseSafe,
.link_libc = true,
});
harness_mod.addImport("tinyexpr", root_mod);
const harness_tests = b.addTest(.{
.name = "fuzz-harness",
.root_module = harness_mod,
});
const run_harness_tests = b.addRunArtifact(harness_tests);
fuzz_step.dependOn(&run_harness_tests.step);

_ = b.step("demo", "Prompt 9: demo assets");
}
150 changes: 150 additions & 0 deletions docs/evidence/allocfault-asan.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
=== allocfault metadata ===
started_at_utc=2026-08-02T05:31:27Z
git_sha=b5e302ed15281e3d96c0998e64e0b8aa20afeee0
host=Linux elmengo 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 18 21:54:43 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
asan=1
/mnt/d/hackathons/portmortem/tinyexpr.zig/tools/allocfault/interposer.c: In function ‘ledger_write’:
/mnt/d/hackathons/portmortem/tinyexpr.zig/tools/allocfault/interposer.c:38:11: warning: ignoring return value of ‘write’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
38 | (void)write(ledger_fd, buf, n);
| ^~~~~~~~~~~~~~~~~~~~~~~~
/mnt/d/hackathons/portmortem/tinyexpr.zig/tools/allocfault/interposer.c: In function ‘fail_alloc’:
/mnt/d/hackathons/portmortem/tinyexpr.zig/tools/allocfault/interposer.c:76:26: warning: ignoring return value of ‘write’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
76 | if (m > 0) (void)write(STDERR_FILENO, msg, (size_t)m);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
interposer_probe={"ok":true,"error":0,"bits":"0x401c000000000000","allocs":5}
interposer_ok allocs= 5
107
expr='1' n_c=1 n_z=1 sampled_divergences_so_far=0
expr='1+2' n_c=3 n_z=3 sampled_divergences_so_far=0
expr='1+2*3' n_c=5 n_z=5 sampled_divergences_so_far=0
expr='(1+2)*3' n_c=5 n_z=5 sampled_divergences_so_far=0
expr='sin(0)' n_c=2 n_z=2 sampled_divergences_so_far=0
expr='fac(5)' n_c=2 n_z=2 sampled_divergences_so_far=0
expr='ncr(10,3)' n_c=3 n_z=3 sampled_divergences_so_far=0
expr='atan2(1,1)' n_c=3 n_z=3 sampled_divergences_so_far=0
expr='((((1))))' n_c=1 n_z=1 sampled_divergences_so_far=0
expr='1+(2+(3+(4+5)))' n_c=9 n_z=9 sampled_divergences_so_far=0
expr='pi*e' n_c=3 n_z=3 sampled_divergences_so_far=0
expr='abs(-3)+floor(2.7)' n_c=6 n_z=6 sampled_divergences_so_far=0
expr='(' n_c=1 n_z=0 sampled_divergences_so_far=1
expr='1+' n_c=3 n_z=1 sampled_divergences_so_far=3
expr='sin(' n_c=2 n_z=0 sampled_divergences_so_far=5
expr='unknown(1)' n_c=1 n_z=0 sampled_divergences_so_far=6
expr='1,,2' n_c=3 n_z=1 sampled_divergences_so_far=8
expr='((((((1+2)*3)/4)^2)%3)' n_c=11 n_z=11 sampled_divergences_so_far=8
expr='1)' n_c=1 n_z=1 sampled_divergences_so_far=8
expr='(1' n_c=1 n_z=1 sampled_divergences_so_far=8
expr='1**1' n_c=3 n_z=1 sampled_divergences_so_far=10
expr='1*2(+4' n_c=3 n_z=3 sampled_divergences_so_far=10
expr='1*2(1+4' n_c=3 n_z=3 sampled_divergences_so_far=10
expr='a+5' n_c=1 n_z=0 sampled_divergences_so_far=11
expr='!+5' n_c=1 n_z=0 sampled_divergences_so_far=12
expr='_a+5' n_c=1 n_z=0 sampled_divergences_so_far=13
expr='#a+5' n_c=1 n_z=0 sampled_divergences_so_far=14
expr='1^^5' n_c=3 n_z=1 sampled_divergences_so_far=16
expr='1**5' n_c=3 n_z=1 sampled_divergences_so_far=18
expr='sin(cos5' n_c=2 n_z=0 sampled_divergences_so_far=20
expr='((1' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='1))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='((1)' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='(((1' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='1)))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='(((1))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='((((1' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='1))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='((((1)))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='(((((1' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='1)))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='(((((1))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='((((((1' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='1))))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='((((((1)))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='(((((((1' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='1)))))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='(((((((1))))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='((((((((1' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='1))))))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='((((((((1)))))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='(((((((((1' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='1)))))))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='(((((((((1))))))))' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='0' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='2' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='3' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='2+3' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='2*3+4' n_c=5 n_z=5 sampled_divergences_so_far=20
expr='(1+2)' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='1,2,3' n_c=5 n_z=5 sampled_divergences_so_far=20
expr='2^3' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='-1' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='-2^2' n_c=4 n_z=4 sampled_divergences_so_far=20
expr='pi' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='e' n_c=1 n_z=1 sampled_divergences_so_far=20
expr='cos(0)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='tan(0)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='sqrt(4)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='abs(-5)' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='npr(10,3)' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='log(100)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='ln(e)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='exp(1)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='pow(2,3)' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='1/0' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='0/0' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='-1/0' n_c=4 n_z=4 sampled_divergences_so_far=20
expr='0/1' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='1.5+2.5' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='sin(pi)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='cos(pi)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='fac(4.8)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='ncr(16,7)' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='npr(20,5)' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='sin(cos(tan(0)))' n_c=4 n_z=4 sampled_divergences_so_far=20
expr='sqrt(sin(0)^2+cos(0)^2)' n_c=10 n_z=10 sampled_divergences_so_far=20
expr='2+3*4' n_c=5 n_z=5 sampled_divergences_so_far=20
expr='1*(2+3)' n_c=5 n_z=5 sampled_divergences_so_far=20
expr='((1+2))' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='(1,2,3)' n_c=5 n_z=5 sampled_divergences_so_far=20
expr='2^3^2' n_c=5 n_z=5 sampled_divergences_so_far=20
expr='-2' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='1-2-3' n_c=5 n_z=5 sampled_divergences_so_far=20
expr='10/2/2' n_c=5 n_z=5 sampled_divergences_so_far=20
expr='1%3' n_c=3 n_z=3 sampled_divergences_so_far=20
expr='1+2+3+4+5' n_c=9 n_z=9 sampled_divergences_so_far=20
expr='sin(1)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='cos(1)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='tan(1)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='asin(0)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='acos(1)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='atan(1)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='sinh(0)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='cosh(0)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='tanh(0)' n_c=2 n_z=2 sampled_divergences_so_far=20
expr='sqrt(2)' n_c=2 n_z=2 sampled_divergences_so_far=20
{
"schema_version": 1,
"subject": "allocfault",
"command_line": "bash tools/allocfault/run.sh",
"git_sha": "b5e302ed15281e3d96c0998e64e0b8aa20afeee0",
"host": "Linux elmengo 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 18 21:54:43 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux",
"started_at_utc": "2026-08-02T05:31:27Z",
"completed_at_utc": "2026-08-02T05:31:37Z",
"asan": true,
"corpus_inputs": 107,
"ordinals_tested": 271,
"behaviour_divergences": 20,
"crashes_c": 0,
"crashes_zig": 0,
"leaks_detected": "see_asan_log",
"ledger_input": "1+2*3",
"ledger_identical": false,
"ledger_c_events": 0,
"ledger_zig_events": 11,
"ledger_claim": "ledgers differ; equivalence claim limited to return value / error / crash at each ordinal",
"track_g_allocator_misuse_criterion": "not_satisfied_negative_result",
"allocator_misuse_events": [],
"oom_error_position_divergences": 20,
"behaviour_divergence_note": "20/20 divergences are C error=-1 (te_compile NULL root / OOM) vs Zig positive error offset; not crashes or success/failure flips"
}
wrote /mnt/d/hackathons/portmortem/tinyexpr.zig/docs/evidence/fragments/allocfault-asan.json
completed_at_utc=2026-08-02T05:31:37Z
Loading
Loading