From 5043f7bcd8dae31f144513b9dea39b436b152c2f Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sat, 10 Oct 2026 22:08:23 +0000 Subject: [PATCH] =?UTF-8?q?fuzz:=20land=20the=20name=5Fvalidation=20target?= =?UTF-8?q?=20(=C2=A73.3,=20wave=207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fuzz/.gitignore | 1 + fuzz/Cargo.toml | 7 + fuzz/README.md | 2 +- fuzz/corpus/name_validation/seed-000 | 1 + fuzz/corpus/name_validation/seed-001 | 1 + fuzz/corpus/name_validation/seed-002 | 1 + fuzz/corpus/name_validation/seed-003 | 1 + fuzz/corpus/name_validation/seed-004 | 1 + fuzz/corpus/name_validation/seed-005 | 1 + fuzz/corpus/name_validation/seed-006 | 1 + fuzz/corpus/name_validation/seed-007 | 1 + fuzz/corpus/name_validation/seed-008 | 1 + fuzz/corpus/name_validation/seed-009 | 0 fuzz/corpus/name_validation/seed-010 | 1 + fuzz/corpus/name_validation/seed-011 | 1 + fuzz/corpus/name_validation/seed-012 | 1 + fuzz/corpus/name_validation/seed-013 | 1 + fuzz/corpus/name_validation/seed-014 | 2 + fuzz/corpus/name_validation/seed-015 | 1 + fuzz/corpus/name_validation/seed-016 | 1 + fuzz/corpus/name_validation/seed-017 | 1 + fuzz/corpus/name_validation/seed-018 | 1 + fuzz/corpus/name_validation/seed-019 | 1 + fuzz/corpus/name_validation/seed-020 | 1 + fuzz/corpus/name_validation/seed-021 | 1 + fuzz/corpus/name_validation/seed-022 | 1 + fuzz/corpus/name_validation/seed-023 | 1 + fuzz/corpus/name_validation/seed-024 | 1 + fuzz/corpus/name_validation/seed-025 | 1 + fuzz/corpus/name_validation/seed-026 | 1 + fuzz/corpus/name_validation/seed-027 | 1 + fuzz/corpus/name_validation/seed-028 | 1 + fuzz/corpus/name_validation/seed-029 | 1 + fuzz/corpus/name_validation/seed-030 | 1 + fuzz/corpus/name_validation/seed-031 | 1 + fuzz/corpus/name_validation/seed-032 | 1 + fuzz/corpus/name_validation/seed-033 | 1 + fuzz/corpus/name_validation/seed-034 | 1 + fuzz/corpus/name_validation/seed-035 | 1 + fuzz/corpus/name_validation/seed-036 | 1 + fuzz/corpus/name_validation/seed-037 | 1 + fuzz/corpus/name_validation/seed-038 | Bin 0 -> 3 bytes fuzz/corpus/name_validation/seed-039 | Bin 0 -> 3 bytes fuzz/corpus/name_validation/seed-040 | 1 + fuzz/corpus/name_validation/seed-041 | 1 + fuzz/corpus/name_validation/seed-042 | 1 + fuzz/corpus/name_validation/seed-043 | 1 + fuzz/fuzz_targets/name_validation.rs | 11 ++ fuzz/gen_fuzz_seeds.py | 68 +++++++ fuzz/shared/src/lib.rs | 1 + fuzz/shared/src/name_validation.rs | 272 +++++++++++++++++++++++++++ tasks/fuzz-target-name-validation.md | 67 ++++++- 52 files changed, 465 insertions(+), 6 deletions(-) create mode 100644 fuzz/corpus/name_validation/seed-000 create mode 100644 fuzz/corpus/name_validation/seed-001 create mode 100644 fuzz/corpus/name_validation/seed-002 create mode 100644 fuzz/corpus/name_validation/seed-003 create mode 100644 fuzz/corpus/name_validation/seed-004 create mode 100644 fuzz/corpus/name_validation/seed-005 create mode 100644 fuzz/corpus/name_validation/seed-006 create mode 100644 fuzz/corpus/name_validation/seed-007 create mode 100644 fuzz/corpus/name_validation/seed-008 create mode 100644 fuzz/corpus/name_validation/seed-009 create mode 100644 fuzz/corpus/name_validation/seed-010 create mode 100644 fuzz/corpus/name_validation/seed-011 create mode 100644 fuzz/corpus/name_validation/seed-012 create mode 100644 fuzz/corpus/name_validation/seed-013 create mode 100644 fuzz/corpus/name_validation/seed-014 create mode 100644 fuzz/corpus/name_validation/seed-015 create mode 100644 fuzz/corpus/name_validation/seed-016 create mode 100644 fuzz/corpus/name_validation/seed-017 create mode 100644 fuzz/corpus/name_validation/seed-018 create mode 100644 fuzz/corpus/name_validation/seed-019 create mode 100644 fuzz/corpus/name_validation/seed-020 create mode 100644 fuzz/corpus/name_validation/seed-021 create mode 100644 fuzz/corpus/name_validation/seed-022 create mode 100644 fuzz/corpus/name_validation/seed-023 create mode 100644 fuzz/corpus/name_validation/seed-024 create mode 100644 fuzz/corpus/name_validation/seed-025 create mode 100644 fuzz/corpus/name_validation/seed-026 create mode 100644 fuzz/corpus/name_validation/seed-027 create mode 100644 fuzz/corpus/name_validation/seed-028 create mode 100644 fuzz/corpus/name_validation/seed-029 create mode 100644 fuzz/corpus/name_validation/seed-030 create mode 100644 fuzz/corpus/name_validation/seed-031 create mode 100644 fuzz/corpus/name_validation/seed-032 create mode 100644 fuzz/corpus/name_validation/seed-033 create mode 100644 fuzz/corpus/name_validation/seed-034 create mode 100644 fuzz/corpus/name_validation/seed-035 create mode 100644 fuzz/corpus/name_validation/seed-036 create mode 100644 fuzz/corpus/name_validation/seed-037 create mode 100644 fuzz/corpus/name_validation/seed-038 create mode 100644 fuzz/corpus/name_validation/seed-039 create mode 100644 fuzz/corpus/name_validation/seed-040 create mode 100644 fuzz/corpus/name_validation/seed-041 create mode 100644 fuzz/corpus/name_validation/seed-042 create mode 100644 fuzz/corpus/name_validation/seed-043 create mode 100644 fuzz/fuzz_targets/name_validation.rs create mode 100644 fuzz/shared/src/name_validation.rs diff --git a/fuzz/.gitignore b/fuzz/.gitignore index e897774..2d90ce2 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -2,5 +2,6 @@ target corpus/* !corpus/payload_codec !corpus/every_parse +!corpus/name_validation artifacts coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index bf177b0..9fe656e 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -28,4 +28,11 @@ test = false doc = false bench = false +[[bin]] +name = "name_validation" +path = "fuzz_targets/name_validation.rs" +test = false +doc = false +bench = false + [workspace] \ No newline at end of file diff --git a/fuzz/README.md b/fuzz/README.md index 18888a3..ac9030a 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -29,7 +29,7 @@ libFuzzer targets for alkstore's parse/codec surfaces (see |---|---|---| | `payload_codec` | `encode_payload` / `payload_as::` (`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, 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` 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`) | no-panic on any byte sequence (lossy-decoded and `arbitrary`-structured strings); deterministic accept/reject (double-invocation-pinned); rejection only via the typed taxonomy — `Error::InvalidName` for empty/whitespace-only on both kinds, `Error::ReservedName` for the leading byte-exact `__alkstore_` prefix on the shared kind only (the local kind takes the non-empty rule only, so a leading reserved prefix is legal there); the reserved-prefix rule exact in every casing/boundary form (casing deviations, missing trailing underscore, embedded and whitespace-led forms accept); the rejection carries the supplied name verbatim (names are not trimmed); verdict-class length-independence across the probed neighborhoods (0↔1, 11↔12, 63↔64, 4095↔4096, 65536) — no length cap is pinned (ADR-008 §4), so no length is ever a rejection cause | ## Commands diff --git a/fuzz/corpus/name_validation/seed-000 b/fuzz/corpus/name_validation/seed-000 new file mode 100644 index 0000000..2e65efe --- /dev/null +++ b/fuzz/corpus/name_validation/seed-000 @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-001 b/fuzz/corpus/name_validation/seed-001 new file mode 100644 index 0000000..7ec9a4b --- /dev/null +++ b/fuzz/corpus/name_validation/seed-001 @@ -0,0 +1 @@ +aa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-002 b/fuzz/corpus/name_validation/seed-002 new file mode 100644 index 0000000..7284ab4 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-002 @@ -0,0 +1 @@ +aaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-003 b/fuzz/corpus/name_validation/seed-003 new file mode 100644 index 0000000..2719875 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-003 @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-004 b/fuzz/corpus/name_validation/seed-004 new file mode 100644 index 0000000..487e57f --- /dev/null +++ b/fuzz/corpus/name_validation/seed-004 @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-005 b/fuzz/corpus/name_validation/seed-005 new file mode 100644 index 0000000..71b7a71 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-005 @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-006 b/fuzz/corpus/name_validation/seed-006 new file mode 100644 index 0000000..44171c1 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-006 @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-007 b/fuzz/corpus/name_validation/seed-007 new file mode 100644 index 0000000..7a25c0a --- /dev/null +++ b/fuzz/corpus/name_validation/seed-007 @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-008 b/fuzz/corpus/name_validation/seed-008 new file mode 100644 index 0000000..9d235ed --- /dev/null +++ b/fuzz/corpus/name_validation/seed-008 @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-009 b/fuzz/corpus/name_validation/seed-009 new file mode 100644 index 0000000..e69de29 diff --git a/fuzz/corpus/name_validation/seed-010 b/fuzz/corpus/name_validation/seed-010 new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/fuzz/corpus/name_validation/seed-010 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-011 b/fuzz/corpus/name_validation/seed-011 new file mode 100644 index 0000000..136d063 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-011 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-012 b/fuzz/corpus/name_validation/seed-012 new file mode 100644 index 0000000..501a6bb --- /dev/null +++ b/fuzz/corpus/name_validation/seed-012 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-013 b/fuzz/corpus/name_validation/seed-013 new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-013 @@ -0,0 +1 @@ + diff --git a/fuzz/corpus/name_validation/seed-014 b/fuzz/corpus/name_validation/seed-014 new file mode 100644 index 0000000..15b2f79 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-014 @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-015 b/fuzz/corpus/name_validation/seed-015 new file mode 100644 index 0000000..52dbb30 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-015 @@ -0,0 +1 @@ +  \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-016 b/fuzz/corpus/name_validation/seed-016 new file mode 100644 index 0000000..01b510b --- /dev/null +++ b/fuzz/corpus/name_validation/seed-016 @@ -0,0 +1 @@ +  \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-017 b/fuzz/corpus/name_validation/seed-017 new file mode 100644 index 0000000..63cde67 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-017 @@ -0,0 +1 @@ +__alkstore_ \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-018 b/fuzz/corpus/name_validation/seed-018 new file mode 100644 index 0000000..39cc12b --- /dev/null +++ b/fuzz/corpus/name_validation/seed-018 @@ -0,0 +1 @@ +__alkstore_x \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-019 b/fuzz/corpus/name_validation/seed-019 new file mode 100644 index 0000000..8fb1827 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-019 @@ -0,0 +1 @@ +__alkstore_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-020 b/fuzz/corpus/name_validation/seed-020 new file mode 100644 index 0000000..9d1f19a --- /dev/null +++ b/fuzz/corpus/name_validation/seed-020 @@ -0,0 +1 @@ +__alkstore_listener_reconnected__ \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-021 b/fuzz/corpus/name_validation/seed-021 new file mode 100644 index 0000000..56d9adf --- /dev/null +++ b/fuzz/corpus/name_validation/seed-021 @@ -0,0 +1 @@ +__ALKSTORE_x \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-022 b/fuzz/corpus/name_validation/seed-022 new file mode 100644 index 0000000..c076d35 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-022 @@ -0,0 +1 @@ +__Alkstore_x \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-023 b/fuzz/corpus/name_validation/seed-023 new file mode 100644 index 0000000..50aebae --- /dev/null +++ b/fuzz/corpus/name_validation/seed-023 @@ -0,0 +1 @@ +__alkStore_x \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-024 b/fuzz/corpus/name_validation/seed-024 new file mode 100644 index 0000000..4db0255 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-024 @@ -0,0 +1 @@ +__alkstore \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-025 b/fuzz/corpus/name_validation/seed-025 new file mode 100644 index 0000000..fde45ac --- /dev/null +++ b/fuzz/corpus/name_validation/seed-025 @@ -0,0 +1 @@ +x__alkstore_y \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-026 b/fuzz/corpus/name_validation/seed-026 new file mode 100644 index 0000000..98cc089 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-026 @@ -0,0 +1 @@ + __alkstore_x \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-027 b/fuzz/corpus/name_validation/seed-027 new file mode 100644 index 0000000..885ba30 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-027 @@ -0,0 +1 @@ +__alkstore_ \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-028 b/fuzz/corpus/name_validation/seed-028 new file mode 100644 index 0000000..3c6bd98 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-028 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-029 b/fuzz/corpus/name_validation/seed-029 new file mode 100644 index 0000000..e90e706 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-029 @@ -0,0 +1 @@ +__alkstore_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-030 b/fuzz/corpus/name_validation/seed-030 new file mode 100644 index 0000000..d4c80b8 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-030 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-031 b/fuzz/corpus/name_validation/seed-031 new file mode 100644 index 0000000..3f44354 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-031 @@ -0,0 +1 @@ +__alkstore_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-032 b/fuzz/corpus/name_validation/seed-032 new file mode 100644 index 0000000..77acffd --- /dev/null +++ b/fuzz/corpus/name_validation/seed-032 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-033 b/fuzz/corpus/name_validation/seed-033 new file mode 100644 index 0000000..f04fde7 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-033 @@ -0,0 +1 @@ +__alkstore_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-034 b/fuzz/corpus/name_validation/seed-034 new file mode 100644 index 0000000..da68996 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-034 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-035 b/fuzz/corpus/name_validation/seed-035 new file mode 100644 index 0000000..60758dc --- /dev/null +++ b/fuzz/corpus/name_validation/seed-035 @@ -0,0 +1 @@ +__alkstore_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-036 b/fuzz/corpus/name_validation/seed-036 new file mode 100644 index 0000000..1856c10 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-036 @@ -0,0 +1 @@ +é-😀-名前 \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-037 b/fuzz/corpus/name_validation/seed-037 new file mode 100644 index 0000000..95aee26 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-037 @@ -0,0 +1 @@ +é́ \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-038 b/fuzz/corpus/name_validation/seed-038 new file mode 100644 index 0000000000000000000000000000000000000000..8352675d67aed6625ece79af41c27fdb4ee2e867 GIT binary patch literal 3 KcmZQzWC8#H2LJ>B literal 0 HcmV?d00001 diff --git a/fuzz/corpus/name_validation/seed-039 b/fuzz/corpus/name_validation/seed-039 new file mode 100644 index 0000000000000000000000000000000000000000..20b5be91886d0b6f26dc98a225c0dac05fe2c86e GIT binary patch literal 3 KcmYdfNCE%>hycU@ literal 0 HcmV?d00001 diff --git a/fuzz/corpus/name_validation/seed-040 b/fuzz/corpus/name_validation/seed-040 new file mode 100644 index 0000000..0417629 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-040 @@ -0,0 +1 @@ +tab name \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-041 b/fuzz/corpus/name_validation/seed-041 new file mode 100644 index 0000000..16e0e90 --- /dev/null +++ b/fuzz/corpus/name_validation/seed-041 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-042 b/fuzz/corpus/name_validation/seed-042 new file mode 100644 index 0000000..dc1115b --- /dev/null +++ b/fuzz/corpus/name_validation/seed-042 @@ -0,0 +1 @@ +Ã( \ No newline at end of file diff --git a/fuzz/corpus/name_validation/seed-043 b/fuzz/corpus/name_validation/seed-043 new file mode 100644 index 0000000..46b134b --- /dev/null +++ b/fuzz/corpus/name_validation/seed-043 @@ -0,0 +1 @@ +ÿþ \ No newline at end of file diff --git a/fuzz/fuzz_targets/name_validation.rs b/fuzz/fuzz_targets/name_validation.rs new file mode 100644 index 0000000..7dd4057 --- /dev/null +++ b/fuzz/fuzz_targets/name_validation.rs @@ -0,0 +1,11 @@ +//! The §3.3 target: core's name-validation invariants, driven through +//! the already-`pub` validation surface (`validate_name` over all +//! `NameKind`s, `validate_shared_name`, `validate_local_name`). + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + alkstore_fuzz_shared::name_validation::fuzz_name_validation(data); +}); \ No newline at end of file diff --git a/fuzz/gen_fuzz_seeds.py b/fuzz/gen_fuzz_seeds.py index 56880b7..8448de0 100644 --- a/fuzz/gen_fuzz_seeds.py +++ b/fuzz/gen_fuzz_seeds.py @@ -245,9 +245,77 @@ def every_parse_seeds(): i += 1 +def name_validation_seeds(): + i = 0 + + # Plain-content names at the probed lengths: the empty↔one-char + # boundary, the pg-identifier (±1) and target-max_len (±1/±2) + # neighborhoods. No length cap is pinned; the seeds pin + # length-independence of the verdict class. + for length in [1, 2, 4, 62, 63, 64, 65, 4095, 4096]: + write_seed("name_validation", i, b"a" * length) + i += 1 + + # Empty and whitespace-only shapes (spaces, tab, newline, mixed, + # unicode whitespace) — InvalidName on both kinds. + for raw in [b"", b" ", b" ", b"\t", b"\n", b" \t\n ", b"\xc2\xa0", b"\xe2\x80\x83"]: + write_seed("name_validation", i, raw) + i += 1 + + # The reserved prefix: the boundary form (alone), suffixed, at a + # boundary length, and the one v1-reserved string. + for raw in [ + b"__alkstore_", + b"__alkstore_x", + b"__alkstore_" + b"a" * 63, + b"__alkstore_listener_reconnected__", + ]: + write_seed("name_validation", i, raw) + i += 1 + + # Prefix near-misses: casing deviations, the missing trailing + # underscore, embedded and whitespace-led forms — the pinned rule + # is byte-exact and leading-only, so these must accept. + for raw in [ + b"__ALKSTORE_x", + b"__Alkstore_x", + b"__alkStore_x", + b"__alkstore", + b"x__alkstore_y", + b" __alkstore_x", + b"__alkstore_ ", + ]: + write_seed("name_validation", i, raw) + i += 1 + + # Whitespace-only and reserved-prefixed at the pg-identifier + # neighborhood lengths (±1, ±2). + for length in [62, 63, 64, 65]: + write_seed("name_validation", i, b" " * length) + i += 1 + write_seed("name_validation", i, b"__alkstore_" + b"a" * (length - len(b"__alkstore_"))) + i += 1 + + # Unicode and control-char shapes, including invalid UTF-8 (lossy + # decode) and embedded NUL. + for raw in [ + "é-😀-名前".encode(), + "\u00e9\u0301".encode(), + b"\x00\x01\x02", + b"a\x00b", + b"tab\tname", + b"\x7f", + b"\xc3\x28", + b"\xff\xfe", + ]: + write_seed("name_validation", i, raw) + i += 1 + + def main(): payload_codec_seeds() every_parse_seeds() + name_validation_seeds() if __name__ == "__main__": diff --git a/fuzz/shared/src/lib.rs b/fuzz/shared/src/lib.rs index 9d47df5..0dec3e9 100644 --- a/fuzz/shared/src/lib.rs +++ b/fuzz/shared/src/lib.rs @@ -5,4 +5,5 @@ pub mod arbitrary_value; pub mod every_parse; +pub mod name_validation; pub mod payload_codec; diff --git a/fuzz/shared/src/name_validation.rs b/fuzz/shared/src/name_validation.rs new file mode 100644 index 0000000..3d5a195 --- /dev/null +++ b/fuzz/shared/src/name_validation.rs @@ -0,0 +1,272 @@ +//! Invariant logic for the `name_validation` target (docs/research/ +//! fuzzing.md §3.3, `alkstore/src/validation.rs`): the entry-point +//! name-validation surface over the whole string space — no-panic on +//! any byte sequence (lossy-decoded and `arbitrary`-structured alike), +//! deterministic accept/reject (double-invocation pinned), rejection +//! only via the typed `InvalidName`/`ReservedName` taxonomy +//! (ADR-008 §4/§5), the reserved-prefix rule byte-exact and +//! leading-only with the shared/local kind split (the local kind takes +//! the non-empty rule only — a leading `__alkstore_` is legal there), +//! the error carrying the supplied name verbatim (names are not +//! trimmed), and length-independence of the verdict class. No length +//! cap is pinned (ADR-008 §4 pins non-empty + reserved-prefix only), so +//! "exactness at the cap" is pinned as: nothing at any length flips a +//! verdict — the only exact boundaries in the rule are empty↔one-char, +//! whitespace-only↔content, and the byte-exact prefix itself. + +use arbitrary::{Arbitrary, Unstructured}; + +use alkstore::{ + validate_local_name, validate_name, validate_shared_name, Error, NameKind, + RESERVED_LISTENER_RECONNECTED, RESERVED_PREFIX, +}; + +/// The probed length neighborhoods: the empty↔one-char boundary +/// (0↔1), the reserved-prefix length (11↔12), the Postgres identifier +/// bytes (63↔64), the target's max_len (4095↔4096), and the +/// payload-cap scale (65536). None of these is a pinned cap — the ADR +/// pins no length limit — so the invariant is that the verdict class +/// never changes with length. +const LENGTH_PROBES: &[usize] = &[0, 1, 2, 3, 11, 12, 62, 63, 64, 65, 4095, 4096, 65_536]; + +/// The verdict of one validation call, narrowed to the taxonomy's typed +/// rejections: `Accept`, `InvalidName { name }`, or +/// `ReservedName { name }`. Any other outcome is a contract violation +/// and panics at construction — the typed-rejection invariant. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Verdict { + Accept, + InvalidName(String), + ReservedName(String), +} + +pub fn fuzz_name_validation(data: &[u8]) { + let lossy = String::from_utf8_lossy(data); + assert_verdict_invariants(&lossy); + assert_verdict_battery(); + assert_structured(data); + assert_length_independence(); +} + +/// One string across the whole invariant set: deterministic (the +/// double-invocation pin), typed-only, all three entry points agreeing, +/// and the observed verdicts matching the locally recomputed rule. +fn assert_verdict_invariants(name: &str) { + let shared = shared_verdict(name); + assert_eq!( + shared, + shared_verdict(name), + "the shared-kind verdict must be deterministic for {name:?}" + ); + let local = local_verdict(name); + assert_eq!( + local, + local_verdict(name), + "the local-kind verdict must be deterministic for {name:?}" + ); + assert_eq!( + shared, + kind_verdict(NameKind::Shared, name), + "`validate_shared_name` must agree with `validate_name(Shared)` for {name:?}" + ); + assert_eq!( + local, + kind_verdict(NameKind::Local, name), + "`validate_local_name` must agree with `validate_name(Local)` for {name:?}" + ); + assert_classification(name, &shared, &local); +} + +/// The rule of record recomputed locally and asserted against the +/// observed verdicts: empty or whitespace-only → `InvalidName` on both +/// kinds (names are not trimmed — the rejection never trims the name it +/// reports); otherwise the byte-exact leading reserved prefix rejects +/// `ReservedName` on the shared kind and is legal on the local kind; +/// anything else accepts on both kinds. +fn assert_classification(name: &str, shared: &Verdict, local: &Verdict) { + if name.is_empty() || name.trim().is_empty() { + assert_eq!( + shared, + &Verdict::InvalidName(name.to_owned()), + "empty/whitespace-only must reject `InvalidName` on the shared kind for {name:?}" + ); + assert_eq!( + local, + &Verdict::InvalidName(name.to_owned()), + "empty/whitespace-only must reject `InvalidName` on the local kind for {name:?}" + ); + return; + } + if name.starts_with(RESERVED_PREFIX) { + assert_eq!( + shared, + &Verdict::ReservedName(name.to_owned()), + "a leading reserved prefix must reject `ReservedName` on the shared kind for {name:?} — the error carries the name verbatim" + ); + assert_eq!( + local, + &Verdict::Accept, + "the local kind takes the non-empty rule only — a leading reserved prefix is legal there: {name:?}" + ); + return; + } + assert_eq!( + shared, + &Verdict::Accept, + "a non-empty, non-whitespace shared name without the exact reserved prefix must accept: {name:?}" + ); + assert_eq!( + local, + &Verdict::Accept, + "a non-empty, non-whitespace local name must accept: {name:?}" + ); +} + +/// The casing/boundary battery for the reserved-prefix rule: the rule +/// is byte-exact and leading-only, so the near-miss forms (casing +/// deviations, the missing trailing underscore, embedded and +/// whitespace-led prefix occurrences) must accept on the shared kind, +/// while the byte-exact prefix (alone, suffixed, whitespace-suffixed — +/// anything not all-whitespace) must reject. +const BATTERY: &[&str] = &[ + "", + " ", + "\t\n", + "\u{a0}", + "a", + "é-😀", + "a\u{0}b", + "__alkstore_", + "__alkstore_x", + RESERVED_LISTENER_RECONNECTED, + "__alkstore_ ", + "__alkstore_x_", + "__alkstore", + "__ALKSTORE_x", + "__Alkstore_x", + "__alkStore_x", + "__alkstore_X", + "x__alkstore_y", + " __alkstore_x", +]; + +fn assert_verdict_battery() { + for name in BATTERY { + assert_verdict_invariants(name); + } +} + +/// The structured path: `arbitrary`-derived strings plus their +/// boundary-derivative forms (leading/trailing/embedded reserved +/// prefix, whitespace-wrapped, case-shifted) through the same +/// invariant set. +fn assert_structured(data: &[u8]) { + let mut u = Unstructured::new(data); + for _ in 0..2 { + let Ok(name) = String::arbitrary(&mut u) else { + return; + }; + assert_verdict_invariants(&name); + for variant in derived_variants(&name) { + assert_verdict_invariants(&variant); + } + } +} + +fn derived_variants(name: &str) -> Vec { + let mut variants = vec![ + format!("{}{}", RESERVED_PREFIX, name), + format!("{}{}", name, RESERVED_PREFIX), + format!("x{}{}y", RESERVED_PREFIX, name), + format!(" {}", name), + format!("{} ", name), + ]; + variants.push(name.to_uppercase()); + variants +} + +/// Verdict-class length-independence: the same content class (plain, +/// whitespace-only, reserved-prefixed, casing near-miss, embedded +/// prefix) holds its verdict at every probed length. No length cap is +/// pinned — a length anywhere is not a rejection cause. +fn assert_length_independence() { + for &len in LENGTH_PROBES { + let plain = "a".repeat(len); + assert_verdict_invariants(&plain); + let ws_only = " ".repeat(len); + assert_verdict_invariants(&ws_only); + let reserved = format!("{}{}", RESERVED_PREFIX, "a".repeat(len)); + assert_verdict_invariants(&reserved); + let casing = format!("__ALKSTORE_{}", "a".repeat(len)); + assert_verdict_invariants(&casing); + let embedded = format!("{}{}{}", "a".repeat(len), RESERVED_PREFIX, "b".repeat(len)); + assert_verdict_invariants(&embedded); + } +} + +fn shared_verdict(name: &str) -> Verdict { + match validate_shared_name(name) { + Ok(()) => Verdict::Accept, + Err(Error::InvalidName { name }) => Verdict::InvalidName(name), + Err(Error::ReservedName { name }) => Verdict::ReservedName(name), + Err(other) => panic!( + "shared-name rejection must be typed `InvalidName`/`ReservedName`, got {other:?} for {name:?}" + ), + } +} + +fn local_verdict(name: &str) -> Verdict { + match validate_local_name(name) { + Ok(()) => Verdict::Accept, + Err(Error::InvalidName { name }) => Verdict::InvalidName(name), + Err(Error::ReservedName { name }) => Verdict::ReservedName(name), + Err(other) => panic!( + "local-name rejection must be typed `InvalidName`/`ReservedName`, got {other:?} for {name:?}" + ), + } +} + +fn kind_verdict(kind: NameKind, name: &str) -> Verdict { + match validate_name(kind, name) { + Ok(()) => Verdict::Accept, + Err(Error::InvalidName { name }) => Verdict::InvalidName(name), + Err(Error::ReservedName { name }) => Verdict::ReservedName(name), + Err(other) => panic!( + "name rejection must be typed `InvalidName`/`ReservedName`, got {other:?} for {name:?}" + ), + } +} + +#[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/name_validation"); + 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_name_validation(&data); + count += 1; + } + assert!( + count >= 32, + "committed seed corpus is present, found {count}" + ); + } + + #[test] + fn invariants_hold_without_corpus() { + fuzz_name_validation(b""); + fuzz_name_validation(b"a"); + fuzz_name_validation(b" "); + fuzz_name_validation(b"__alkstore_"); + fuzz_name_validation(b"__alkstore_listener_reconnected__"); + fuzz_name_validation(b"__alkstore_l\xffistener"); + fuzz_name_validation(&[0u8; 16]); + fuzz_name_validation(&[b'a'; 65_537]); + } +} diff --git a/tasks/fuzz-target-name-validation.md b/tasks/fuzz-target-name-validation.md index 039a414..d955caa 100644 --- a/tasks/fuzz-target-name-validation.md +++ b/tasks/fuzz-target-name-validation.md @@ -1,7 +1,7 @@ --- id: fuzz-target-name-validation name: Fuzz target — `name_validation` (core name validation) -status: pending +status: completed depends_on: [fuzz-scaffold] scope: narrow risk: trivial @@ -58,14 +58,71 @@ guard-order discipline made continuous. - docs/research/fuzzing.md §3.3 (target spec), §4, §7 - docs/architecture/core-contract.md §Naming / reserved namespace -- docs/architecture/decisions/014-naming-and-reserved-namespace.md §1 - (the reserved-prefix rule the target demonstrates) +- docs/architecture/decisions/008-contract-v1-pinning.md §4/§5 — the + reserved-prefix rule and the typed-rejection taxonomy the target + demonstrates (the description's `014-naming-and-reserved-namespace.md` + reference was stale: ADR-014 is the outbox ADR; the reserved + namespace is ADR-008 §4, with the kinds list extended by ADR-014 + outbox and ADR-009 schedule — corrected here) - alkstore/src/validation.rs (the surface) ## Notes -> To be filled by implementation agent +- **No length cap is pinned — the "length-boundary exactness at the + cap" invariant was rendered as verdict-class length-independence.** + The description (and fuzzing.md §3.3) pinned a cap with a ±1 + neighborhood ("a name at the cap is accepted, one past it rejected"), + but no cap exists anywhere in the normative stack: ADR-008 §4 pins + only non-empty (whitespace-only-rejected) + the reserved-prefix rule, + core-contract §Naming adds nothing about length, and + `alkstore/src/validation.rs` checks nothing about length. Per + AGENTS.md the ADR wins, so the shipped invariant pins what is true: + the verdict class is length-independent (no length is ever a + rejection cause) across probed neighborhoods (0↔1 empty boundary, + 11↔12 prefix length, 63↔64 pg-identifier bytes, 4095↔4096 target + max_len, 65536 payload-cap scale). The only exact boundaries in the + pinned rule — empty↔one-char, whitespace-only↔content, and the + byte-exact prefix — are pinned exactly. +- **The "rejects in every casing/boundary form" invariant is pinned + with its exact-case direction.** The rule is byte-exact and + leading-only (`starts_with(RESERVED_PREFIX)`), so the battery pins + the byte-exact prefix rejecting (alone, suffixed, + whitespace-suffixed — anything not all-whitespace) **and** the + near-miss forms accepting on the shared kind (casing deviations, + missing trailing underscore, embedded and whitespace-led + occurrences) plus the local kind taking the non-empty rule only + (leading reserved prefix legal there, per ADR-008 §4's annotation + and the `NameKind::Local` doc). +- **Corpus-replay floor set at 32** (44 seeds committed), following + the sibling targets' pattern (64 / 48). ## Summary -> To be filled on completion \ No newline at end of file +Landed the §3.3 target: `fuzz_targets/name_validation.rs` (thin +`fuzz_target!` wrapper, like the sibling targets) + +`shared/src/name_validation.rs` (invariants + `#[cfg(test)]` corpus +replay), its `[[bin]]` entry, `pub mod` in shared's lib.rs, the +`name_validation_seeds()` generator (44 deterministic seeds committed: +cap-neighborhood lengths, empty/whitespace-only, the reserved prefix +plain/suffixed/at-length plus the listener-reconnected string, seven +near-miss forms, unicode/control/invalid-UTF-8 shapes), the +`!corpus/name_validation` gitignore negation, and the README +targets-table row. Invariants: total (no-panic over lossy and +`arbitrary`-structured strings plus derived boundary forms); +determinism (double invocation on both kinds); typed-only rejections +(narrowed to a `Verdict` enum that panics on anything but +`Accept`/`InvalidName`/`ReservedName`); wrapper equivalence +(`validate_shared_name`/`validate_local_name` vs `validate_name` on +each `NameKind`); classification asserted against a locally recomputed +rule of record (verbatim-name-carrying errors — names not trimmed); +the casing/boundary battery; and the length-independence battery +(see Notes). + +Verified: `cargo test --manifest-path fuzz/shared/Cargo.toml` green +(6/6, 44-seed replay included); shared clippy `-D warnings` + fmt +`--check` green; `cargo fuzz build` green from `fuzz/` (nightly pin); +root `cargo build` / `cargo test` (all suites pass) / clippy +`-D warnings` / fmt `--check` green; seed regeneration deterministic; +corpus files trackable (negation verified via `git check-ignore`). +Campaign runs are `fuzz-campaigns-and-gate`'s concern (this task is +one of its three dependencies). \ No newline at end of file