docs(comments): sweep em dashes from source comments, test prose, and manifest
Same treatment for the prose the compiler carries: doc and line comments across src/ and tests/, test assertion and error-message strings (prose surfaces: the tunnel-map registration/establish errors, the key-source decode payload, assertion messages), scripts/matrix.sh comment lines, the fuzz sidecar's comments and README (corpus seeds untouched), and the Cargo.toml comment headers plus the crates.io description. The AGENTS.md no-em-dash rule (convention 14) now holds for the whole tracked tree. Verification: repo-wide git grep is clean (single exempt hit: the verification command's own search pattern), cargo test (18 suites, zero failures, default and per-feature builds via scripts/matrix.sh), clippy --all-targets -D warnings and clippy all-features, fmt --check, doc --no-deps, and the fuzz corpus gate (fuzz/shared, 11 tests) all green.
This commit is contained in:
1 parent
f4b5f896b4
commit
1e583ab55b
55 files changed
+1756
-1751
No files matched your search
+16
-16
@@ -1,16 +1,16 @@
|
||||
# alkssh fuzzing
|
||||
|
||||
libFuzzer targets for alkssh's ingress surfaces — the family standing
|
||||
libFuzzer targets for alkssh's ingress surfaces: the family standing
|
||||
rule (anything worth fuzzing gets fuzzed), landed per the plan's wave-5
|
||||
charter and the alkgit convention: the JSON schema gate itself is
|
||||
charter and the alkgit convention. The JSON schema gate itself is
|
||||
alkcall's surface, family-fuzzed there; SSH framing, key exchange, and
|
||||
the channel multiplexer are russh's wire (family ADR-003) — the
|
||||
the channel multiplexer are russh's wire (family ADR-003); the
|
||||
alkssh-owned post-framing ingress is what fuzzes here.
|
||||
|
||||
## Layout
|
||||
|
||||
- `fuzz_targets/` — one binary per target; thin `fuzz_target!` wrappers.
|
||||
- `shared/` — the invariant logic, as a plain library so normal
|
||||
- `fuzz_targets/`: one binary per target; thin `fuzz_target!` wrappers.
|
||||
- `shared/`: the invariant logic, as a plain library so normal
|
||||
`cargo test` (stable toolchain) can replay the committed corpora
|
||||
through the same invariants (`#[cfg(test)]` modules; quinn's CI
|
||||
pattern). The fuzz binaries are nightly-only; the shared crate is
|
||||
@@ -18,34 +18,34 @@ alkssh-owned post-framing ingress is what fuzzes here.
|
||||
`#[doc(hidden)]` fuzz-visible exposures: the exec-grammar parse path
|
||||
in `src/dispatch.rs`, the key-decode arm `decode_key_arm` in
|
||||
`src/key_source.rs`, and the `terminal_params` conversion in
|
||||
`src/tty_map.rs` — the recorded exposure decisions: the ingresses are
|
||||
`src/tty_map.rs`; the recorded exposure decisions: the ingresses are
|
||||
production-private, so the exposures are the production surfaces
|
||||
themselves, never forks; the fingerprint ingress is fully public API)
|
||||
and must never become a dependency of the root crate.
|
||||
- `corpus/<target>/` — committed hand-made seeds (regenerate with
|
||||
`python3 fuzz/gen_fuzz_seeds.py` from the repo root — must be a no-op
|
||||
diff on the committed set). Only `seed-*` files are tracked; grown
|
||||
- `corpus/<target>/`: committed hand-made seeds (regenerate with
|
||||
`python3 fuzz/gen_fuzz_seeds.py` from the repo root; the result must
|
||||
be a no-op diff on the committed set). Only `seed-*` files are tracked; grown
|
||||
corpora (SHA-named units), artifacts, and coverage output are
|
||||
gitignored and are **not** merged back into the committed seeds.
|
||||
- `run-detached.sh` — mandatory runner for agent sessions: wraps
|
||||
- `run-detached.sh`: the mandatory runner for agent sessions. It wraps
|
||||
`cargo fuzz run` in `setsid` + `nohup` + log redirection so an OOM
|
||||
in a target can never take down the agent host.
|
||||
- `gen_fuzz_seeds.py` — deterministic seed generator (quiche pattern).
|
||||
- `gen_fuzz_seeds.py`: deterministic seed generator (quiche pattern).
|
||||
|
||||
## Targets
|
||||
|
||||
| Target | Drives | Invariants |
|
||||
|---|---|---|
|
||||
| `exec_parse` | the production exec-grammar parse (`parse_exec`, `src/dispatch.rs` — the handler's ingress shape: byte string → `from_utf8` fallback `""` → parse; the pre-parse `max_exec_command_len` cap and the git row's `invalid-repo-id` gate are the root contract rows' territory, the parse is everything behind them) | no-panic on any byte string (any length behind the cap); the typed routing classes (git-parseable → the git row, non-git → the session pipe path, service-only → decline-with-reason) agree with a from-spec grammar oracle (the differential pair) and are stable per input; for `Git` routes the repo-id gate classification is deterministic |
|
||||
| `fingerprint` | the crate's single fingerprint path ([`alkssh::fingerprint`] over `russh::keys::parse_public_key_base64` — ADR-004 decision 1) | no-panic on arbitrary key blobs (a non-blob is the parse's typed refusal); every accepted key's fingerprint is exactly the `SHA256:` + unpadded-standard-base64 contract shape (43-char body, canonical alphabet, decodes to a 32-byte digest, re-encodes canonically); determinism; the committed `ssh-keygen -lf` parity vectors still agree (the corpus-gate test row) |
|
||||
| `key_decode` | the decrypt-at-use chain's decode arm (`decode_key_arm`, `src/key_source.rs` — the production `decrypt_and_decode` conversion exposed `#[doc(hidden)]`) + the underlying `decode_secret_key` passphrase arm (robustness drive — the production chain always passes `None`) | no-panic on arbitrary plaintext shapes (the vault's `String` shape, lossy from arbitrary bytes) and passphrase shapes; every failure on the production arm is exactly the typed `KeySourceError::Decode` class — never a catch-all, never a panic; the failure text never carries the plaintext; the `Zeroizing` posture rides the drive (the wrap is what the adapter seam applies); determinism |
|
||||
| `terminal_params` | the `pty_request` args → `TerminalParamsWire` conversion (`terminal_params`, `src/tty_map.rs` — OQ-44's SSH modes → reserved-modes `{}` JSON) | no-panic on arbitrary term strings, u32 dimensions, and mode blobs (the modes are never parsed — reserved); the dimension clamp is exactly the recorded cap (`min(value, 65535)`, never a magnitude truncation); `term` preserved verbatim; the reserved-modes JSON is always exactly `{}`; the full serialized wire shape is deterministic per input |
|
||||
| `exec_parse` | the production exec-grammar parse (`parse_exec`, `src/dispatch.rs`, the handler's ingress shape: byte string → `from_utf8` fallback `""` → parse; the pre-parse `max_exec_command_len` cap and the git row's `invalid-repo-id` gate are the root contract rows' territory, the parse is everything behind them) | no-panic on any byte string (any length behind the cap); the typed routing classes (git-parseable → the git row, non-git → the session pipe path, service-only → decline-with-reason) agree with a from-spec grammar oracle (the differential pair) and are stable per input; for `Git` routes the repo-id gate classification is deterministic |
|
||||
| `fingerprint` | the crate's single fingerprint path ([`alkssh::fingerprint`] over `russh::keys::parse_public_key_base64`, ADR-004 decision 1) | no-panic on arbitrary key blobs (a non-blob is the parse's typed refusal); every accepted key's fingerprint is exactly the `SHA256:` + unpadded-standard-base64 contract shape (43-char body, canonical alphabet, decodes to a 32-byte digest, re-encodes canonically); determinism; the committed `ssh-keygen -lf` parity vectors still agree (the corpus-gate test row) |
|
||||
| `key_decode` | the decrypt-at-use chain's decode arm (`decode_key_arm`, `src/key_source.rs`, the production `decrypt_and_decode` conversion exposed `#[doc(hidden)]`) + the underlying `decode_secret_key` passphrase arm (a robustness drive; the production chain always passes `None`) | no-panic on arbitrary plaintext shapes (the vault's `String` shape, lossy from arbitrary bytes) and passphrase shapes; every failure on the production arm is exactly the typed `KeySourceError::Decode` class, never a catch-all, never a panic; the failure text never carries the plaintext; the `Zeroizing` posture rides the drive (the wrap is what the adapter seam applies); determinism |
|
||||
| `terminal_params` | the `pty_request` args → `TerminalParamsWire` conversion (`terminal_params`, `src/tty_map.rs`, OQ-44's SSH modes → reserved-modes `{}` JSON) | no-panic on arbitrary term strings, u32 dimensions, and mode blobs (the modes are never parsed; they are reserved); the dimension clamp is exactly the recorded cap (`min(value, 65535)`, never a magnitude truncation); `term` preserved verbatim; the reserved-modes JSON is always exactly `{}`; the full serialized wire shape is deterministic per input |
|
||||
|
||||
## Commands
|
||||
|
||||
`cargo fuzz build` and `cargo fuzz run` must be executed with `fuzz/`
|
||||
(or deeper) as the working directory so rustup selects the pinned
|
||||
nightly toolchain — the detached runner handles that itself.
|
||||
nightly toolchain; the detached runner handles that itself.
|
||||
|
||||
```sh
|
||||
# build (nightly, pinned by rust-toolchain.toml inside fuzz/; run from fuzz/)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![no_main]
|
||||
|
||||
//! The exec-grammar target (`mapping.md` §Session-channel dispatch —
|
||||
//! the git-parseable exec grammar + the dispatch parse path) — thin
|
||||
//! The exec-grammar target (`mapping.md` §Session-channel dispatch,
|
||||
//! the git-parseable exec grammar + the dispatch parse path): a thin
|
||||
//! wrapper over the shared invariant battery.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![no_main]
|
||||
|
||||
//! The fingerprint-format target (ADR-004 decision 1 — the `SHA256:` +
|
||||
//! unpadded base64 path) — thin wrapper over the shared invariant
|
||||
//! The fingerprint-format target (ADR-004 decision 1, the `SHA256:` +
|
||||
//! unpadded base64 path): a thin wrapper over the shared invariant
|
||||
//! battery.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![no_main]
|
||||
|
||||
//! The key-decode target (ADR-005 decision 2 — the decrypt-at-use
|
||||
//! chain's `decode_secret_key` seam) — thin wrapper over the shared
|
||||
//! The key-decode target (ADR-005 decision 2, the decrypt-at-use
|
||||
//! chain's `decode_secret_key` seam): a thin wrapper over the shared
|
||||
//! invariant battery.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#![no_main]
|
||||
|
||||
//! The terminal-parameters target (`tty_map.rs` — the `pty_request`
|
||||
//! The terminal-parameters target (`tty_map.rs`, the `pty_request`
|
||||
//! args → `TerminalParamsWire` conversion, the SSH modes → reserved
|
||||
//! modes JSON) — thin wrapper over the shared invariant battery.
|
||||
//! modes JSON): a thin wrapper over the shared invariant battery.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The corpus-replay plumbing: the committed seeds replay through the
|
||||
//! invariant batteries on the stable toolchain (`cargo test
|
||||
//! --manifest-path fuzz/shared/Cargo.toml` runs this in
|
||||
//! `#[cfg(test)]` modules across the ingress units — the quinn CI
|
||||
//! `#[cfg(test)]` modules across the ingress units, the quinn CI
|
||||
//! pattern), so the gate never needs a fuzzing session.
|
||||
|
||||
pub fn read_corpus(target: &str) -> Vec<Vec<u8>> {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! The exec-grammar ingress (`mapping.md` §Session-channel dispatch,
|
||||
//! `dispatch.rs`): the production `parse_exec` path driven on arbitrary
|
||||
//! command strings — no-panic, typed routing classes, and agreement
|
||||
//! command strings: no-panic, typed routing classes, and agreement
|
||||
//! with a from-spec re-derivation of the grammar. The production
|
||||
//! ingress shape is the handler's: the per-length cap
|
||||
//! (`Limits::max_exec_command_len`) is enforced *before* the parse
|
||||
//! (decline first, alloc never — the cap's pre-parse placement is
|
||||
//! (decline first, alloc never; the cap's pre-parse placement is
|
||||
//! pinned by the root's contract rows in `tests/dispatch_core.rs`);
|
||||
//! everything behind the cap is this battery: a byte string →
|
||||
//! `std::str::from_utf8(..).unwrap_or("")` → the parse (invalid UTF-8
|
||||
@@ -43,7 +43,7 @@ pub fn fuzz_exec_parse(data: &[u8]) {
|
||||
|
||||
/// The from-spec oracle: the grammar re-derived from the dispatch
|
||||
/// module's documented rules, sharing no code with the
|
||||
/// implementation — the differential pair (the repo_id target's
|
||||
/// implementation, the differential pair (the repo_id target's
|
||||
/// oracle pattern).
|
||||
fn spec_parse(command: &str) -> ExecRoute {
|
||||
let (token, argument) = match command.split_once(' ') {
|
||||
@@ -64,7 +64,7 @@ fn spec_parse(command: &str) -> ExecRoute {
|
||||
}
|
||||
|
||||
/// The serving-service arm of the oracle: a serving token must carry
|
||||
/// a non-empty, path-shaped argument (SSH exec must carry the repo —
|
||||
/// a non-empty, path-shaped argument (SSH exec must carry the repo;
|
||||
/// service-only is the decline-with-reason class; anything else
|
||||
/// unparseable is the pipe row).
|
||||
fn spec_serving(service: GitServiceRoute, argument: Option<&str>) -> ExecRoute {
|
||||
@@ -78,9 +78,9 @@ fn spec_serving(service: GitServiceRoute, argument: Option<&str>) -> ExecRoute {
|
||||
}
|
||||
}
|
||||
|
||||
/// The argument grammar re-derived: a path-shaped argument — fully
|
||||
/// single-quote-wrapped (a non-empty inner ending in a quote) or
|
||||
/// unquoted absolute — normalized by stripping the quotes, one
|
||||
/// The argument grammar re-derived: a path-shaped argument, either
|
||||
/// fully single-quote-wrapped (a non-empty inner ending in a quote) or
|
||||
/// unquoted absolute, normalized by stripping the quotes, one
|
||||
/// leading `/`, and one trailing `.git` suffix (an empty-after-strip
|
||||
/// prefix keeps the suffix; the git row's validation declines the
|
||||
/// empties, never a silent emptying).
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! The fingerprint format ingress (ADR-004 decision 1,
|
||||
//! `fingerprint.rs`): arbitrary key blobs through the crate's single
|
||||
//! fingerprint path — the composition of russh's public OpenSSH blob
|
||||
//! fingerprint path: the composition of russh's public OpenSSH blob
|
||||
//! parse (`russh::keys::parse_public_key_base64`, the same re-export
|
||||
//! the crate's surface rides) and the crate's public
|
||||
//! [`alkssh::fingerprint`]. The ingress is fully public API — the
|
||||
//! [`alkssh::fingerprint`]. The ingress is fully public API; the
|
||||
//! recorded exposure decision needs no `#[doc(hidden)]` surface.
|
||||
//!
|
||||
//! Invariants: no-panic on any byte string (a non-blob is the parse's
|
||||
@@ -11,7 +11,7 @@
|
||||
//! accepted key's fingerprint is exactly the `SHA256:` + unpadded
|
||||
//! standard base64 contract shape (one `:` separator, 43-char body,
|
||||
//! canonical alphabet, 32-byte digest decoded exactly); determinism
|
||||
//! (same key, same string — and the same string re-parses to the same
|
||||
//! (same key, same string; the same string re-parses to the same
|
||||
//! verdict). The committed `ssh-keygen -lf` parity vectors
|
||||
//! (`tests/vectors/`) are replayed by the corpus-gate tests below.
|
||||
|
||||
@@ -98,7 +98,7 @@ fn decode_unpadded(body: &str) -> Option<Vec<u8>> {
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// A from-spec unpadded standard-base64 encoder — the re-encode half.
|
||||
/// A from-spec unpadded standard-base64 encoder: the re-encode half.
|
||||
fn encode_unpadded(bytes: &[u8]) -> String {
|
||||
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
//! The key-decode ingress (ADR-005 decision 2, `key_source.rs`): the
|
||||
//! decrypt-at-use chain's one-time `decode_secret_key` conversion —
|
||||
//! decrypt-at-use chain's one-time `decode_secret_key` conversion,
|
||||
//! driven through the production arm itself
|
||||
//! ([`alkssh::decode_key_arm`], the `#[doc(hidden)]` exposure that is
|
||||
//! `decrypt_and_decode`'s exact decode stage, not a fork).
|
||||
//!
|
||||
//! Invariants: no-panic on arbitrary plaintext shapes (the production
|
||||
//! plaintext is the vault's `Zeroizing<String>` — valid UTF-8 by
|
||||
//! plaintext is the vault's `Zeroizing<String>`, valid UTF-8 by
|
||||
//! return type; the battery shapes arbitrary bytes into that form via
|
||||
//! the lossy conversion and drives it through the same zeroized
|
||||
//! wrap the vault's `decrypt` return applies, alkssh 0.2.0 seam) and
|
||||
//! arbitrary passphrase shapes (the
|
||||
//! underlying decode's passphrase arm — the production chain always
|
||||
//! underlying decode's passphrase arm; the production chain always
|
||||
//! passes `None`; the `Some` arm is robustness drive only); every
|
||||
//! failure is the typed [`KeySourceError::Decode`] class — never a
|
||||
//! failure is the typed [`KeySourceError::Decode`] class, never a
|
||||
//! catch-all, never another variant on this arm, never a panic; the
|
||||
//! failure text never carries the plaintext (the assertable half of
|
||||
//! the Zeroizing posture — the plaintext exists solely inside the
|
||||
//! call, dropped zeroized; no retention, no static cache — and the
|
||||
//! the Zeroizing posture: the plaintext exists solely inside the
|
||||
//! call, dropped zeroized, with no retention and no static cache; the
|
||||
//! typed error's only payload is the decode machinery's message
|
||||
//! class, never the plaintext).
|
||||
//!
|
||||
//! The no-echo invariant's assertable form is *the bounded-message
|
||||
//! pair* (reshaped in-task after the first campaign's finding): the
|
||||
//! failure text is a bounded fixed message (≤ 256 bytes — the decode
|
||||
//! failure text is a bounded fixed message (≤ 256 bytes; the decode
|
||||
//! machinery's vocabulary, never a growable echo), and no 256-byte
|
||||
//! contiguous plaintext run appears in it — which, given the bound,
|
||||
//! contiguous plaintext run appears in it, which, given the bound,
|
||||
//! cannot false-fire (a string shorter than 256 bytes cannot contain a
|
||||
//! 256-byte run). A plaintext-`contains`-message check in the other
|
||||
//! direction was unsound: an attacker-chosen plaintext can be a
|
||||
@@ -70,7 +70,7 @@ fn fingerprint(key: &russh::keys::PrivateKey) -> String {
|
||||
}
|
||||
|
||||
/// The typed-class invariant: `Ok` or exactly
|
||||
/// [`KeySourceError::Decode`] — the decode arm's only failure shape —
|
||||
/// [`KeySourceError::Decode`] (the decode arm's only failure shape),
|
||||
/// and the Decode text is the bounded fixed-message pair (see the
|
||||
/// module docs; the two assertions are jointly airtight).
|
||||
fn typed_decode_class(outcome: &Result<russh::keys::PrivateKey, KeySourceError>, plaintext: &str) {
|
||||
@@ -123,7 +123,7 @@ mod invariants {
|
||||
}
|
||||
|
||||
/// A plaintext that is not OpenSSH-PEM key material is a
|
||||
/// [`KeySourceError::Decode`] refusal (the inner-format contract —
|
||||
/// [`KeySourceError::Decode`] refusal (the inner-format contract:
|
||||
/// decision 1, never a fallback).
|
||||
#[test]
|
||||
fn non_key_plaintext_is_the_typed_decode_refusal() {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
//! The terminal-parameters ingress (`tty_map.rs`, OQ-44's reserved
|
||||
//! modes): the `pty_request` args → `TerminalParamsWire` conversion
|
||||
//! driven through the production [terminal_params] surface (the
|
||||
//! `#[doc(hidden)]` exposure — `handler.rs`'s `pty_request` row
|
||||
//! `#[doc(hidden)]` exposure: `handler.rs`'s `pty_request` row
|
||||
//! constructs the stored params with exactly this function).
|
||||
//!
|
||||
//! The harness's framed feed (deterministic, seed-friendly):
|
||||
//! `term_len` (u32 LE, bounded to the remaining input) + the term
|
||||
//! bytes + four u32 LE dimensions (`cols`/`rows`/pixel width/height,
|
||||
//! zero-defaulted when the input runs out) + an arbitrary trailing
|
||||
//! mode blob. The mode blob is *never parsed* — the SSH ttymodes ride
|
||||
//! the reserved `{}` JSON (OQ-44, backends ignore modes in v1) — so
|
||||
//! mode blob. The mode blob is *never parsed*; the SSH ttymodes ride
|
||||
//! the reserved `{}` JSON (OQ-44, backends ignore modes in v1), so
|
||||
//! the battery's totality over arbitrary mode blobs is by
|
||||
//! construction; the framing demonstrates it (the conversion's
|
||||
//! outcome never varies with the blob).
|
||||
//!
|
||||
//! Invariants: no-panic on any term string (empty, unicode, control
|
||||
//! characters) and any u32 dimensions; the dimension clamp is the
|
||||
//! recorded cap (`min(value, 65535)` — a dimension beyond 65535 is
|
||||
//! recorded cap (`min(value, 65535)`; a dimension beyond 65535 is
|
||||
//! nonsense on both papers) computed exactly, never a magnitude
|
||||
//! truncation; `term` is preserved verbatim; the reserved-modes JSON
|
||||
//! is always exactly the empty object; the full wire shape is
|
||||
@@ -89,7 +89,7 @@ pub fn fuzz_terminal_params(data: &[u8]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The cursor's u32 (zero-defaulted when the framed field is absent —
|
||||
/// The cursor's u32 (zero-defaulted when the framed field is absent;
|
||||
/// the feed is total over any input).
|
||||
fn take_u32(cursor: &mut &[u8]) -> u32 {
|
||||
if cursor.len() >= 4 {
|
||||
|
||||
Reference in new issue
Block a user