[pitboss/grind] deferred session-0003 (20260521T143544Z-f898)

This commit is contained in:
pitboss 2026-05-21 12:17:45 -05:00
parent b3766311fb
commit 6341afec59
16 changed files with 346 additions and 48 deletions

View file

@ -21,6 +21,28 @@ fn callee_is_header_setter(name: &str) -> bool {
matches!(last, "Set" | "Add" | "Header" | "WriteHeader")
}
/// True when `receiver` looks like a Go HTTP response-writer or framework
/// context expression. Filters out `url.Values.Set` / `sync.Map.Store` /
/// `flag.FlagSet.Set` and similar map-like receivers whose `Set` / `Add`
/// names collide with `http.Header.Set` / `Add`.
///
/// Drilled forms (root_receiver_text reduces `w.Header().Set` to `w`):
/// * `w` / `rw` / `writer` — canonical `http.ResponseWriter` names
/// * `c` / `ctx` — gin / echo / fiber / chi context handles
/// * `resp` / `response` — common response-wrapper names
/// * `headers` — `Header` value handle
///
/// Non-drilled forms (raw text when drilling fails):
/// * Any expression containing `.Header()` or `.Headers()` —
/// canonical chain accessor returning `http.Header`.
fn receiver_is_go_response_writer(receiver: &str) -> bool {
matches!(
receiver,
"w" | "rw" | "writer" | "c" | "ctx" | "resp" | "response" | "headers" | "header"
) || receiver.contains(".Header()")
|| receiver.contains(".Headers()")
}
fn source_imports_go_http(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"\"net/http\"",
@ -69,7 +91,11 @@ impl FrameworkAdapter for HeaderGoAdapter {
if value_routed_through_encoder(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_header_setter,
receiver_is_go_response_writer,
);
let matches_source = source_imports_go_http(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
@ -125,6 +151,55 @@ mod tests {
.is_none());
}
#[test]
fn skips_url_values_set_collision() {
// `params.Set(k, v)` on a `url.Values` collides with `http.Header.Set`
// on the bare callee name. Real CFG-derived callees carry the
// receiver text `params`, which is not in the response-writer
// allowlist, so the adapter rejects. Net/url is intentionally
// imported here to ensure the source-import gate alone would fire.
let src: &[u8] = b"package x\nimport (\"net/http\"; \"net/url\")\n\
func Run(w http.ResponseWriter, v string) {\n\
params := url.Values{}\n\
params.Set(\"k\", v)\n\
_ = params\n\
}\n";
let tree = parse_go(src);
let summary = FuncSummary {
name: "Run".into(),
callees: vec![crate::summary::CalleeSite {
name: "Set".into(),
receiver: Some("params".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderGoAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
#[test]
fn fires_on_response_writer_receiver() {
// Receiver-text discriminator accepts `w` (canonical
// `http.ResponseWriter` shorthand).
let src: &[u8] = b"package x\nimport \"net/http\"\n\
func Run(w http.ResponseWriter, v string) { w.Header().Set(\"X\", v) }\n";
let tree = parse_go(src);
let summary = FuncSummary {
name: "Run".into(),
callees: vec![crate::summary::CalleeSite {
name: "Set".into(),
receiver: Some("w".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderGoAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_when_value_url_encoded() {
let src: &[u8] = b"package x\nimport (\"net/http\"; \"net/url\")\n\

View file

@ -22,6 +22,23 @@ fn callee_is_header_setter(name: &str) -> bool {
matches!(last, "set_header" | "[]=" | "store" | "add_header")
}
/// True when `receiver` looks like a Ruby response or headers handle.
/// Filters out `Hash#[]=` / generic `Hash#store` collisions where the
/// receiver is an unrelated local (`h`, `params`, `attrs`, etc.).
///
/// Drilled forms covered:
/// * `response` / `resp` / `res` — `Rack::Response` / Rails / Sinatra response
/// * `headers` — bare headers handle
/// * `@response` / `@headers` — instance-var equivalents
/// * Any expression containing `.headers` or `.response` (chain access).
fn receiver_is_ruby_response(receiver: &str) -> bool {
matches!(
receiver,
"response" | "resp" | "res" | "headers" | "@response" | "@headers"
) || receiver.contains(".headers")
|| receiver.contains(".response")
}
fn source_uses_ruby_web(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"Rack::Response",
@ -73,7 +90,11 @@ impl FrameworkAdapter for HeaderRubyAdapter {
if value_routed_through_encoder(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_header_setter,
receiver_is_ruby_response,
);
let matches_source = source_uses_ruby_web(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
@ -129,6 +150,49 @@ mod tests {
.is_none());
}
#[test]
fn skips_hash_subscript_assign_collision() {
// `h['Set-Cookie'] = value` on a plain `Hash` collides with
// `response['Set-Cookie'] = value` on the bare `[]=` callee
// name. Receiver text `h` is not in the response allowlist,
// so the adapter rejects.
let src: &[u8] = b"require 'rack'\n\
def run(value)\n h = {}\n h['Set-Cookie'] = value\n h\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "[]=".into(),
receiver: Some("h".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderRubyAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
#[test]
fn fires_on_response_receiver() {
// Receiver `response` is in the allowlist.
let src: &[u8] = b"require 'rack'\n\
def run(value)\n response = Rack::Response.new\n response['Set-Cookie'] = value\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "[]=".into(),
receiver: Some("response".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderRubyAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_when_value_url_encoded() {
let src: &[u8] = b"require 'rack'\nrequire 'uri'\n\

View file

@ -23,6 +23,25 @@ fn callee_is_header_setter(name: &str) -> bool {
matches!(last, "insert" | "append" | "insert_header" | "header")
}
/// True when `receiver` looks like a Rust `HeaderMap` / response handle.
/// Filters out `BTreeMap::insert` / `HashMap::insert` / `Vec::insert`
/// collisions where the receiver is an unrelated local (`map`, `cache`,
/// `entries`, etc.).
///
/// Drilled forms covered:
/// * `headers` / `headers_mut` — canonical `axum` / `hyper` handles
/// * `response` / `resp` / `res` — `actix_web::HttpResponse` / hyper builder
/// * `builder` — `axum::http::Response::builder()` chain root
/// * Any expression containing `.headers_mut()` or `.headers()` —
/// chain accessor returning `&mut HeaderMap` / `&HeaderMap`.
fn receiver_is_rust_header_map(receiver: &str) -> bool {
matches!(
receiver,
"headers" | "headers_mut" | "response" | "resp" | "res" | "builder"
) || receiver.contains(".headers_mut()")
|| receiver.contains(".headers()")
}
fn source_imports_rust_http(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"use http::HeaderMap",
@ -71,7 +90,11 @@ impl FrameworkAdapter for HeaderRustAdapter {
if value_routed_through_encoder(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_header_setter,
receiver_is_rust_header_map,
);
let matches_source = source_imports_rust_http(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
@ -127,6 +150,54 @@ mod tests {
.is_none());
}
#[test]
fn skips_btreemap_insert_collision() {
// `map.insert(k, v)` on a `BTreeMap` / `HashMap` collides with
// `headers.insert(k, v)` on `HeaderMap` at the bare callee name.
// Receiver text `map` is not in the HeaderMap allowlist, so the
// adapter rejects. `headers_mut()` substring is present in the
// file so source-import gate alone would fire.
let src: &[u8] = b"use std::collections::BTreeMap;\nuse axum::http::HeaderMap;\n\
fn run(headers: &mut HeaderMap, value: String) {\n\
let mut map: BTreeMap<String, String> = BTreeMap::new();\n\
map.insert(\"k\".into(), value);\n\
let _ = headers.headers_mut();\n\
}\n";
let tree = parse_rust(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "insert".into(),
receiver: Some("map".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderRustAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
#[test]
fn fires_on_headers_receiver() {
// Receiver `headers` is in the HeaderMap allowlist.
let src: &[u8] = b"use axum::http::HeaderMap;\n\
fn run(headers: &mut HeaderMap, value: &str) { headers.insert(\"X\", value.parse().unwrap()); }\n";
let tree = parse_rust(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "insert".into(),
receiver: Some("headers".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderRustAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_when_value_url_encoded() {
let src: &[u8] = b"use axum::http::HeaderMap;\n\

View file

@ -227,6 +227,36 @@ fn any_callee_matches(
.any(|c| predicate(c.name.as_str()))
}
/// True when any callee in `summary.callees` matches `name_pred` AND
/// (its receiver matches `receiver_pred` OR its receiver is `None`).
///
/// Used by adapters where the callee name is ambiguous (e.g. Go's bare
/// `Set` / `Add` collides with `url.Values.Set`, Rust's `insert` collides
/// with `BTreeMap::insert`) and the receiver text provides the only
/// non-type-aware discriminator.
///
/// Receivers of `None` fall through to acceptance to preserve backward
/// compatibility with synthetic unit-test summaries built via
/// `CalleeSite::bare(...)` and with adapters whose callees are free
/// functions (no receiver). Real CFG-derived callees populate
/// `CalleeSite.receiver` whenever the call is a method invocation, so
/// the gate engages on production scans.
fn any_callee_matches_with_receiver(
summary: &crate::summary::FuncSummary,
name_pred: impl Fn(&str) -> bool,
receiver_pred: impl Fn(&str) -> bool,
) -> bool {
summary.callees.iter().any(|c| {
if !name_pred(c.name.as_str()) {
return false;
}
match c.receiver.as_deref() {
Some(r) => receiver_pred(r),
None => true,
}
})
}
/// True when `arg_text` resolves to a function parameter whose 0-based
/// index participates in taint flow — either listed in
/// `summary.tainted_sink_params` (param reaches an internal sink) or

View file

@ -16,6 +16,19 @@ fn callee_is_lodash_merge(name: &str) -> bool {
matches!(last, "merge" | "mergeWith" | "defaultsDeep" | "set" | "setWith")
}
/// True when `receiver` looks like a lodash module handle (`_`, `lodash`,
/// or any expression where lodash sits to the left of the dot).
///
/// Filters out `state.set(k, v)` on `Map`, `cache.set(k, v)` on `LRU`,
/// `tokens.merge(...)` on a user class, and similar same-name collisions
/// outside lodash scope. Receivers of `None` (bare callees like
/// `set(state, key, value)` from `const { set } = require('lodash')`
/// or unit-test `CalleeSite::bare`) pass through to preserve the
/// standalone-import path.
fn receiver_is_lodash(receiver: &str) -> bool {
matches!(receiver, "_" | "lodash" | "lodashImport") || receiver.starts_with("_.")
}
fn source_imports_lodash(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"require('lodash')",
@ -68,7 +81,11 @@ impl FrameworkAdapter for PpLodashMergeJsAdapter {
if super::source_filters_proto_keys(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_lodash_merge);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_lodash_merge,
receiver_is_lodash,
);
let matches_source = source_imports_lodash(file_bytes);
if matches_call && matches_source {
Some(build_binding(JS_ADAPTER_NAME))
@ -100,7 +117,11 @@ impl FrameworkAdapter for PpLodashMergeTsAdapter {
if super::source_filters_proto_keys(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_lodash_merge);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_lodash_merge,
receiver_is_lodash,
);
let matches_source = source_imports_lodash(file_bytes);
if matches_call && matches_source {
Some(build_binding(TS_ADAPTER_NAME))
@ -149,6 +170,54 @@ mod tests {
.is_none());
}
#[test]
fn skips_map_set_collision() {
// `state.set(k, v)` on a Map collides with `_.set(state, k, v)`
// on the bare callee name. Receiver text `state` is not in the
// lodash allowlist, so the adapter rejects. The lodash import
// is intentionally present to ensure the source-import gate
// alone would have fired.
let src: &[u8] = b"const _ = require('lodash');\n\
function run(payload) {\n\
const state = new Map();\n\
state.set('key', payload);\n\
return state;\n\
}\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "set".into(),
receiver: Some("state".into()),
..Default::default()
}],
..Default::default()
};
assert!(PpLodashMergeJsAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
#[test]
fn fires_on_underscore_receiver() {
// Receiver `_` is the canonical lodash binding.
let src: &[u8] = b"const _ = require('lodash');\n\
function run(payload) { return _.merge({}, payload); }\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "merge".into(),
receiver: Some("_".into()),
..Default::default()
}],
..Default::default()
};
assert!(PpLodashMergeJsAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_when_proto_key_filter_present() {
let src: &[u8] = b"const _ = require('lodash');\n\

View file

@ -537,7 +537,7 @@ fn build_toolchain_lock(spec: &HarnessSpec, root: &Path) -> Result<serde_json::V
}))
}
/// Phase 28 — Track H.3. Outcome of [`replay_bundle`].
/// Outcome of [`replay_bundle`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplayResult {
/// `reproduce.sh` exited 0 — replay matched the recorded outcome.
@ -548,14 +548,13 @@ pub enum ReplayResult {
DockerUnavailable,
/// `reproduce.sh` exited 3 — host toolchain mismatched in process mode.
ToolchainMismatch,
/// Any other non-zero exit code, treated as an unexpected error. The
/// Phase 28 m7 Gate 5 inversion treats this as instability.
/// Any other non-zero exit code, treated as an unexpected error.
UnexpectedError {
/// Exit code surfaced by the script.
exit_code: i32,
},
/// `reproduce.sh` could not be invoked at all (script missing,
/// permissions, etc.). Phase 28 Gate 5 treats this as instability.
/// permissions, etc.).
ScriptInvocationFailed {
/// Human-readable error.
message: String,