fuzz-target-every-parse: the §3.2 differential target — three parsers + skip_forward through the doc-hidden surfaces via shared invariants, 66 committed seeds + gitignore negation + README row; two real skip_forward findings fixed inline (division-by-zero/negative-interval wrap on sqlite+mem, i64 gap subtraction overflow on all three engines incl. pg) and registered as PROVENANCE D-38
This commit is contained in:
1 parent
1f6cc0970a
commit
f7c1bd79b0
80 files changed
+1559
-18
No files matched your search
@@ -252,8 +252,8 @@ fn fire_due(sched: &mut ScheduleState, queues: &mut QueueState, state: &State, n
|
|||||||
/// in the future, grid-aligned (the schedule row's advance makes the
|
/// in the future, grid-aligned (the schedule row's advance makes the
|
||||||
/// gap inspectable).
|
/// gap inspectable).
|
||||||
pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 {
|
pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 {
|
||||||
let gap = (now - next_fire_at) as i128;
|
let gap = now as i128 - next_fire_at as i128;
|
||||||
let interval = interval as i128;
|
let interval = interval.max(1) as i128;
|
||||||
let next = (next_fire_at as i128) + (gap / interval + 1) * interval;
|
let next = (next_fire_at as i128) + (gap / interval + 1) * interval;
|
||||||
if next > i64::MAX as i128 {
|
if next > i64::MAX as i128 {
|
||||||
i64::MAX
|
i64::MAX
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ fn next_every_boundary(spec: &str, after: i64) -> Result<i64> {
|
|||||||
/// (the skipped boundaries never enqueue). 128-bit intermediates keep
|
/// (the skipped boundaries never enqueue). 128-bit intermediates keep
|
||||||
/// the arithmetic saturation-safe; the result clamps at `i64::MAX`.
|
/// the arithmetic saturation-safe; the result clamps at `i64::MAX`.
|
||||||
pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 {
|
pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 {
|
||||||
let gap = (now - next_fire_at) as i128;
|
let gap = now as i128 - next_fire_at as i128;
|
||||||
let interval = interval.max(1) as i128;
|
let interval = interval.max(1) as i128;
|
||||||
let next = (next_fire_at as i128) + (gap / interval + 1) * interval;
|
let next = (next_fire_at as i128) + (gap / interval + 1) * interval;
|
||||||
if next > i64::MAX as i128 {
|
if next > i64::MAX as i128 {
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ Cherry-picks: empty at scaffold, append-only forever.
|
|||||||
| D-35 | drop | `Writer::try_acquire` cut (the non-blocking writer-slot try): upstream's embedding harness used it for try-shaped entry points its surface exposed; this engine's seam always blocks (`acquire`) — the writer-slot lease is a *parks-the-writer* lease by design (ADR-007's honest model), so a try-shaped acquire would contradict the posture rather than extend it. The probe tests driving it re-express their properties through `acquire`/`release`/`close` (the engine-reachable surface); the engine-level lease tests (`tx_tests`' lease/parking suite) pin the observable posture end-to-end | `schema.rs` (`Writer::try_acquire` + the three `writer_reader_tests` call sites) | ADR-007 (the caller-held-lease seam: `begin_tx` parks a concurrent writer — no try-shaped begin exists on the contract surface); ADR-012 §2 (nothing on the engine side reaches a try-acquire); wave-3 lint-removal obligation — cut, not suppressed | ours |
|
| D-35 | drop | `Writer::try_acquire` cut (the non-blocking writer-slot try): upstream's embedding harness used it for try-shaped entry points its surface exposed; this engine's seam always blocks (`acquire`) — the writer-slot lease is a *parks-the-writer* lease by design (ADR-007's honest model), so a try-shaped acquire would contradict the posture rather than extend it. The probe tests driving it re-express their properties through `acquire`/`release`/`close` (the engine-reachable surface); the engine-level lease tests (`tx_tests`' lease/parking suite) pin the observable posture end-to-end | `schema.rs` (`Writer::try_acquire` + the three `writer_reader_tests` call sites) | ADR-007 (the caller-held-lease seam: `begin_tx` parks a concurrent writer — no try-shaped begin exists on the contract surface); ADR-012 §2 (nothing on the engine side reaches a try-acquire); wave-3 lint-removal obligation — cut, not suppressed | ours |
|
||||||
| D-36 | drop | `UpdateWatcher::spawn` and `SharedUpdateWatcher::new` cut (the default-cadence convenience constructors): upstream's embedding harnesses opened watchers without explicit config; this engine's single open path always constructs a `WatcherConfig` — from `SqliteOpts` (ADR-023 §4's `poll_interval` knob, `None` = the 1 ms default) — and passes it through `*_with_config`; a config-less twin would re-create two open postures (the mixed-flag inconsistency D-29 resolved at the URI layer). `WatcherConfig::default()` remains the default's single owner; the tests driving the cut constructors now build the config explicitly, pinning the same behavior | `watcher.rs` (both constructors + five watcher-test call sites) | ADR-023 §4 (the cadence knob rides the engine's opts — one open posture carries config); ADR-012 §2 (one construction path the engine reviews); wave-3 lint-removal obligation — cut, not suppressed | ours |
|
| D-36 | drop | `UpdateWatcher::spawn` and `SharedUpdateWatcher::new` cut (the default-cadence convenience constructors): upstream's embedding harnesses opened watchers without explicit config; this engine's single open path always constructs a `WatcherConfig` — from `SqliteOpts` (ADR-023 §4's `poll_interval` knob, `None` = the 1 ms default) — and passes it through `*_with_config`; a config-less twin would re-create two open postures (the mixed-flag inconsistency D-29 resolved at the URI layer). `WatcherConfig::default()` remains the default's single owner; the tests driving the cut constructors now build the config explicitly, pinning the same behavior | `watcher.rs` (both constructors + five watcher-test call sites) | ADR-023 §4 (the cadence knob rides the engine's opts — one open posture carries config); ADR-012 §2 (one construction path the engine reviews); wave-3 lint-removal obligation — cut, not suppressed | ours |
|
||||||
| D-37 | re-derivation | visibility-only widening on the re-derived `@every` surface: `parse_every_interval` (`pub(crate)` → `pub`) and `skip_forward` (module-private → `pub`) in `queue_ops.rs`, plus this subtree's facade re-export (`mod.rs`: `pub use queue_ops::{parse_every_interval, skip_forward}`), so the engine crate root can `#[doc(hidden)] pub use` both as the wave-7 fuzzing differential target's drive surface (`fuzz-parser-exposure`, target `every_parse`). Zero logic change, zero signature change, zero name change; `mod substrate` stays private, so containment holds — nothing leaves the crate except the doc-hidden root re-exports. A `pub use` re-export mechanically requires item-level `pub` (E0364/E0603), which is why the facade line is in `mod.rs`; the module doc's "nothing re-exported" boundary sentence was amended in the same commit to name this sole exception | `queue_ops.rs` (two visibility keywords); `mod.rs` (facade re-export line + boundary-doc exception sentence) | ADR-012 §3 (fidelity governs the ported honker body — `watcher.rs`/`schema.rs`/`ops.rs` untouched; `queue_ops.rs` is the re-derived half, owned contract-v1 code, so a visibility-only delta is legitimate); docs/research/fuzzing.md §3.2 (option (b): the `#[doc(hidden)]` narrow exposure); ADR-017 §3 (doc-hidden test surface, no stability guarantee — the `Job::from_row` precedent) | ours |
|
| D-37 | re-derivation | visibility-only widening on the re-derived `@every` surface: `parse_every_interval` (`pub(crate)` → `pub`) and `skip_forward` (module-private → `pub`) in `queue_ops.rs`, plus this subtree's facade re-export (`mod.rs`: `pub use queue_ops::{parse_every_interval, skip_forward}`), so the engine crate root can `#[doc(hidden)] pub use` both as the wave-7 fuzzing differential target's drive surface (`fuzz-parser-exposure`, target `every_parse`). Zero logic change, zero signature change, zero name change; `mod substrate` stays private, so containment holds — nothing leaves the crate except the doc-hidden root re-exports. A `pub use` re-export mechanically requires item-level `pub` (E0364/E0603), which is why the facade line is in `mod.rs`; the module doc's "nothing re-exported" boundary sentence was amended in the same commit to name this sole exception | `queue_ops.rs` (two visibility keywords); `mod.rs` (facade re-export line + boundary-doc exception sentence) | ADR-012 §3 (fidelity governs the ported honker body — `watcher.rs`/`schema.rs`/`ops.rs` untouched; `queue_ops.rs` is the re-derived half, owned contract-v1 code, so a visibility-only delta is legitimate); docs/research/fuzzing.md §3.2 (option (b): the `#[doc(hidden)]` narrow exposure); ADR-017 §3 (doc-hidden test surface, no stability guarantee — the `Job::from_row` precedent) | ours |
|
||||||
|
| D-38 | port delta | degenerate/adversarial-triple hardening on `skip_forward` (two legs, both aligning to the pg engine's committed clamp shape where it existed and extending pg itself): (1) the `interval` is clamped (`interval.max(1) as i128`) — pre-fix, `interval == 0` panicked on i128 division-by-zero, reachable only through degenerate/non-grammar callers (the parsers reject `n <= 0`, so every engine-internal call site passes `interval >= 1`); (2) the gap is computed in i128 (`now as i128 - next_fire_at as i128`) — pre-fix, `now - next_fire_at` was an **i64 subtraction** that panics in debug (overflow) / silently wraps in release when `(next_fire_at, now)` straddle the full domain (`now=i64::MIN`-adjacent, `next_fire_at=i64::MAX`-adjacent), feeding a wrapped gap to the 128-bit arithmetic and breaking the saturation guarantee. Found by the `every_parse` differential target (wave 7, tasks/fuzz-target-every-parse) driving the adversarial `(next_fire_at, now, interval)` space; pg needed leg (2) as well. Observable behavior unaffected in the reachable domain (|gap| < i64 range), saturation posture now holds over the whole domain. Zero signature change; lineage body untouched (`skip_forward` is the re-derived scheduler half, D-12's own) | `queue_ops.rs` (`skip_forward`, two lines) | docs/research/fuzzing.md §3.2 (the overflow-posture invariant: reject-or-saturate, never wrap or panic, over the whole domain); wave-6's register-overflow alignment (tasks/review-wave-6); alkstore-postgres/src/scheduler.rs `skip_forward` (the pinned clamp shape) | ours |
|
||||||
|
|
||||||
### Cherry-picks
|
### Cherry-picks
|
||||||
|
|
||||||
|
|||||||
@@ -525,8 +525,8 @@ fn next_every_boundary(spec: &str, after: i64) -> Result<i64, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 {
|
pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 {
|
||||||
let gap = (now - next_fire_at) as i128;
|
let gap = now as i128 - next_fire_at as i128;
|
||||||
let interval = interval as i128;
|
let interval = interval.max(1) as i128;
|
||||||
let next = (next_fire_at as i128) + (gap / interval + 1) * interval;
|
let next = (next_fire_at as i128) + (gap / interval + 1) * interval;
|
||||||
if next > i64::MAX as i128 {
|
if next > i64::MAX as i128 {
|
||||||
i64::MAX
|
i64::MAX
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
target
|
target
|
||||||
corpus/*
|
corpus/*
|
||||||
!corpus/payload_codec
|
!corpus/payload_codec
|
||||||
|
!corpus/every_parse
|
||||||
artifacts
|
artifacts
|
||||||
coverage
|
coverage
|
||||||
Generated
+955
-3
File diff suppressed because it is too large.
Load diff
@@ -21,4 +21,11 @@ test = false
|
|||||||
doc = false
|
doc = false
|
||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "every_parse"
|
||||||
|
path = "fuzz_targets/every_parse.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
+1
-1
@@ -28,7 +28,7 @@ libFuzzer targets for alkstore's parse/codec surfaces (see
|
|||||||
| Target | Drives | Invariants |
|
| Target | Drives | Invariants |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `payload_codec` | `encode_payload` / `payload_as::<T>` (`alkstore/src/payload.rs`) | no-panic on any byte sequence; every decode failure typed `Error::Codec`; deep nesting loud at serde_json's recursion limit; `Value` round-trip (serde_json canonicalization pinned); typed round-trip for a representative payload set |
|
| `payload_codec` | `encode_payload` / `payload_as::<T>` (`alkstore/src/payload.rs`) | no-panic on any byte sequence; every decode failure typed `Error::Codec`; deep nesting loud at serde_json's recursion limit; `Value` round-trip (serde_json canonicalization pinned); typed round-trip for a representative payload set |
|
||||||
| `every_parse` | the three `parse_every_interval` implementations (differential) | filled by its target task |
|
| `every_parse` | the three `parse_every_interval` implementations (differential, through the engines' `#[doc(hidden)]` re-exports) + each engine's `skip_forward` boundary arithmetic | no-panic on any UTF-8-lossy string (each parser, each boundary path); typed rejection (`Error::InvalidSpec` on pg/mem, the substrate's error under its mapping on SQLite); three-way agreement on the Ok/Err partition and the `Ok` interval value (error identity not pinned — the SQLite `Result<i64, String>` vs `InvalidSpec` asymmetry is known); overflow posture: `skip_forward` over adversarial `(next_fire_at, now, interval)` triples rejects-or-saturates, never wraps or panics; the `n <= 0` and giant-`n` arms (leading zeros, digit overflow at exactly `i64::MAX/86400`, positive-zero rejection) |
|
||||||
| `name_validation` | `validate_name` / `validate_shared_name` / `validate_local_name` (`alkstore/src/validation.rs`) | filled by its target task |
|
| `name_validation` | `validate_name` / `validate_shared_name` / `validate_local_name` (`alkstore/src/validation.rs`) | filled by its target task |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 1s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 9s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 17s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 42m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 1m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 90m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 2h
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 24h
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 1d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 7d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 365d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 007s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 007m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 000000042h
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 007d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 0s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 00m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 0000d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every -5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every -0s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 9223372036854775807s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 9223372036854775806s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 9223372036854775808s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 9223372036854775807m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 9223372036854775807h
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 9223372036854775807d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 18446744073709551616s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 18446744073709551616000000m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 106751991167300d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 106751991167301d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 106751991167299d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 2147483647d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 4294967295d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 100000000000d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5 s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5sx
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5S
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every +5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every .5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 3.5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5.0s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5 s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every és
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ÿ @every ÿ 5s þ
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5sÿ
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@once
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@hourly
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@daily
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@weekly
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*/5 * * * *
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0 0 * * *
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
every 5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
5s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 5
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@every 9223372036854775807s skip-forward-triple payload XXXXX
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
������������������������
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
//! The §3.2 target: the three per-engine `@every` parsers driven
|
||||||
|
//! differentially through the `fuzz-parser-exposure` doc-hidden
|
||||||
|
//! surfaces, plus each engine's `skip_forward` boundary arithmetic.
|
||||||
|
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
alkstore_fuzz_shared::every_parse::fuzz_every_parse(data);
|
||||||
|
});
|
||||||
@@ -124,8 +124,130 @@ def payload_codec_seeds():
|
|||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
|
def every_parse_seeds():
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
# Every grammar-valid unit form (s|m|h|d), single and multi-digit.
|
||||||
|
for spec in [
|
||||||
|
"@every 1s",
|
||||||
|
"@every 5s",
|
||||||
|
"@every 9s",
|
||||||
|
"@every 17s",
|
||||||
|
"@every 42m",
|
||||||
|
"@every 1m",
|
||||||
|
"@every 90m",
|
||||||
|
"@every 2h",
|
||||||
|
"@every 24h",
|
||||||
|
"@every 1d",
|
||||||
|
"@every 7d",
|
||||||
|
"@every 365d",
|
||||||
|
]:
|
||||||
|
write_seed("every_parse", i, spec.encode())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Leading-zero forms (parsed through, value = the digits).
|
||||||
|
for spec in ["@every 007s", "@every 007m", "@every 000000042h", "@every 007d"]:
|
||||||
|
write_seed("every_parse", i, spec.encode())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# The n <= 0 arms: positive zero (with and without leading zeros)
|
||||||
|
# and the negative shapes (digits == 0 inside the grammar).
|
||||||
|
for spec in ["@every 0s", "@every 00m", "@every 0000d", "@every -5s", "@every -0s"]:
|
||||||
|
write_seed("every_parse", i, spec.encode())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# The i64::MAX neighborhood: giant-n accept at scale-1, digit
|
||||||
|
# overflow past i64, and unit-multiplier overflow above it.
|
||||||
|
for spec in [
|
||||||
|
"@every 9223372036854775807s",
|
||||||
|
"@every 9223372036854775806s",
|
||||||
|
"@every 9223372036854775808s",
|
||||||
|
"@every 9223372036854775807m",
|
||||||
|
"@every 9223372036854775807h",
|
||||||
|
"@every 9223372036854775807d",
|
||||||
|
"@every 18446744073709551616s",
|
||||||
|
"@every 18446744073709551616000000m",
|
||||||
|
]:
|
||||||
|
write_seed("every_parse", i, spec.encode())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# The digit-overflow boundary at exactly i64::MAX/86400 (and ±1).
|
||||||
|
for spec in [
|
||||||
|
"@every 106751991167300d",
|
||||||
|
"@every 106751991167301d",
|
||||||
|
"@every 106751991167299d",
|
||||||
|
]:
|
||||||
|
write_seed("every_parse", i, spec.encode())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# d-unit multiplier blowups.
|
||||||
|
for spec in ["@every 2147483647d", "@every 4294967295d", "@every 100000000000d"]:
|
||||||
|
write_seed("every_parse", i, spec.encode())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Whitespace / garbage / truncation shapes.
|
||||||
|
for raw in [
|
||||||
|
b"@every",
|
||||||
|
b"@every ",
|
||||||
|
b"@every 5s",
|
||||||
|
b"@every 5s ",
|
||||||
|
b"@every\t5s",
|
||||||
|
b"@every 5 s",
|
||||||
|
b"@every 5sx",
|
||||||
|
b"@every 5S",
|
||||||
|
b"@every +5s",
|
||||||
|
b"@every .5s",
|
||||||
|
b"@every 3.5s",
|
||||||
|
b"@every 5.0s",
|
||||||
|
b"@every 5\ts",
|
||||||
|
b"@every \xc3\xa9s",
|
||||||
|
b"\xff @every \xff 5s \xfe",
|
||||||
|
b"@every 5s\xff",
|
||||||
|
]:
|
||||||
|
write_seed("every_parse", i, raw)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Non-grammar prefixes and shapes (cron / @once adjacency).
|
||||||
|
for raw in [
|
||||||
|
b"@once",
|
||||||
|
b"@hourly",
|
||||||
|
b"@daily",
|
||||||
|
b"@weekly",
|
||||||
|
b"*/5 * * * *",
|
||||||
|
b"0 0 * * *",
|
||||||
|
b"every 5s",
|
||||||
|
b"5s",
|
||||||
|
b"@every 5",
|
||||||
|
b"@every s",
|
||||||
|
]:
|
||||||
|
write_seed("every_parse", i, raw)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Byte payloads ≥ 24 bytes: the boundary-arithmetic leg folds
|
||||||
|
# adversarial (next_fire_at, now, interval) triples straight out of
|
||||||
|
# the bytes — the i64::MAX neighborhood, negative shapes, d-blowups.
|
||||||
|
for raw in [
|
||||||
|
b"@every 9223372036854775807s skip-forward-triple payload XXXXX",
|
||||||
|
b"\x00" * 24,
|
||||||
|
b"\xff" * 24,
|
||||||
|
bytes([
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, # next = i64::MAX
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, # now = 2^62
|
||||||
|
0x80, 0xC8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, # interval = 250000
|
||||||
|
]),
|
||||||
|
bytes([
|
||||||
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, # next = i64::MIN + 1
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, # now = i64::MAX
|
||||||
|
0x80, 0x92, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, # interval = negative
|
||||||
|
]),
|
||||||
|
]:
|
||||||
|
write_seed("every_parse", i, raw)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
payload_codec_seeds()
|
payload_codec_seeds()
|
||||||
|
every_parse_seeds()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
alkstore = { path = "../../alkstore" }
|
alkstore = { path = "../../alkstore" }
|
||||||
|
alkstore-sqlite = { path = "../../alkstore-sqlite" }
|
||||||
|
alkstore-postgres = { path = "../../alkstore-postgres" }
|
||||||
|
alkstore-mem = { path = "../../alkstore-mem" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
//! Invariant logic for the `every_parse` target (docs/research/
|
||||||
|
//! fuzzing.md §3.2): the three one-owner `@every` parsers (ADR-009 §2's
|
||||||
|
//! grammar, ADR-012 §2's one-owner rule) driven **differentially**
|
||||||
|
//! through the engines' `#[doc(hidden)]` re-exports — no-panic on any
|
||||||
|
//! string, typed rejection, three-way agreement (the Ok/Err partition
|
||||||
|
//! and the `Ok` value; error *identity* is deliberately not pinned —
|
||||||
|
//! the SQLite surface's `Result<i64, String>` vs pg/mem's
|
||||||
|
//! `Result<i64>`/`InvalidSpec` is a known, typed asymmetry), the
|
||||||
|
//! overflow posture, and the `n <= 0` / giant-`n` arms.
|
||||||
|
|
||||||
|
use alkstore::Error;
|
||||||
|
use alkstore_mem as mem_engine;
|
||||||
|
use alkstore_postgres as pg_engine;
|
||||||
|
use alkstore_sqlite as sqlite_engine;
|
||||||
|
|
||||||
|
pub fn fuzz_every_parse(data: &[u8]) {
|
||||||
|
let spec = String::from_utf8_lossy(data);
|
||||||
|
assert_parse_agreement(spec.as_ref());
|
||||||
|
assert_parse_battery();
|
||||||
|
assert_skip_battery();
|
||||||
|
assert_skip_triples(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// pg and mem reject grammar-violating shapes with their own typed
|
||||||
|
/// error, `Error::InvalidSpec` — never a panic, never a silent `Ok`
|
||||||
|
/// on a shape the authored tests reject.
|
||||||
|
fn assert_typed_rejection(spec: &str, engine: &str) {
|
||||||
|
let result = match engine {
|
||||||
|
"pg" => pg_engine::parse_every_interval(spec),
|
||||||
|
_ => mem_engine::parse_every_interval(spec),
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Ok(_) | Err(Error::InvalidSpec { .. }) => {}
|
||||||
|
Err(other) => panic!("{engine} rejects must be `InvalidSpec`, got {other:?} for {spec:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One invocation of the three parsers over a lossy-decoded spec:
|
||||||
|
/// three-way agreement on the Ok/Err partition and the `Ok` value
|
||||||
|
/// (SQLite's `Err` payloads are `String`s — the accepted asymmetry — so
|
||||||
|
/// the SQLite comparison is partition-and-value only).
|
||||||
|
fn assert_parse_agreement(spec: &str) {
|
||||||
|
assert_typed_rejection(spec, "pg");
|
||||||
|
assert_typed_rejection(spec, "mem");
|
||||||
|
let sqlite_result = sqlite_engine::parse_every_interval(spec).map_err(|_| ());
|
||||||
|
let pg_result = pg_engine::parse_every_interval(spec).map_err(|_| ());
|
||||||
|
let mem_result = mem_engine::parse_every_interval(spec).map_err(|_| ());
|
||||||
|
assert_eq!(
|
||||||
|
pg_result, mem_result,
|
||||||
|
"pg and mem must agree on the Ok/Err partition for {spec:?}"
|
||||||
|
);
|
||||||
|
match pg_result {
|
||||||
|
Ok(interval) => {
|
||||||
|
assert_eq!(
|
||||||
|
sqlite_result,
|
||||||
|
Ok(interval),
|
||||||
|
"sqlite must accept {spec:?} with pg/mem's interval"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(()) => {
|
||||||
|
assert_eq!(
|
||||||
|
sqlite_result,
|
||||||
|
Err(()),
|
||||||
|
"sqlite must reject what pg/mem reject for {spec:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Engine = (&'static str, fn(&str) -> Result<i64, ()>);
|
||||||
|
|
||||||
|
fn engines() -> Vec<Engine> {
|
||||||
|
vec![
|
||||||
|
("sqlite", |s: &str| {
|
||||||
|
sqlite_engine::parse_every_interval(s).map_err(|_| ())
|
||||||
|
}),
|
||||||
|
("pg", |s: &str| {
|
||||||
|
pg_engine::parse_every_interval(s).map_err(|_| ())
|
||||||
|
}),
|
||||||
|
("mem", |s: &str| {
|
||||||
|
mem_engine::parse_every_interval(s).map_err(|_| ())
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The grammar battery: shapes the authored tests already reject/accept
|
||||||
|
/// — pinned on every invocation so neither the fuzz path nor a corpus
|
||||||
|
/// replay ever observes drift (a silent `Ok` on a known-bad shape, a
|
||||||
|
/// changed interval value, a lost reject). Giant-`n` arms: the
|
||||||
|
/// `9223372036854775807` neighborhood and digit overflow at exactly
|
||||||
|
/// `i64::MAX/86400`.
|
||||||
|
fn assert_parse_battery() {
|
||||||
|
for (spec, want) in accept_battery() {
|
||||||
|
for (engine_name, engine) in engines() {
|
||||||
|
assert_eq!(
|
||||||
|
engine(spec),
|
||||||
|
Ok(want),
|
||||||
|
"{engine_name} must accept the grammar-valid {spec:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for spec in reject_battery() {
|
||||||
|
for (engine_name, engine) in engines() {
|
||||||
|
assert!(
|
||||||
|
engine(spec).is_err(),
|
||||||
|
"{engine_name} must reject the grammar-violating {spec:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_battery() -> Vec<(&'static str, i64)> {
|
||||||
|
vec![
|
||||||
|
("@every 1s", 1),
|
||||||
|
("@every 5s", 5),
|
||||||
|
("@every 9s", 9),
|
||||||
|
("@every 17s", 17),
|
||||||
|
("@every 42m", 2520),
|
||||||
|
("@every 1m", 60),
|
||||||
|
("@every 90m", 5400),
|
||||||
|
("@every 2h", 7200),
|
||||||
|
("@every 24h", 86_400),
|
||||||
|
("@every 1d", 86_400),
|
||||||
|
("@every 7d", 604_800),
|
||||||
|
("@every 007s", 7),
|
||||||
|
("@every 007m", 420),
|
||||||
|
("@every 000000042h", 151_200),
|
||||||
|
("@every 106751991167300d", 9_223_372_036_854_720_000),
|
||||||
|
("@every 9223372036854775807s", i64::MAX),
|
||||||
|
("@every 9223372036854775806s", i64::MAX - 1),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reject_battery() -> Vec<&'static str> {
|
||||||
|
vec![
|
||||||
|
"",
|
||||||
|
"@every",
|
||||||
|
"@every ",
|
||||||
|
"@every 0s",
|
||||||
|
"@every -5s",
|
||||||
|
"@every +5s",
|
||||||
|
"@every 5",
|
||||||
|
"@every s",
|
||||||
|
"@every 5sx",
|
||||||
|
"@every 5S",
|
||||||
|
"@every 5s",
|
||||||
|
"@every 5s ",
|
||||||
|
"@every 3.5s",
|
||||||
|
"@every 9223372036854775808s",
|
||||||
|
"@every 9223372036854775807m",
|
||||||
|
"@every 106751991167301d",
|
||||||
|
"@every 18446744073709551616s",
|
||||||
|
"@every 00m",
|
||||||
|
"every 5s",
|
||||||
|
"5s",
|
||||||
|
"*/5 * * * *",
|
||||||
|
"0 0 * * *",
|
||||||
|
"@once",
|
||||||
|
"@hourly",
|
||||||
|
"@daily",
|
||||||
|
"@weekly",
|
||||||
|
"@minute",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The overflow-posture battery for the next-boundary arithmetic:
|
||||||
|
/// adversarial `(next_fire_at, now, interval)` triples (the `i64::MAX`
|
||||||
|
/// neighborhood, negative shapes, `d`-unit multiplier blowups) —
|
||||||
|
/// reject-or-saturate semantics, never wrapping or panicking. The
|
||||||
|
/// three implementations are asserted equal over the whole domain
|
||||||
|
/// (post-clamp they carry pg's pinned shape identically).
|
||||||
|
fn assert_skip_battery() {
|
||||||
|
for &(next_fire_at, now, interval) in TRIPLES {
|
||||||
|
assert_skip_posture(next_fire_at, now, interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRIPLES: &[(i64, i64, i64)] = &[
|
||||||
|
(0, 0, 0),
|
||||||
|
(0, 0, 1),
|
||||||
|
(0, 1, 1),
|
||||||
|
(5, 4, 60),
|
||||||
|
(5, 3, 60),
|
||||||
|
(i64::MAX, i64::MAX, 1),
|
||||||
|
(i64::MAX - 5, i64::MAX, 1),
|
||||||
|
(i64::MAX - 100, i64::MAX, 86_400),
|
||||||
|
(i64::MAX - 100, i64::MAX, 86_400 * 30),
|
||||||
|
(i64::MAX / 2, i64::MAX, i64::MAX / 2),
|
||||||
|
(i64::MAX / 2, i64::MAX, i64::MAX),
|
||||||
|
(i64::MAX, i64::MAX, i64::MAX),
|
||||||
|
(i64::MIN, i64::MIN, 1),
|
||||||
|
(i64::MIN, i64::MIN, 86_400),
|
||||||
|
(i64::MIN, i64::MAX, 1),
|
||||||
|
(i64::MIN, i64::MAX, 86_400 * 30),
|
||||||
|
(i64::MIN, 0, 86_400),
|
||||||
|
(0, i64::MAX, 86_400 * 365),
|
||||||
|
(5, 4, 0),
|
||||||
|
(5, 4, -60),
|
||||||
|
(i64::MAX, i64::MIN, -1),
|
||||||
|
(i64::MIN, i64::MAX, i64::MIN),
|
||||||
|
(0, i64::MAX, i64::MIN),
|
||||||
|
(86_400, 86_399, 86_400 * u32::MAX as i64),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn assert_skip_posture(next_fire_at: i64, now: i64, interval: i64) {
|
||||||
|
let a = sqlite_engine::skip_forward(next_fire_at, now, interval);
|
||||||
|
let b = pg_engine::skip_forward(next_fire_at, now, interval);
|
||||||
|
let c = mem_engine::skip_forward(next_fire_at, now, interval);
|
||||||
|
assert_eq!(
|
||||||
|
a, b,
|
||||||
|
"sqlite and pg skip-forward must agree for ({next_fire_at}, {now}, {interval})"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
b, c,
|
||||||
|
"pg and mem skip-forward must agree for ({next_fire_at}, {now}, {interval})"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Saturation: the result is either the `i64::MAX` clamp or a value
|
||||||
|
// strictly beyond `now` — no wrap into the past, ever.
|
||||||
|
assert!(
|
||||||
|
a == i64::MAX || a > now,
|
||||||
|
"skip-forward must land strictly beyond `now` or saturate: got {a} for ({next_fire_at}, {now}, {interval})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adversarial triples folded straight out of the input bytes — the
|
||||||
|
/// whole `(next_fire_at, now, interval)` domain through the fuzz path
|
||||||
|
/// and every ≥24-byte corpus seed.
|
||||||
|
fn assert_skip_triples(data: &[u8]) {
|
||||||
|
let (chunks, remainder) = data.as_chunks::<24>();
|
||||||
|
for chunk in chunks {
|
||||||
|
assert_skip_triple(chunk);
|
||||||
|
}
|
||||||
|
let mut tail = [0u8; 24];
|
||||||
|
tail[..remainder.len()].copy_from_slice(remainder);
|
||||||
|
assert_skip_triple(&tail);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_skip_triple(chunk: &[u8]) {
|
||||||
|
let next_fire_at = i64::from_le_bytes(chunk[..8].try_into().expect("slice is 8 bytes"));
|
||||||
|
let now = i64::from_le_bytes(chunk[8..16].try_into().expect("slice is 8 bytes"));
|
||||||
|
let interval = i64::from_le_bytes(chunk[16..24].try_into().expect("slice is 8 bytes"));
|
||||||
|
assert_skip_posture(next_fire_at, now, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod corpus_replay {
|
||||||
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn committed_corpus_replays_through_the_invariants() {
|
||||||
|
let corpus = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../corpus/every_parse");
|
||||||
|
let mut count = 0usize;
|
||||||
|
for entry in std::fs::read_dir(&corpus).expect("corpus directory is committed") {
|
||||||
|
let path = entry.expect("corpus entry readable").path();
|
||||||
|
let data = std::fs::read(&path).expect("corpus entry readable");
|
||||||
|
fuzz_every_parse(&data);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
count >= 48,
|
||||||
|
"committed seed corpus is present, found {count}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invariants_hold_without_corpus() {
|
||||||
|
fuzz_every_parse(b"");
|
||||||
|
fuzz_every_parse(b"@every 5s");
|
||||||
|
fuzz_every_parse(b"@every 106751991167301d");
|
||||||
|
fuzz_every_parse(b"\xff @every \xff 5s \xfe");
|
||||||
|
fuzz_every_parse(&[0u8; 24]);
|
||||||
|
fuzz_every_parse(&(i64::MIN as i128).to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,4 +4,5 @@
|
|||||||
//! CI pattern) without a nightly toolchain.
|
//! CI pattern) without a nightly toolchain.
|
||||||
|
|
||||||
pub mod arbitrary_value;
|
pub mod arbitrary_value;
|
||||||
|
pub mod every_parse;
|
||||||
pub mod payload_codec;
|
pub mod payload_codec;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: fuzz-target-every-parse
|
id: fuzz-target-every-parse
|
||||||
name: Fuzz target — `every_parse` (the three `@every` parsers, differential)
|
name: Fuzz target — `every_parse` (the three `@every` parsers, differential)
|
||||||
status: pending
|
status: completed
|
||||||
depends_on: [fuzz-scaffold, fuzz-parser-exposure]
|
depends_on: [fuzz-scaffold, fuzz-parser-exposure]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
risk: medium
|
risk: medium
|
||||||
@@ -73,17 +73,23 @@ escalate), not a task failure.
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- [ ] `fuzz_targets/every_parse.rs` + `shared/src/every_parse.rs`
|
- [x] `fuzz_targets/every_parse.rs` + `shared/src/every_parse.rs`
|
||||||
exist; `cargo fuzz build` builds the target
|
exist; `cargo fuzz build` builds the target
|
||||||
- [ ] All five §3.2 invariant classes implemented and exercised by both
|
- [x] All five §3.2 invariant classes implemented and exercised by both
|
||||||
the fuzz path and the replay module; the three-way agreement
|
the fuzz path and the replay module; the three-way agreement
|
||||||
asserted on every input (fuzz) and every seed (replay)
|
asserted on every input (fuzz) and every seed (replay)
|
||||||
- [ ] Seed corpus committed, deterministic-generated, gitignore
|
- [x] Seed corpus committed, deterministic-generated, gitignore
|
||||||
negation in place
|
negation in place
|
||||||
- [ ] `cargo test --manifest-path fuzz/shared/Cargo.toml` green;
|
- [x] `cargo test --manifest-path fuzz/shared/Cargo.toml` green;
|
||||||
`shared` clippy/fmt gates green
|
`shared` clippy/fmt gates green
|
||||||
- [ ] Root workspace gates still green; substrate diff still empty
|
- [x] Root workspace gates still green; substrate diff still empty
|
||||||
- [ ] Any three-way divergence found during target development is
|
*(amended at implementation, 2026-10-10 — see Notes: the target
|
||||||
|
found a real no-panic/overflow-posture finding in `skip_forward`,
|
||||||
|
dispositioned inline per the task's own finding posture; the
|
||||||
|
substrate delta is two keyword-free lines in the re-derived
|
||||||
|
`queue_ops.rs`, registered as PROVENANCE D-38; lineage body
|
||||||
|
untouched)*
|
||||||
|
- [x] Any three-way divergence found during target development is
|
||||||
recorded in Notes with its disposition (fixed / escalated), not
|
recorded in Notes with its disposition (fixed / escalated), not
|
||||||
silently absorbed
|
silently absorbed
|
||||||
|
|
||||||
@@ -101,8 +107,106 @@ escalate), not a task failure.
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
> To be filled by implementation agent
|
Decisions of record the implementation made that the description didn't
|
||||||
|
pin:
|
||||||
|
|
||||||
|
- **Two real findings, both in `skip_forward` (the boundary leg),
|
||||||
|
dispositioned fixed inline** (per this task's risk note, the
|
||||||
|
campaign task's posture — small crate-side fixes ride the task that
|
||||||
|
finds them):
|
||||||
|
1. **Division-by-zero panic + negative-`interval` under-wrap** —
|
||||||
|
SQLite and mem's `skip_forward` cast `interval` straight into
|
||||||
|
`i128` (pg had `interval.max(1)`); `interval == 0` panicked on
|
||||||
|
i128 division, and a negative `interval` skipped the high-side
|
||||||
|
guard so `next < i64::MIN` wrapped silently. Fix: the clamp, pg's
|
||||||
|
committed shape. With `interval >= 1` the under-flow path is
|
||||||
|
unreachable, so the fix is exactly pg's body — no extra guard was
|
||||||
|
added beyond it.
|
||||||
|
2. **i64 gap subtraction before the i128 cast (all three engines,
|
||||||
|
including pg)** — `(now - next_fire_at) as i128` computed the
|
||||||
|
difference in i64: panics (debug overflow) / wraps silently
|
||||||
|
(release) when `(next_fire_at, now)` straddle the full domain
|
||||||
|
(`now = i64::MIN`-adjacent, `next_fire_at = i64::MAX`-adjacent).
|
||||||
|
The 128-bit intermediates story had a hole at its first line. Fix:
|
||||||
|
`now as i128 - next_fire_at as i128` — same one line, all three.
|
||||||
|
Observable reachability: both pre-fix paths are reachable only
|
||||||
|
outside the parsers' grammar domain (`n <= 0` rejected ⇒ callers
|
||||||
|
pass `interval >= 1`; `|gap| < i64` range in every clock-domain
|
||||||
|
call) — no engine-internal behavior change; the invariant now holds
|
||||||
|
over the whole domain. SQLite's delta registered as
|
||||||
|
`substrate/PROVENANCE.md` **D-38** (queue_ops.rs is the re-derived
|
||||||
|
half, D-12's own — D-31/D-37's precedent for owned-code deltas in
|
||||||
|
the register); mem needs no provenance entry (engine-owned
|
||||||
|
`scheduler.rs`), and pg's one-line delta is engine-owned likewise.
|
||||||
|
- **(a)-companion leg not built.** The optional `schedule()`-driven
|
||||||
|
register-posture leg stayed unbuilt: the register-path posture (parse
|
||||||
|
→ first-boundary `checked_add` → reject `InvalidSpec`) is suite-pinned
|
||||||
|
already, and driving the mem factory from `shared` would pull a
|
||||||
|
tokio runtime into the stable gate crate for one redundant leg.
|
||||||
|
- **`skip_forward` three-way equality asserted, not just no-panic.**
|
||||||
|
Post-fix the three bodies are lexically identical, so the target
|
||||||
|
goes further than the task text required: three-way output
|
||||||
|
equality on *every* `(next_fire_at, now, interval)` triple (fixed
|
||||||
|
adversarial battery + triples folded out of the input bytes at
|
||||||
|
24-byte stride), plus the saturation pin
|
||||||
|
(`i64::MAX` clamp or strictly-beyond-`now`, never a past value).
|
||||||
|
- **Error identity left unpinned as specified**: SQLite's
|
||||||
|
`Result<i64, String>` vs pg/mem's `InvalidSpec` — the differential
|
||||||
|
pins partition-and-value; the typed-rejection check (pg/mem must
|
||||||
|
reject with `Error::InvalidSpec`, never another variant) runs ahead
|
||||||
|
of the agreement assert.
|
||||||
|
- **Corpus growth noise after the smoke campaign**: a 180 s detached
|
||||||
|
smoke (`run-detached.sh every_parse -max_len=4096
|
||||||
|
-use_value_profile=1`, ~737k execs, 0 crash/oom/timeout — the
|
||||||
|
campaign task re-runs the full budget) grew `corpus/every_parse` to
|
||||||
|
686 files; the grown additions were pruned back to the 66 committed
|
||||||
|
generator seeds since corpus-policy reconciliation
|
||||||
|
(grown-corpus disposition, the seed-promotion posture) is the
|
||||||
|
campaign task's named AC. The `!corpus/every_parse` negation does
|
||||||
|
un-ignore grown additions in that dir — the same shape alksocks
|
||||||
|
shipped, so the tension is the campaign task's to resolve, noted
|
||||||
|
here for context.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
> To be filled on completion
|
Landed the `every_parse` differential target (2026-10-10): the §3.2
|
||||||
|
surface — all three one-owner `parse_every_interval` implementations
|
||||||
|
driven through the `fuzz-parser-exposure` `#[doc(hidden)]` re-exports,
|
||||||
|
plus all three `skip_forward` boundary postures.
|
||||||
|
|
||||||
|
- **`fuzz/shared/src/every_parse.rs`** — the invariant logic (stable
|
||||||
|
toolchain, replayable): typed-rejection pin (pg/mem rejects are
|
||||||
|
`Error::InvalidSpec` only), three-way agreement on partition +
|
||||||
|
`Ok` value for arbitrary lossy-decoded strings, a fixed grammar
|
||||||
|
battery (17 accept shapes with pinned interval values incl.
|
||||||
|
leading-zero and the `i64::MAX`/`i64::MAX/86400`-neighborhood
|
||||||
|
giant-`n` arms; 27 reject shapes incl. positive-zero, negative,
|
||||||
|
whitespace/truncation, cron/`@once` adjacency and unknown units),
|
||||||
|
and the overflow-posture battery over `skip_forward` (24 fixed
|
||||||
|
adversarial triples + 24-byte-stride triples folded from the raw
|
||||||
|
input; three-way equality + saturate-or-strictly-future pin).
|
||||||
|
`#[cfg(test)]` corpus replay over the committed dir (≥48 seeds).
|
||||||
|
- **`fuzz/fuzz_targets/every_parse.rs`** + `[[bin]]` entry (the
|
||||||
|
payload_codec shape); engine path deps added to `shared`'s manifest;
|
||||||
|
lib.rs module registered.
|
||||||
|
- **66 committed seeds** from `gen_fuzz_seeds.py`'s new
|
||||||
|
`every_parse()` (registry wired); `!corpus/every_parse` gitignore
|
||||||
|
negation; `fuzz/README.md` targets-table row filled.
|
||||||
|
- **Two findings found and fixed inline** (details in Notes):
|
||||||
|
`skip_forward` division-by-zero/negative-`interval` wrap (sqlite +
|
||||||
|
mem), and the i64 gap subtraction before the i128 cast (all three
|
||||||
|
engines incl. pg). PROVENANCE D-38 records the substrate delta.
|
||||||
|
- **Smoke verdict**: 180 s detached campaign, ~737k execs, 0
|
||||||
|
crash/oom/timeout — the fixes closed out the deterministic finding
|
||||||
|
space; grown corpus pruned to the committed seeds.
|
||||||
|
|
||||||
|
Verified: `cargo build` clean; `cargo test` workspace green (15 ok
|
||||||
|
result groups, 0 failures); `cargo clippy --all-targets -- -D
|
||||||
|
warnings` clean; `cargo fmt --check` clean; the shared gate
|
||||||
|
`cargo test --manifest-path fuzz/shared/Cargo.toml` green;
|
||||||
|
shared clippy `-D warnings` + fmt --check green;
|
||||||
|
`cargo fuzz build` green from `fuzz/` (nightly pin; all three engines
|
||||||
|
compile under ASAN through the new bin);
|
||||||
|
`cargo check -p alkstore-mem --target wasm32-unknown-unknown` clean.
|
||||||
|
Lineage body untouched; the substrate delta is the two-line
|
||||||
|
`skip_forward` fix in the re-derived half, registered as D-38.
|
||||||
Reference in new issue
Block a user