fuzz: adopt fuzzing (wave 1) + alkcall 0.8.1 / alktunnels 0.2.1 bumps
Adopt the alk*/fuzzing posture (alkcall 0.8.1 + alktunnels 0.2.1 precedent) before the v0.1 publish — the cheapest moment this crate will ever have for it: - alkcall 0.8.1 + alktunnels 0.2.1: the duplicate-adopt/open fix is directly relevant to this crate's consumer open/adopt paths - fuzz/ workspace (the sibling layout): nightly-pinned subtree, stable fuzz/shared library (corpus replay = the fuzz gate), detached runner, JSON dict, deterministic seed generator; 129 committed seeds - three wave-1 targets over already-pub APIs: datagram_codec (the ASSOCIATE frame codec — the alkcall-fuzz-doc-assigned downstream target), rfc1928_codec (the forked RFC 1928 core's handshake state machines, both protocol directions), params_parse (the wire-stable open-op surface) - one diagnostic: DatagramReader::retained_bytes() (the decoder-state bound's assertion target, the alktunnels retained_bytes() posture) - docs/research/fuzzing.md: the scoped plan + implementation log; AGENTS.md gains the corpus-replay verification line - campaigns: 3 × 10-min detached runs exited 0 (datagram ~1.9M execs, rfc1928 ~2.0M, params ~3.1M; oom/timeout/crash 0/0/0 on all 33 fork jobs per target; coverage saturated on both codec targets). No crate bugs; three harness-model defects found + fixed (2 pre-campaign by corpus replay, 2 campaign-found, crash inputs promoted as seeds 900/ 901): the len==0 well-framed decode partition, the fieldless-struct serde-seq form, the BadAtyp re-frame arm, the short-domain count-walk gate - wave 2 (session_pumps stateful target) deferred to post-0.1 — the byte-level targets carry the attacker-reachable load for the publish Verification: cargo test (15 suites ok, default + all-features), clippy (all-targets + wasm + fuzz/shared), fmt, cargo doc, publish --dry-run, corpus replay 129 seeds green, cargo fuzz build clean. Refs: alkcall docs/research/fuzzing.md (§7.9 posture), alktunnels docs/research/fuzzing.md; docs/research/fuzzing.md (this crate's copy)
This commit is contained in:
1 parent
8af23eeee0
commit
08a3694464
149 files changed
+1822
-5
No files matched your search
@@ -273,11 +273,19 @@ cargo clippy --all-targets -- -D warnings
|
|||||||
cargo fmt --check
|
cargo fmt --check
|
||||||
cargo doc --no-deps # if docs changed
|
cargo doc --no-deps # if docs changed
|
||||||
cargo test --all-features # if features are added
|
cargo test --all-features # if features are added
|
||||||
|
cargo test --manifest-path fuzz/shared/Cargo.toml # corpus replay (the fuzz gate)
|
||||||
cargo check --target wasm32-unknown-unknown # default crate stays wasm-clean
|
cargo check --target wasm32-unknown-unknown # default crate stays wasm-clean
|
||||||
cargo clippy --target wasm32-unknown-unknown -- -D warnings
|
cargo clippy --target wasm32-unknown-unknown -- -D warnings
|
||||||
cargo publish --dry-run --allow-dirty # before a release
|
cargo publish --dry-run --allow-dirty # before a release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The corpus replay is the fuzz gate (the alkcall §7.9 posture): it
|
||||||
|
replays every committed seed through the same invariant functions the
|
||||||
|
fuzz targets run, on stable, without nightly. Campaigns (nightly,
|
||||||
|
cargo-fuzz) run manually via `fuzz/run-detached.sh` when
|
||||||
|
`src/wire.rs`, `src/rfc1928/`, or `src/params.rs` change — never as a
|
||||||
|
foreground child of an agent session (docs/research/fuzzing.md §5).
|
||||||
|
|
||||||
The wasm check is the structural guard for the "default crate is
|
The wasm check is the structural guard for the "default crate is
|
||||||
wasm-clean" invariant (same as alktty/alktunnels) — run it whenever a
|
wasm-clean" invariant (same as alktty/alktunnels) — run it whenever a
|
||||||
non-backend module changes. It is a hard gate: the RFC 1928 core is
|
non-backend module changes. It is a hard gate: the RFC 1928 core is
|
||||||
|
|||||||
+13
-5
@@ -9,7 +9,11 @@ repository = "https://git.alk.dev/alkdev/alksocks"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["socks5", "proxy", "rfc1928", "channels", "alkcall"]
|
keywords = ["socks5", "proxy", "rfc1928", "channels", "alkcall"]
|
||||||
categories = ["network-programming", "asynchronous"]
|
categories = ["network-programming", "asynchronous"]
|
||||||
exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/research/", "docs/plans/", "tasks/", "docs/sdd_process.md"]
|
exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/research/", "docs/plans/", "tasks/", "docs/sdd_process.md", "fuzz/"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["."]
|
||||||
|
exclude = ["fuzz"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "alksocks"
|
name = "alksocks"
|
||||||
@@ -24,8 +28,10 @@ composed = ["dep:alktunnels"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# The substrate (convention 11): pinned, bump deliberately, never
|
# The substrate (convention 11): pinned, bump deliberately, never
|
||||||
# transitively. First consumption of the 0.8.0 surface.
|
# transitively. 0.8.1: the duplicate-adopt/open fix (the alkcall 0.8.1
|
||||||
alkcall = "0.8.0"
|
# fuzz finding — the consumer sessions' open/adopt paths are exposed to
|
||||||
|
# the replayed-open-op scenario its changelog names).
|
||||||
|
alkcall = "0.8.1"
|
||||||
tokio = { version = "1", default-features = false, features = ["rt", "sync", "io-util", "macros", "time"] }
|
tokio = { version = "1", default-features = false, features = ["rt", "sync", "io-util", "macros", "time"] }
|
||||||
# The fork's declared logging seam (rfc1928.md: upstream `log` calls
|
# The fork's declared logging seam (rfc1928.md: upstream `log` calls
|
||||||
# were dropped or re-pointed here); no source file references it yet —
|
# were dropped or re-pointed here); no source file references it yet —
|
||||||
@@ -40,7 +46,9 @@ futures = { version = "0.3", default-features = false, features = ["std"] }
|
|||||||
# The sibling crate (the composed egress's direct-op vehicle — ADR-008/
|
# The sibling crate (the composed egress's direct-op vehicle — ADR-008/
|
||||||
# ADR-009): rides `composed` alone. Pinned; bump deliberately
|
# ADR-009): rides `composed` alone. Pinned; bump deliberately
|
||||||
# (convention 11's deliberate-bump rule applies to siblings too).
|
# (convention 11's deliberate-bump rule applies to siblings too).
|
||||||
alktunnels = { version = "0.2.0", optional = true }
|
# 0.2.1: the fuzzing graduation (its wave-1/wave-2 targets + the
|
||||||
|
# alkcall 0.8.1 bump), bug-fix release, no API/wire change.
|
||||||
|
alktunnels = { version = "0.2.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["full", "test-util", "macros"] }
|
tokio = { version = "1", features = ["full", "test-util", "macros"] }
|
||||||
@@ -48,4 +56,4 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|||||||
# The composed suite's topology (the alktunnels direct-op producer) —
|
# The composed suite's topology (the alktunnels direct-op producer) —
|
||||||
# dev-only: the test binary gates itself on the `composed` feature
|
# dev-only: the test binary gates itself on the `composed` feature
|
||||||
# (`#![cfg(feature = "composed")]`), the dev-dep keeps it compiling.
|
# (`#![cfg(feature = "composed")]`), the dev-dep keeps it compiling.
|
||||||
alktunnels = "0.2.0"
|
alktunnels = "0.2.1"
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
# Fuzzing alksocks: Research and Plan
|
||||||
|
|
||||||
|
**Status:** adopted — wave-1 targets (`datagram_codec`, `rfc1928_codec`,
|
||||||
|
`params_parse`) implemented, seeds committed, campaigns run clean (§7).
|
||||||
|
Wave 2 (`session_pumps`) deferred to post-0.1 (§4). Campaigns run via the
|
||||||
|
detached runner (§6) — **never as a foreground child of an agent
|
||||||
|
session** (§5).
|
||||||
|
**Date:** 2026-09-28
|
||||||
|
**Upstream precedent:** `/workspace/@alkdev/alkcall/docs/research/fuzzing.md`
|
||||||
|
(one real finding: the duplicate adopt/open destroying the live channel —
|
||||||
|
fixed in 0.8.1) and
|
||||||
|
`/workspace/@alkdev/alktunnels/docs/research/fuzzing.md` (wave-1/wave-2
|
||||||
|
adopted, six harness-model findings, no crate bugs). Tooling, corpus
|
||||||
|
policy, runner, and the no-hosted-CI posture are inherited from those
|
||||||
|
docs wholesale — this doc does not re-litigate them; it scopes them to
|
||||||
|
this crate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Should this crate be fuzzed?
|
||||||
|
|
||||||
|
**Yes — and alkcall's fuzz doc already assigned this to us.** Three
|
||||||
|
lines, all pointing the same way:
|
||||||
|
|
||||||
|
1. **alkcall's fuzz doc §5 named this crate's surfaces as downstream
|
||||||
|
targets.** Its "non-surfaces (deliberately)" note ends: *the channels
|
||||||
|
layer carries opaque `Bytes` and the handler owns sub-stream framing
|
||||||
|
(ADR-035) — those downstream parsers are downstream crates' fuzz
|
||||||
|
targets.* Our `src/wire.rs` datagram codec **is** that named
|
||||||
|
downstream target (shaped on alktunnels' codec, which alktunnels
|
||||||
|
fuzzed as its wave-1 first target — the same assignment, landed
|
||||||
|
here). Fuzzing it completes the upstream scope rather than
|
||||||
|
duplicating it.
|
||||||
|
|
||||||
|
2. **We own an entire RFC handshake state machine fed by arbitrary
|
||||||
|
peers — on both sides.** The targeted fork (`src/rfc1928/`, ADR-013)
|
||||||
|
is fully owned code with project conventions applying unconditionally
|
||||||
|
(convention 17). Its parse surfaces — `read_address` (the address
|
||||||
|
codec), `negotiate_auth` (method negotiation), `read_command` (the
|
||||||
|
command encoding), `read_username_password` (RFC 1929), the client's
|
||||||
|
`read_request_reply` — are fed bytes produced by **arbitrary peers**
|
||||||
|
in both directions: the producer drives the server typestate against
|
||||||
|
consumer bytes, and the consumer sessions + front doors drive the
|
||||||
|
client path against producer bytes. An RFC state machine is exactly
|
||||||
|
the shape the quinn-proto CVE class (RUSTSEC-2026-0037 — remote DoS
|
||||||
|
via `unwrap()` in parsing, the upstream doc §1's strongest
|
||||||
|
datapoint) lives in: hand-rolled state transitions, length arithmetic,
|
||||||
|
and boundary handling. One fork-carried correction is already of this
|
||||||
|
class — upstream's `ReplyError::from_u8` ended in `unreachable!()`
|
||||||
|
for unassigned reply codes; the fork made it a typed catch-all (the
|
||||||
|
review's F-1) precisely because the value comes off the wire. A fuzz
|
||||||
|
target is how that posture is *demonstrated* across the whole codec,
|
||||||
|
not believed spotwise.
|
||||||
|
|
||||||
|
3. **The differential suite's blind spot is exactly fuzzing's
|
||||||
|
strength.** `tests/rfc1928-differential.rs` pins our fork's behavior
|
||||||
|
against the reference checkout on *valid* RFC edge cases — curated
|
||||||
|
inputs a human thought of. What no example-based suite provides:
|
||||||
|
random *malformed* input exploration (the panic-on-attacker-bytes
|
||||||
|
class), stateful interleavings, and resource-bound assertions under
|
||||||
|
adversarial sequences. The fork is about to be published (0.1) as
|
||||||
|
wire-stable; after that, targets are regression protection for a
|
||||||
|
frozen surface — cheapest now.
|
||||||
|
|
||||||
|
**The honest caveat** (mirroring both upstream docs): the crate has no
|
||||||
|
`unsafe` (checked at the crate root), the codec's decode direction is
|
||||||
|
infallible by contract (oversize rejected at frame time; the decoder
|
||||||
|
buffers, bounded by the u16 declared length), the RFC 1929 length
|
||||||
|
fields are one byte each (the attacker-controlled byte cannot ask for
|
||||||
|
more than 255), and the fork's error vocabulary is typed end-to-end
|
||||||
|
(the `unreachable!()`/`unwrap` sweep already ran in review 001).
|
||||||
|
Expected yield is therefore not memory-safety bugs; it is (a) the
|
||||||
|
demonstration that the infallibility/no-panic contracts hold on
|
||||||
|
adversarial input, (b) the decoder-state bound assertion (alktunnels
|
||||||
|
§1.3's finding class — a peer chunk amplifying into unbounded state),
|
||||||
|
and (c) regression protection for the published wire surface. If the
|
||||||
|
campaigns find nothing, that is the successful-negative-result posture
|
||||||
|
alkcall's §1 codified.
|
||||||
|
|
||||||
|
## 2. Toolchain (inherited, not re-decided)
|
||||||
|
|
||||||
|
Identical to both upstream docs: **cargo-fuzz 0.13.2 + `arbitrary`
|
||||||
|
1.4.x**, nightly confined to the `fuzz/` subtree via
|
||||||
|
`fuzz/rust-toolchain.toml`, the crate itself stays stable at MSRV 1.88.
|
||||||
|
AFL optional/deferred; bolero deferred (the async-friction argument);
|
||||||
|
libafl/honggfuzz/OSS-Fuzz not adopted. Sanitizer: `--sanitizer=address`
|
||||||
|
with debug assertions (cargo-fuzz defaults) — no MSAN (no `unsafe` in
|
||||||
|
this crate).
|
||||||
|
|
||||||
|
**Layout (the upstream pattern, load-bearing):**
|
||||||
|
|
||||||
|
```
|
||||||
|
fuzz/
|
||||||
|
├── Cargo.toml alksocks-fuzz (nightly-only bins; own [workspace])
|
||||||
|
├── rust-toolchain.toml pins nightly for this subtree only
|
||||||
|
├── fuzz_targets/ thin fuzz_target! wrappers (3-line each)
|
||||||
|
│ ├── datagram_codec.rs
|
||||||
|
│ ├── rfc1928_codec.rs
|
||||||
|
│ └── params_parse.rs
|
||||||
|
├── shared/ alksocks-fuzz-shared — STABLE-toolchain library
|
||||||
|
│ └── src/… invariant logic + corpus replay (§3)
|
||||||
|
├── corpus/<target>/ committed hand-made seeds (§3)
|
||||||
|
├── gen_fuzz_seeds.py deterministic seed generator (quiche pattern)
|
||||||
|
├── run-detached.sh §6 detached runner (CWD-independent)
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Invariant logic lives in `fuzz/shared/`**, not inline in the target
|
||||||
|
binaries — the stable-toolchain corpus replay
|
||||||
|
(`cargo test --manifest-path fuzz/shared/Cargo.toml`) runs the SAME
|
||||||
|
invariant functions the fuzzer runs, on stable, in the release
|
||||||
|
verification checklist. This is alkcall §7.2 tier-3's deliverable
|
||||||
|
minus automation; per alkcall §7.9 (adopted here identically) this
|
||||||
|
repo has no hosted CI and will not get one — corpus replay as plain
|
||||||
|
`cargo test` is the fuzz gate.
|
||||||
|
- **Root `Cargo.toml` needs the explicit `[workspace]` table**
|
||||||
|
(`members = ["."]`, `exclude = ["fuzz"]`) and `"fuzz/"` on the
|
||||||
|
publish exclude list — without it cargo auto-discovers `fuzz/shared/`
|
||||||
|
into the main workspace and the MSRV toolchain tries to build the
|
||||||
|
nightly-consumed dev-deps (the exact failure alkcall hit).
|
||||||
|
- **Corpus policy:** commit hand-made seeds, gitignore grown corpora
|
||||||
|
(quinn/h2 model). Seeds per target listed in §3.
|
||||||
|
|
||||||
|
## 3. Invariants by target
|
||||||
|
|
||||||
|
### 3.1 `datagram_codec` (the ASSOCIATE frame codec, `src/wire.rs`)
|
||||||
|
|
||||||
|
Input: raw `&[u8]`. Drives `DatagramReader::feed` + `frame` /
|
||||||
|
`frame_inner` / `addr_encoding_len`. This is the alktunnels wave-1
|
||||||
|
`datagram_codec` target's shape, scoped to this crate's codec (the
|
||||||
|
frame carries `addr + data` inner bytes and decodes to
|
||||||
|
`(TargetAddr, payload)`, not opaque datagrams — the invariants follow
|
||||||
|
the inner structure). Invariants:
|
||||||
|
|
||||||
|
- **no-panic on any byte sequence** (the baseline).
|
||||||
|
- **infallible reassembly**: `feed` never errors at the reassembly
|
||||||
|
layer — every frame completes as `Ok` or as one of the four
|
||||||
|
*decode-garbage* `WireError` variants (`EmptyFrame` / `BadAtyp` /
|
||||||
|
`ShortAddress` / `DomainNotUtf8`); `Oversize`/`Address` are
|
||||||
|
unreachable from `feed` (they are frame-time errors — a shape
|
||||||
|
assertion pins the partition).
|
||||||
|
- **empty-payload F-2 layering**: a decoded `(addr, empty)` pair comes
|
||||||
|
only from a real `[len][addr]` frame whose payload region is empty —
|
||||||
|
never invented by the codec; a `len == 0` frame (zero inner bytes)
|
||||||
|
always surfaces as `EmptyFrame`, never as an empty datagram.
|
||||||
|
- **bad-ATYP loudness**: every `BadAtyp` error's byte is the actual
|
||||||
|
first inner byte of its frame, and it is not one of the three
|
||||||
|
RFC ATYPs (the post-reply control-byte semantic — any non-ATYP
|
||||||
|
control byte must fail loud, never guess).
|
||||||
|
- **consumption accounting / splitting transparency**: feeding in
|
||||||
|
3-byte slices yields exactly the datagram sequence the whole-input
|
||||||
|
feed yields (both directions' framing is transparent to splitting).
|
||||||
|
- **round-trip**: every decoded datagram re-frames via `frame` (or
|
||||||
|
`frame_inner` over its inner bytes) and re-decodes to itself;
|
||||||
|
well-framed input re-frames byte-for-byte.
|
||||||
|
- **joint bound**: `frame` rejects iff
|
||||||
|
`addr_encoding_len(addr) + payload.len() > MAX_INNER_LEN`, echoing
|
||||||
|
`(total, max)` exactly — including the max-domain case where a legal
|
||||||
|
RFC payload (65507 to a 255-char domain = 65766) must reject (the
|
||||||
|
joint-bound reason the codec exists); `frame_inner` rejects iff
|
||||||
|
`inner.len() > MAX_INNER_LEN`. The `checked_add` path (payload
|
||||||
|
overflow wrapping the u16) rejects, never wraps.
|
||||||
|
- **decoder-state bound**: `buf` + in-flight `current` stay bounded by
|
||||||
|
the fed chunk size plus one frame (header + `MAX_INNER_LEN`) on any
|
||||||
|
adversarial input — the alktunnels §1.3 finding-class assertion
|
||||||
|
(state must never grow with input). The crate's `DatagramReader`
|
||||||
|
exposes this via `is_mid_frame()` + a new `retained_bytes()`
|
||||||
|
diagnostic (the alktunnels `retained_bytes()` posture).
|
||||||
|
|
||||||
|
Seeds: valid frames (all three ATYPs), truncation at every prefix
|
||||||
|
length, `len = 0` / `len = MAX` / `len = MAX+1`, empty payloads,
|
||||||
|
batched frames, split frames, garbage ATYP, non-UTF-8 domains, the
|
||||||
|
65766 joint-bound shape, deep batch/split mixes.
|
||||||
|
|
||||||
|
### 3.2 `rfc1928_codec` (the forked RFC 1928 core, `src/rfc1928/`)
|
||||||
|
|
||||||
|
Input: raw `&[u8]`. Drives the fork's byte-level parsers over tokio
|
||||||
|
duplex streams (the in-tree test shape — mock bytes, no sockets):
|
||||||
|
|
||||||
|
- **`read_address`** (the address codec, both directions consume it):
|
||||||
|
no-panic on any ATYP/bytes; every failure is a typed `AddrError`
|
||||||
|
(`IncorrectAddressType` for unknown ATYP — the F-1 class, already
|
||||||
|
fixed; never a panic); round-trip for every well-formed address
|
||||||
|
(`to_be_bytes` → parse → equal).
|
||||||
|
- **the server handshake** (`negotiate_auth` → `read_command` over a
|
||||||
|
duplex, `accept_no_auth` + `read_command`): no-panic on any byte
|
||||||
|
sequence; every malformed shape is a typed `SocksServerError`
|
||||||
|
(version mismatch, unacceptable method, unknown command, bad addr);
|
||||||
|
a well-formed negotiation + CONNECT request parses to the exact
|
||||||
|
command + target.
|
||||||
|
- **the client reply path** (`read_request_reply`): no-panic on any
|
||||||
|
server byte sequence; unassigned reply codes map to the
|
||||||
|
`GeneralFailure` catch-all (the F-1 pin — never `unreachable!()`);
|
||||||
|
version mismatches are typed errors.
|
||||||
|
- **`parse_udp_request`** (the RFC UDP header codec, front doors only
|
||||||
|
but still wire-reachable): no-panic; garbage RSV / unknown ATYP /
|
||||||
|
truncation are typed `UdpHeaderError`s; round-trip with
|
||||||
|
`new_udp_header`.
|
||||||
|
- **auth sub-handshakes** (`read_username_password`, RFC 1929):
|
||||||
|
no-panic; the u8 length fields bound allocations structurally
|
||||||
|
(≤ 255 bytes — asserted); empty username/password are typed errors;
|
||||||
|
round-trip accept/reject status bytes.
|
||||||
|
- **`ReplyError::from_u8`** full-domain table: every `u8` maps, no
|
||||||
|
panic, unassigned → catch-all (the direct probe of the F-1 class).
|
||||||
|
|
||||||
|
This is the wave-1 target of highest value: it is the entire RFC 1928
|
||||||
|
byte surface, fed attacker bytes from both protocol directions, and the
|
||||||
|
fork is owned code whose behavior the differential suite pins only on
|
||||||
|
valid inputs.
|
||||||
|
|
||||||
|
Seeds: valid handshakes (no-auth / password), version mismatches,
|
||||||
|
truncations at every prefix length of each message shape, unknown
|
||||||
|
methods/commands/ATYPs, empty and 255-byte domains, non-UTF-8 domains,
|
||||||
|
unassigned reply codes, RSV garbage.
|
||||||
|
|
||||||
|
### 3.3 `params_parse` (the open-op params, `src/params.rs`)
|
||||||
|
|
||||||
|
Input: raw bytes → `serde_json` (alkcall's bounded `arbitrary_value`
|
||||||
|
generator) → `Socks5Params` deserialization + `socks5_open_spec()` +
|
||||||
|
`SocksEstablishmentReason::parse` / `establishment_reason`. Invariants:
|
||||||
|
|
||||||
|
- **no-panic on any JSON-shaped input** (`deny_unknown_fields`
|
||||||
|
dispatch, the registry-spec builder).
|
||||||
|
- **the trivial-params pin**: `{}` parses, `Socks5Params::as_json()` ==
|
||||||
|
`{}`, and unknown fields always reject (the loud-rejection posture —
|
||||||
|
the serde pin and the schema's `additionalProperties: false` agree).
|
||||||
|
- **the spec is a constant**: `socks5_open_spec()`'s wire-visible
|
||||||
|
fields (op id, ALPN, scopes, schemas) are identical on every
|
||||||
|
execution — the open-op surface is wire-stable (ADR-001); the
|
||||||
|
reason-enum table equals the typed parse table (the schema/typed-view
|
||||||
|
drift guard, mirrored from the in-suite test).
|
||||||
|
- **the reason surface is a pass-through extraction, not a validated
|
||||||
|
table**: `establishment_reason` yields `Some(details.reason)` iff the
|
||||||
|
code is `channel:open_failed` and the reason is a string — any string
|
||||||
|
is surfaced as-is (the alktunnels harness-model finding #2, adopted
|
||||||
|
here as the modeled contract); `SocksEstablishmentReason::parse`
|
||||||
|
never panics and is total.
|
||||||
|
|
||||||
|
### 3.4 What is deliberately NOT a target (yet)
|
||||||
|
|
||||||
|
- **The producer pump (`serve_socks5_over`) / consumer sessions as
|
||||||
|
stateful op-sequence targets** — wave 2, deferred (§4). The
|
||||||
|
handshake-level target (§3.2) covers the byte surface; the
|
||||||
|
session-level interleavings (associate stage select-shape, BIND quiet
|
||||||
|
wait, the two-pump CONNECT plane) are stateful shapes over a live
|
||||||
|
channels stack — the alktunnels wave-2 pattern, worth its own session
|
||||||
|
post-0.1.
|
||||||
|
- **The doors (`door.rs`, `local/`, `composed/`)** — feature-gated
|
||||||
|
assembly; their RFC-codec boundary (`parse_udp_request` /
|
||||||
|
`new_udp_header`) is covered by the §3.2 target. The rest is I/O
|
||||||
|
plumbing over the fuzzed cores, not parse surfaces.
|
||||||
|
- **`front_door` RFC 1929 negotiation** — covered by §3.2's auth
|
||||||
|
sub-handshake invariants (the door drives the same forked parsers).
|
||||||
|
|
||||||
|
## 4. Scope: two waves
|
||||||
|
|
||||||
|
**Wave 1 (this session):** the three targets above. All drive
|
||||||
|
already-`pub` APIs — no `#[cfg(fuzzing)]` exposure needed (matching
|
||||||
|
both upstream docs' experience across all eight targets to date). One
|
||||||
|
diagnostic addition to the crate: `DatagramReader::retained_bytes()`
|
||||||
|
(the decoder-state byte count §3.1's bound asserts against, in
|
||||||
|
alktunnels' `retained_bytes()` style).
|
||||||
|
|
||||||
|
**Wave 2 (deferred — post-0.1):** `session_pumps`, the stateful
|
||||||
|
op-sequence target over the producer pump + consumer sessions in the
|
||||||
|
alkcall `manager_routing` / alktunnels `session_pumps` style (live
|
||||||
|
channels stack; ops over CONNECT/ASSOCIATE/BIND interleavings; the
|
||||||
|
two-pump shutdown-on-completion contract, the egress readiness
|
||||||
|
contract, and the datagram-stage exit matrix as invariants). Deferred
|
||||||
|
because wave 1 covers the wire byte surface completely, the session
|
||||||
|
harness is a large build, and the crate is one commit from its first
|
||||||
|
publish — the alktunnels sequencing (wave 1 at 0.2.0's fuzz adoption,
|
||||||
|
wave 2 the same session) was possible there because its session shape
|
||||||
|
was the crate's whole surface; here the byte-level targets carry the
|
||||||
|
attacker-reachable load and the stateful session target is a separate
|
||||||
|
planning unit.
|
||||||
|
|
||||||
|
Sequencing mirrors the upstream docs: init + wave-1 targets → 10–30
|
||||||
|
min campaigns per target via the detached runner → fix anything found →
|
||||||
|
wave 2 when scheduled.
|
||||||
|
|
||||||
|
## 5. Operational isolation: campaigns never share fate with the agent session
|
||||||
|
|
||||||
|
**Adopted verbatim from the upstream docs (alkcall §7.6, alktunnels
|
||||||
|
§5)** — an environment constraint, not a nice-to-have. Agent sessions
|
||||||
|
run fuzzer builds/campaigns through a bash tool that spawns the fuzzer
|
||||||
|
as a child of the session's own process tree and cgroup; an OOM in a
|
||||||
|
fuzz target must cost the fuzzer, never the session. Three layers, all
|
||||||
|
mandatory: (1) libFuzzer soft limits (`-rss_limit_mb=2048`
|
||||||
|
`-malloc_limit_mb=2048`) — never `ulimit -v` under ASAN; (2) fork mode
|
||||||
|
(`-fork=1`) as blast-radius containment (crash/timeout/OOM kills only
|
||||||
|
the short-lived child; the harness survives and persists the artifact;
|
||||||
|
also the deadlock answer); (3) process detachment — the runner script
|
||||||
|
wraps `cargo fuzz run` in `setsid` + `nohup` + full redirection to a
|
||||||
|
log file; poll, never `wait`.
|
||||||
|
|
||||||
|
Also inherited: the operational note that `cargo fuzz run` returns
|
||||||
|
before its last fork-mode children fully exit — prefer `pgrep -af` to
|
||||||
|
LIST, kill by explicit pid from the runner's echoed `pid=`, and never
|
||||||
|
pattern-kill-and-recheck in one call.
|
||||||
|
|
||||||
|
## 6. No hosted CI
|
||||||
|
|
||||||
|
Per alkcall §7.9 / alktunnels §6 — the standing policy applies to this
|
||||||
|
repo identically: no hosted CI will exist; do not add Actions/Gitea
|
||||||
|
Actions workflow files. **Corpus replay is the fuzz gate as plain
|
||||||
|
`cargo test`**: `cargo test --manifest-path fuzz/shared/Cargo.toml`
|
||||||
|
replays every committed seed through the invariant functions — add it
|
||||||
|
to the release verification checklist (AGENTS.md). Campaigns run
|
||||||
|
manually via the detached runner (§6) — e.g. before a release, or after
|
||||||
|
touching `src/wire.rs`, `src/rfc1928/`, or `src/params.rs`.
|
||||||
|
|
||||||
|
## 7. Implementation log
|
||||||
|
|
||||||
|
- **2026-09-28 — proposal adopted** (this doc). Wave-1 targets
|
||||||
|
`datagram_codec` + `rfc1928_codec` + `params_parse`; wave 2 deferred
|
||||||
|
(§4).
|
||||||
|
- **2026-09-28 — `alkcall` bumped 0.8.0 → 0.8.1,
|
||||||
|
`alktunnels` 0.2.0 → 0.2.1.** Bug-fix releases, no API/wire change;
|
||||||
|
alkcall's fix (the duplicate adopt/open destroying the live channel)
|
||||||
|
is directly relevant to this crate's consumer sessions — every
|
||||||
|
session's teardown reaps via the manager, and the
|
||||||
|
open-op-response-replayed scenario alkcall's changelog names is the
|
||||||
|
consumer open path. Verification: full suite (default + all-features)
|
||||||
|
green, clippy, fmt, wasm check (§8 commands).
|
||||||
|
- **2026-09-28 — wave-1 implemented.** The `fuzz/` workspace laid out
|
||||||
|
per §2 (nightly-pinned subtree, stable `fuzz/shared` library, runner,
|
||||||
|
dict, generator); the three targets over already-`pub` APIs — no
|
||||||
|
`#[cfg(fuzzing)]` exposure needed (the upstream docs' experience,
|
||||||
|
now across eleven targets). One diagnostic addition to the crate:
|
||||||
|
`DatagramReader::retained_bytes()` (§3.1's bound target). Seeds
|
||||||
|
committed: 45 + 65 + 19 generated (fuzz/gen_fuzz_seeds.py) + 2
|
||||||
|
campaign-found crash inputs promoted (`datagram_codec/seed-900`,
|
||||||
|
`seed-901` — the §7 harness findings below).
|
||||||
|
- **2026-09-28 — two harness-model defects caught by corpus replay
|
||||||
|
pre-campaign** (the alkcall §7.7 fuzzer-as-harness-checker pattern):
|
||||||
|
1. **A `len == 0` frame is well-framed but decodes to
|
||||||
|
`Err(EmptyFrame)`** — the re-frame round-trip first expected every
|
||||||
|
well-framed frame to decode `Ok`. The frame count still holds over
|
||||||
|
every frame; the byte-for-byte re-frame holds over the decodable
|
||||||
|
ones; `EmptyFrame` is pinned as the only zero-inner-byte shape.
|
||||||
|
The same arm widened for `ShortAddress`/`DomainNotUtf8`/
|
||||||
|
`BadAtyp`: a well-framed frame can carry any addr-garbage shape —
|
||||||
|
the decode refuses the addr, and the raw frame is the only
|
||||||
|
re-frame possible.
|
||||||
|
2. **A fieldless `deny_unknown_fields` struct deserializes from ANY
|
||||||
|
empty JSON composite** (the serde seq form `[]` included) — not
|
||||||
|
only `{}`. Not wire-reachable (the registry's `type: object`
|
||||||
|
input schema is the gate that keeps non-object forms from ever
|
||||||
|
reaching the type), but the harness must model the serde reality:
|
||||||
|
the trivial-params invariant is now "an empty composite parses,
|
||||||
|
everything non-empty rejects, the canonical round-trip is `{}`."
|
||||||
|
- **2026-09-28 — first campaign finding (fork-mode containment as
|
||||||
|
designed): a third harness-model defect.** The `datagram_codec`
|
||||||
|
campaign crashed at ~35 s on input `0005 0301 1203 03bb 00`
|
||||||
|
(`BadAtyp(0)` shape) — the re-frame walk's `Err` arm (fixed in the
|
||||||
|
replay pass above) still omitted `BadAtyp`. Fixed; the crash input is
|
||||||
|
committed as `seed-900`; the campaign relaunched clean.
|
||||||
|
- **2026-09-28 — second campaign finding (also harness, fork-mode
|
||||||
|
containment): the count walk's `declared >= 7` gate excluded short
|
||||||
|
domains.** Input `0005 0301 1203 03bb 00` — a domain frame with a
|
||||||
|
1-byte domain (`addr_len = 5`) is a legal empty-payload frame, but
|
||||||
|
the walk's `declared >= 7` gate (meant to skip addr-less frames)
|
||||||
|
rejected it; the per-ATYP `addr_len` computation already does that
|
||||||
|
job. The gate lowered to `>= 4` (the minimum ATYP + length-byte
|
||||||
|
prefix the walk reads); the crash input is committed as `seed-901`;
|
||||||
|
the campaign relaunched clean.
|
||||||
|
- **2026-09-28 — wave-1 campaigns run (10-min detached runs, §6
|
||||||
|
runner; `-fork=1 -rss_limit_mb=2048 -malloc_limit_mb=2048
|
||||||
|
-timeout=25`; codec `-max_len=65536`, rfc1928
|
||||||
|
`-max_len=4096 -use_value_profile=1`, params
|
||||||
|
`-dict=json.dict -use_value_profile=1). All three exited 0 with
|
||||||
|
empty artifact directories — no crashes, hangs, OOMs, or leaks.**
|
||||||
|
- `datagram_codec` (post-harness-fixes) — ~1.9M execs (~3k exec/s),
|
||||||
|
33 fork jobs, `oom/timeout/crash: 0/0/0` throughout. Coverage
|
||||||
|
saturated at 481 edges (ft 2222, corp 209) — the 2-byte-header
|
||||||
|
state machine is fully enumerated (the alktunnels wave-1
|
||||||
|
saturation profile).
|
||||||
|
- `rfc1928_codec` — ~2.0M execs (~3.6k exec/s), 33 fork jobs,
|
||||||
|
`oom/timeout/crash: 0/0/0` throughout. Coverage saturated at 1429
|
||||||
|
edges (ft 7987, corp 704) — the handshake state machines'
|
||||||
|
byte-level shape space is fully enumerated; both protocol
|
||||||
|
directions held across every adversarial sequence.
|
||||||
|
- `params_parse` — ~3.1M execs (~5.5k exec/s), 33 fork jobs,
|
||||||
|
`oom/timeout/crash: 0/0/0` throughout. Coverage 1512 edges (ft
|
||||||
|
12970, in-memory corpus 2981 from the 19 committed seeds) — the
|
||||||
|
serde/untagged shape space was still expanding at budget end; the
|
||||||
|
spec-constant and pass-through-reason invariants held everywhere.
|
||||||
|
- The decoder-state bound (§3.1) held everywhere:
|
||||||
|
`retained_bytes()` never exceeded fed + one frame on any
|
||||||
|
adversarial input. Grown corpora were **not** merged into the
|
||||||
|
committed seeds (the §2 policy).
|
||||||
|
- **NO crate bugs found by wave 1.** All three findings this session
|
||||||
|
were harness-model defects, each invisible to example-based review
|
||||||
|
and caught by fuzzing/replay — the upstream "fuzzer as a
|
||||||
|
harness-checker" pattern, confirmed a third time (alkcall §7.7,
|
||||||
|
alktunnels §7, here §7). The codec's contracts (infallible
|
||||||
|
reassembly, the F-2 layering, the joint bound, the retained-bytes
|
||||||
|
bound) and the RFC core's no-panic posture held across every
|
||||||
|
adversarial input the campaigns generated.
|
||||||
|
- (progress appended as implementation proceeds — see git log)
|
||||||
|
|
||||||
|
## 8. Verification commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test # main suite (stable)
|
||||||
|
cargo test --all-features # the feature-gated suite
|
||||||
|
cargo test --manifest-path fuzz/shared/Cargo.toml # corpus replay (fuzz gate)
|
||||||
|
cargo fuzz build # in fuzz/ (nightly, ASAN)
|
||||||
|
fuzz/run-detached.sh <target> # campaigns (never foreground)
|
||||||
|
cargo clippy --all-targets -- -D warnings
|
||||||
|
cargo fmt --check
|
||||||
|
cargo clippy --manifest-path fuzz/shared/Cargo.toml --all-targets -- -D warnings
|
||||||
|
cargo fmt --manifest-path fuzz/shared/Cargo.toml --check
|
||||||
|
cargo check --target wasm32-unknown-unknown # default crate stays wasm-clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Campaign flags per target: `datagram_codec -max_len=65536`;
|
||||||
|
`rfc1928_codec -max_len=4096 -use_value_profile=1`;
|
||||||
|
`params_parse -dict=json.dict -use_value_profile=1`.
|
||||||
|
|
||||||
|
## 9. References
|
||||||
|
|
||||||
|
- Upstream campaigns + operational model (authoritative for tooling,
|
||||||
|
runner, corpus policy, no-CI posture):
|
||||||
|
`/workspace/@alkdev/alkcall/docs/research/fuzzing.md`,
|
||||||
|
`/workspace/@alkdev/alktunnels/docs/research/fuzzing.md`
|
||||||
|
- alkcall 0.8.1 changelog — the finding our consumer open/adopt paths
|
||||||
|
are exposed to
|
||||||
|
- This crate: ADR-001 (op/params), ADR-003 (the frame codec), ADR-013
|
||||||
|
(the targeted fork), `docs/architecture/wire.md`,
|
||||||
|
`docs/architecture/rfc1928.md`
|
||||||
|
- External: RUSTSEC-2026-0037 (quinn-proto — the maintainers'
|
||||||
|
fuzzing-coverage statement), rust-fuzz trophy case, cargo-fuzz Rust
|
||||||
|
Fuzz Book (all linked from the upstream doc's §9)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
target
|
||||||
|
corpus/*
|
||||||
|
!corpus/datagram_codec
|
||||||
|
!corpus/rfc1928_codec
|
||||||
|
!corpus/params_parse
|
||||||
|
artifacts
|
||||||
|
coverage
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "alksocks-fuzz"
|
||||||
|
version = "0.0.0"
|
||||||
|
publish = false
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
cargo-fuzz = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libfuzzer-sys = "0.4"
|
||||||
|
alksocks-fuzz-shared = { path = "shared" }
|
||||||
|
|
||||||
|
[dependencies.alksocks]
|
||||||
|
path = ".."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "datagram_codec"
|
||||||
|
path = "fuzz_targets/datagram_codec.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rfc1928_codec"
|
||||||
|
path = "fuzz_targets/rfc1928_codec.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "params_parse"
|
||||||
|
path = "fuzz_targets/params_parse.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[workspace]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# alksocks fuzzing
|
||||||
|
|
||||||
|
libFuzzer targets for alksocks' wire surfaces (see
|
||||||
|
`docs/research/fuzzing.md` for the full rationale and campaign plan).
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `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 (`fuzz/shared/src/*.rs` `#[cfg(test)]`
|
||||||
|
modules; quinn's CI pattern). The fuzz binaries are nightly-only;
|
||||||
|
the shared crate is stable-clean.
|
||||||
|
- `corpus/<target>/` — committed hand-made seeds (regenerate with
|
||||||
|
`python3 fuzz/gen_fuzz_seeds.py` from the repo root). Grown corpora
|
||||||
|
and artifacts are gitignored.
|
||||||
|
- `run-detached.sh` — mandatory runner for agent sessions: wraps
|
||||||
|
`cargo fuzz run` in `setsid` + `nohup` + log redirection so an OOM
|
||||||
|
in a target can never take down the agent host (research doc §5).
|
||||||
|
- `json.dict` — JSON token dictionary for the params target.
|
||||||
|
|
||||||
|
## Targets
|
||||||
|
|
||||||
|
| Target | Drives | Invariants |
|
||||||
|
|---|---|---|
|
||||||
|
| `datagram_codec` | `DatagramReader::feed` / `frame` / `frame_inner` (`src/wire.rs`) | no-panic; infallible reassembly with the decode-error partition; empty-payload F-2 layering; bad-ATYP loudness; splitting transparency; round-trips; the joint bound (incl. the 65766 shape); the decoder-state retained-bytes bound |
|
||||||
|
| `rfc1928_codec` | the forked RFC 1928 core (`src/rfc1928/`): `read_address`, the server typestate handshake, the RFC 1929 sub-handshake, the RFC UDP header codec, `ReplyError::from_u8` | no-panic on any byte sequence, both protocol directions; typed failures only (the F-1 class); round-trips; the full-domain reply table |
|
||||||
|
| `params_parse` | `Socks5Params` / `socks5_open_spec()` / the reason surface (`src/params.rs`) | no-panic; the trivial-params pin; the spec is a constant (the wire-stable ADR-001 surface + the schema/typed-view drift guard); the pass-through reason-extraction posture |
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# build (nightly, pinned by rust-toolchain.toml inside fuzz/; run from fuzz/)
|
||||||
|
cargo fuzz build
|
||||||
|
|
||||||
|
# agent sessions: detached campaign (never foreground; CWD-independent)
|
||||||
|
FUZZ_RUNTIME_SECS=600 fuzz/run-detached.sh datagram_codec -max_len=65536
|
||||||
|
FUZZ_RUNTIME_SECS=600 fuzz/run-detached.sh rfc1928_codec -max_len=4096 -use_value_profile=1
|
||||||
|
FUZZ_RUNTIME_SECS=600 fuzz/run-detached.sh params_parse -dict=json.dict -use_value_profile=1
|
||||||
|
|
||||||
|
# corpus replay through the invariants (stable toolchain, no nightly)
|
||||||
|
cargo test --manifest-path fuzz/shared/Cargo.toml
|
||||||
|
|
||||||
|
# coverage
|
||||||
|
cargo fuzz coverage <target>
|
||||||
|
```
|
||||||
|
|
||||||
|
The fuzz workspace is excluded from the main workspace and from the
|
||||||
|
published package (`exclude` in the root `Cargo.toml`); it pins its own
|
||||||
|
nightly toolchain via `rust-toolchain.toml` and does not affect the
|
||||||
|
crate's stable MSRV.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"resource": "sneaky"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"substrate": "tcp"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"code": "channel:open_failed", "details": {"reason": "dial_failed"}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"code": "channel:open_failed", "details": {"reason": "made_up_reason"}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"code": "FORBIDDEN", "details": {"reason": "no"}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"reason": {"enum": ["dial_failed", 1]}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nested": {"deep": {"deeper": [1, {"a": null}]}}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"channel_open": {"alpn": "alk/tunnel"}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"access_control": {"required_scopes": ["socks5:open"]}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"details": {"reason": 1}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"details": {"reason": {"enum": "dial_failed"}}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"code": {"channel:open_failed": true}, "details": null}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"type": "object", "additionalProperties": false}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"{}"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"a": {"a": {"a": {"a": {"a": {"a": {"a": {"a": {"a": {"a": {"a": {"a": 1}}}}}}}}}}}}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user