-
Notifications
You must be signed in to change notification settings - Fork 0
test(fuzz): fuzz the shared key-position walker, and unhide new fuzz targets #587
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| GET | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Add the required corpus coverage. The supplied corpus contains only a malformed
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| PING |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| #![no_main] | ||
| use libfuzzer_sys::fuzz_target; | ||
|
|
||
| use bytes::Bytes; | ||
| use moon::acl::keyspec::{CommandKeys, KeyPositions, command_key_positions, command_keys}; | ||
| use moon::protocol::Frame; | ||
| use moon::tracking::invalidation; | ||
|
|
||
| /// Fuzz the shared key-position walker (moon#576). | ||
| /// | ||
| /// `command_key_positions` parses attacker-controlled argv on behalf of THREE | ||
| /// consumers — ACL key-pattern checks, client-side cache invalidation, and | ||
| /// command introspection — so a single bounds bug is a remote panic in three | ||
| /// places at once. PR #571's review already found one of exactly this class: a | ||
| /// `numkeys` usize overflow that produced `&args[1..0]` (fixed in d0423747). | ||
| /// | ||
| /// Beyond "never panics", the properties below are the contract the callers | ||
| /// rely on. Two of them are security-relevant, not merely tidy: | ||
| /// | ||
| /// * every reported position must index `args` — the bounds property; | ||
| /// * `Unknown` and `AtPlusComputed` must reach ACL as `Indeterminate`. | ||
| /// `AtPlusComputed` means at least one key name is computed at runtime | ||
| /// (`SORT k BY w_*`), so a `~pattern` user could otherwise reach keys the | ||
| /// pattern was never meant to cover. Cache invalidation deliberately does | ||
| /// the opposite with the same value, which is why the walker reports facts | ||
| /// and each caller applies its own policy. | ||
| const MAX_ARGS: usize = 256; | ||
|
|
||
| /// Decode `data` into a command name and its argv. | ||
| /// | ||
| /// Fields are NUL-separated: the first is the command name, the rest are the | ||
| /// arguments (which EXCLUDE the command name, matching the walker's contract). | ||
| /// A leading tag byte picks the frame type so the fuzzer can reach the | ||
| /// non-string branches — a key position holding an `Integer` is a malformed | ||
| /// invocation the walker still has to survive. | ||
| fn decode(data: &[u8]) -> Option<(Vec<u8>, Vec<Frame>)> { | ||
| let mut fields = data.split(|&b| b == 0); | ||
| let cmd = fields.next()?.to_vec(); | ||
| let args = fields | ||
| .take(MAX_ARGS) | ||
| .map(|f| match f.split_first() { | ||
| Some((0x01, rest)) => { | ||
| // Reach the numkeys/count walkers with values they must clamp: | ||
| // 0, 1, usize::MAX and its neighbours all live here. | ||
| let mut n = [0u8; 8]; | ||
| let take = rest.len().min(8); | ||
| n[..take].copy_from_slice(&rest[..take]); | ||
| Frame::Integer(i64::from_le_bytes(n)) | ||
| } | ||
| Some((0x02, _)) => Frame::Null, | ||
| Some((0x03, rest)) => Frame::SimpleString(Bytes::copy_from_slice(rest)), | ||
| _ => Frame::BulkString(Bytes::copy_from_slice(f)), | ||
| }) | ||
| .collect(); | ||
| Some((cmd, args)) | ||
| } | ||
|
|
||
| fuzz_target!(|data: &[u8]| { | ||
| let Some((cmd, args)) = decode(data) else { | ||
| return; | ||
| }; | ||
| let argc = args.len(); | ||
|
|
||
| let positions = command_key_positions(&cmd, &args); | ||
|
|
||
| // Bounds: the property whose violation is a remote panic downstream. | ||
| match &positions { | ||
| KeyPositions::At(idx) => { | ||
| assert!(!idx.is_empty(), "At is documented as never empty"); | ||
| for &i in idx.iter() { | ||
| assert!(i < argc, "At position {i} out of bounds for argc {argc}"); | ||
| } | ||
| } | ||
| KeyPositions::AtPlusComputed(idx) => { | ||
| for &i in idx.iter() { | ||
| assert!( | ||
| i < argc, | ||
| "AtPlusComputed position {i} out of bounds for argc {argc}" | ||
| ); | ||
| } | ||
| } | ||
| KeyPositions::None | KeyPositions::Unknown => {} | ||
| } | ||
|
|
||
| // The walker is a pure function of its inputs; a consumer that calls it | ||
| // twice (ACL then tracking, on the same command) must see the same answer. | ||
| let again = command_key_positions(&cmd, &args); | ||
| assert_eq!( | ||
| std::mem::discriminant(&positions), | ||
| std::mem::discriminant(&again), | ||
| "walker is not deterministic" | ||
| ); | ||
|
Comment on lines
+85
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Compare the complete key-position result. The assertion compares only the enum discriminant. Two 🤖 Prompt for AI Agents |
||
|
|
||
| // --- consumer 1: ACL, which must fail CLOSED --- | ||
| let acl = command_keys(&cmd, &args); | ||
| match (&positions, &acl) { | ||
| (KeyPositions::None, CommandKeys::None) => {} | ||
| (KeyPositions::None, other) => { | ||
| panic!("provably keyless command reached ACL as {other:?}") | ||
| } | ||
| // A named position that is not a string cannot be checked against a | ||
| // key pattern, so `At` is allowed to degrade to Indeterminate. | ||
| (KeyPositions::At(_), CommandKeys::Keys(k)) => { | ||
| assert!(!k.is_empty(), "Keys is documented as never empty"); | ||
| } | ||
| (KeyPositions::At(_), CommandKeys::Indeterminate) => {} | ||
| (KeyPositions::At(_), CommandKeys::None) => { | ||
| panic!("command with key positions reached ACL as keyless") | ||
| } | ||
| // The security property: neither of these may ever name keys to ACL. | ||
| (KeyPositions::AtPlusComputed(_) | KeyPositions::Unknown, CommandKeys::Indeterminate) => {} | ||
| (KeyPositions::AtPlusComputed(_) | KeyPositions::Unknown, other) => { | ||
| panic!("unenumerable argv must deny, reached ACL as {other:?}") | ||
| } | ||
| } | ||
|
|
||
| // --- consumer 2: cache invalidation, which must NOT fail closed --- | ||
| let tracked = invalidation::command_keys(&cmd, &args); | ||
| match &positions { | ||
| KeyPositions::None | KeyPositions::Unknown => { | ||
| assert!( | ||
| tracked.is_empty(), | ||
| "nothing to invalidate, got {} keys", | ||
| tracked.len() | ||
| ); | ||
| } | ||
| KeyPositions::At(idx) | KeyPositions::AtPlusComputed(idx) => { | ||
| // Non-string positions are skipped, so this is a ceiling, not an | ||
| // equality — but inventing a key would be an over-invalidation bug. | ||
| assert!( | ||
| tracked.len() <= idx.len(), | ||
| "invalidated {} keys from {} positions", | ||
| tracked.len(), | ||
| idx.len() | ||
| ); | ||
| } | ||
|
Comment on lines
+127
to
+136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
fd -t f '^invalidation\.rs$' src
rg -nP -C 6 'pub(?:\(crate\))?\s+fn\s+command_keys\s*\(' src
rg -n -C 6 'invalidation::command_keys\s*\(' fuzz/fuzz_targets/acl_keyspec.rsRepository: pilotspace/moon Length of output: 1931 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- invalidation contract ---'
sed -n '130,215p' src/tracking/invalidation.rs
printf '%s\n' '--- fuzz target ---'
sed -n '1,155p' fuzz/fuzz_targets/acl_keyspec.rs
printf '%s\n' '--- position and argument types ---'
rg -n -C 8 'enum KeyPositions|command_key_positions|KeyPositions::At|AtPlusComputed' src/acl src/tracking fuzz/fuzz_targets/acl_keyspec.rsRepository: pilotspace/moon Length of output: 29352 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- extract_bytes contract ---'
rg -n -C 10 'fn extract_bytes|pub(?:\(crate\))?\s+fn\s+extract_bytes' src
printf '%s\n' '--- key-byte conversion contract ---'
rg -n -C 8 'fn key_bytes|pub(?:\(crate\))?\s+fn\s+key_bytes' src/acl/keyspec.rs
printf '%s\n' '--- read-only contract probe ---'
python3 - <<'PY'
from pathlib import Path
p = Path("src/tracking/invalidation.rs").read_text()
start = p.index("pub fn command_keys")
end = p.index("\n#[cfg(test)]", start)
body = p[start:end]
required = [
"for i in idx",
".get(i)",
"and_then(crate::server::conn::util::extract_bytes)",
"keys.push(b)",
]
missing = [x for x in required if x not in body]
print("command_keys iterates reported positions:", "for i in idx" in body)
print("command_keys reads each position with get:", ".get(i)" in body)
print("command_keys filters through extract_bytes:", required[2] in body)
print("command_keys pushes only extracted values:", "keys.push(b)" in body)
print("contract checks missing:", missing)
PYRepository: pilotspace/moon Length of output: 5035 Assert exact invalidation-key correspondence.
🤖 Prompt for AI Agents |
||
| } | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the nightly fuzz duration.
The workflow gives each nightly target 18,000 seconds, which is five hours. The job timeout is 350 minutes. Both lines state six hours.
CLAUDE.md#L191-L191: Document the five-hour target budget and the 350-minute job limit.CLAUDE.md#L266-L266: Replace “6h” with the workflow’s actual five-hour target budget.📍 Affects 1 file
CLAUDE.md#L191-L191(this comment)CLAUDE.md#L266-L266🤖 Prompt for AI Agents