[pitboss/grind] deferred session-0021 (20260516T052512Z-20f8)

This commit is contained in:
pitboss 2026-05-16 11:53:15 -05:00
parent aa209148b0
commit c051f58647
8 changed files with 778 additions and 9 deletions

View file

@ -108,7 +108,11 @@ fn read_entry_source(entry_file: &str) -> String {
/// Track C.1). Variadic over `const char *` args; hand-rolled JSON keeps
/// the only dep on libc / stdio.
pub fn probe_shim() -> &'static str {
r#"
// The body holds literal `"# key: value\n"` log-line formats for the
// Phase 10 stub recorders, so the surrounding raw string uses
// `r##"..."##` to keep `"#` substrings from terminating it early
// (same trick the Rust / Java / Go / Ruby siblings use).
r##"
/* ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ── */
#include <signal.h>
#include <stdarg.h>
@ -290,7 +294,67 @@ static void __nyx_install_crash_guard(const char *sink_callee) {
sigaction(sigs[i], &sa, NULL);
}
}
"#
/* Phase 10 (Track D.3) stub recorder helpers. When the verifier spawns a
* SqlStub it publishes the queries-log path through NYX_SQL_LOG; a sink
* call site that wants the host-side stub to see its query appends one
* record-per-call. Detail kv pairs use parallel arrays so the helper is
* variadic in arity without depending on stdarg-with-typed args. The
* helper is a no-op when the env var is unset so the same source still
* runs under harness modes that did not spawn a stub. */
static void __nyx_stub_sql_record(const char *query,
const char **detail_keys,
const char **detail_vals,
int detail_count) {
const char *p = getenv("NYX_SQL_LOG");
if (!p || *p == '\0') return;
FILE *f = fopen(p, "a");
if (!f) return;
for (int i = 0; i < detail_count; ++i) {
if (detail_keys && detail_vals && detail_keys[i] && detail_vals[i]) {
fprintf(f, "# %s: %s\n", detail_keys[i], detail_vals[i]);
}
}
if (query) {
size_t qlen = strlen(query);
fputs(query, f);
if (qlen == 0 || query[qlen - 1] != '\n') {
fputc('\n', f);
}
}
fclose(f);
}
/* Phase 10 (Track D.3) HTTP recording helper. When the verifier spawns an
* HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
* sink call site whose outbound request never reaches the on-the-wire
* listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
* call this helper to surface the attempted call. Format matches the SQL
* helper so the host-side merger parses both streams identically. */
static void __nyx_stub_http_record(const char *method,
const char *url,
const char *body,
const char **detail_keys,
const char **detail_vals,
int detail_count) {
const char *p = getenv("NYX_HTTP_LOG");
if (!p || *p == '\0') return;
FILE *f = fopen(p, "a");
if (!f) return;
if (method) fprintf(f, "# method: %s\n", method);
if (url) fprintf(f, "# url: %s\n", url);
if (body) fprintf(f, "# body: %s\n", body);
for (int i = 0; i < detail_count; ++i) {
if (detail_keys && detail_vals && detail_keys[i] && detail_vals[i]) {
fprintf(f, "# %s: %s\n", detail_keys[i], detail_vals[i]);
}
}
if (method && url) {
fprintf(f, "%s %s\n", method, url);
}
fclose(f);
}
"##
}
impl LangEmitter for CEmitter {
@ -730,6 +794,33 @@ mod tests {
);
}
#[test]
fn probe_shim_publishes_stub_sql_and_http_recorders() {
// Phase 10 (Track D.3): the C probe shim ships the manual-record
// stub helpers so a C harness can surface attempted DB / outbound
// calls to the host-side SqlStub / HttpStub through their
// NYX_SQL_LOG / NYX_HTTP_LOG side channels. Helpers must be
// declared before `__nyx_install_crash_guard` so a sink-rewrite
// pass can reference them from anywhere in the entry source.
let shim = probe_shim();
assert!(
shim.contains("static void __nyx_stub_sql_record("),
"C probe shim must define __nyx_stub_sql_record",
);
assert!(
shim.contains("static void __nyx_stub_http_record("),
"C probe shim must define __nyx_stub_http_record",
);
assert!(
shim.contains("getenv(\"NYX_SQL_LOG\")"),
"SQL recorder must read NYX_SQL_LOG so the SqlStub side channel picks it up",
);
assert!(
shim.contains("getenv(\"NYX_HTTP_LOG\")"),
"HTTP recorder must read NYX_HTTP_LOG so the HttpStub side channel picks it up",
);
}
#[test]
fn emit_install_crash_guard_targets_renamed_main_entry() {
// Real-world Track B CLI vuln: spec.entry_name == "main" → the entry

View file

@ -88,7 +88,11 @@ fn read_entry_source(entry_file: &str) -> String {
/// (Phase 06 — Track C.1). Uses `<fstream>` + variadic templates; the
/// JSON-emit format matches [`crate::dynamic::probe::SinkProbe`].
pub fn probe_shim() -> &'static str {
r#"
// The body holds literal `"# key: value\n"` log-line formats for the
// Phase 10 stub recorders, so the surrounding raw string uses
// `r##"..."##` to keep `"#` substrings from terminating it early
// (same trick the Rust / Java / Go / Ruby siblings use).
r##"
/* ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ── */
#include <algorithm>
#include <array>
@ -263,7 +267,47 @@ inline void __nyx_install_crash_guard(const char *sink_callee) {
sigaction(sig, &sa, nullptr);
}
}
"#
/* Phase 10 (Track D.3) stub recorder helpers. See the C-side commentary
* for the contract these are the same helpers expressed in C++ idiom
* (std::ofstream + std::initializer_list of {key, value} pairs). Both
* are no-ops when the relevant NYX_*_LOG env var is unset. */
inline void __nyx_stub_sql_record(
const std::string &query,
std::initializer_list<std::pair<std::string, std::string>> detail = {}) {
const char *p = std::getenv("NYX_SQL_LOG");
if (!p || *p == '\0') return;
std::ofstream f(p, std::ios::app);
if (!f.is_open()) return;
for (const auto &kv : detail) {
f << "# " << kv.first << ": " << kv.second << "\n";
}
f << query;
if (query.empty() || query.back() != '\n') {
f << "\n";
}
}
inline void __nyx_stub_http_record(
const std::string &method,
const std::string &url,
const std::string &body = std::string(),
std::initializer_list<std::pair<std::string, std::string>> detail = {}) {
const char *p = std::getenv("NYX_HTTP_LOG");
if (!p || *p == '\0') return;
std::ofstream f(p, std::ios::app);
if (!f.is_open()) return;
f << "# method: " << method << "\n";
f << "# url: " << url << "\n";
if (!body.empty()) {
f << "# body: " << body << "\n";
}
for (const auto &kv : detail) {
f << "# " << kv.first << ": " << kv.second << "\n";
}
f << method << " " << url << "\n";
}
"##
}
impl LangEmitter for CppEmitter {
@ -649,6 +693,31 @@ mod tests {
);
}
#[test]
fn probe_shim_publishes_stub_sql_and_http_recorders() {
// Phase 10 (Track D.3): the C++ probe shim ships the manual-record
// stub helpers so a C++ harness can surface attempted DB / outbound
// calls to the host-side SqlStub / HttpStub through their
// NYX_SQL_LOG / NYX_HTTP_LOG side channels.
let shim = probe_shim();
assert!(
shim.contains("inline void __nyx_stub_sql_record("),
"C++ probe shim must define __nyx_stub_sql_record",
);
assert!(
shim.contains("inline void __nyx_stub_http_record("),
"C++ probe shim must define __nyx_stub_http_record",
);
assert!(
shim.contains("std::getenv(\"NYX_SQL_LOG\")"),
"SQL recorder must read NYX_SQL_LOG so the SqlStub side channel picks it up",
);
assert!(
shim.contains("std::getenv(\"NYX_HTTP_LOG\")"),
"HTTP recorder must read NYX_HTTP_LOG so the HttpStub side channel picks it up",
);
}
#[test]
fn emit_cmake_in_extra_files() {
let spec = make_spec(PayloadSlot::Param(0));

View file

@ -128,19 +128,35 @@ const PROFILE_SOURCES: &[(&str, &str)] = &[
];
/// Cap → profile-name dispatch. The most restrictive matching profile
/// wins: `FILE_IO` outranks `SSRF` outranks `CODE_EXEC` outranks
/// `DESERIALIZE`. A cap bit with no matching profile falls back to the
/// `base` profile.
/// wins: filesystem caps outrank network caps outrank CODE_EXEC outranks
/// DESERIALIZE. Filesystem-shaped caps (`FILE_IO`, `SQL_QUERY` — DBs are
/// files in WORKDIR) map to `path_traversal`; outbound-network-shaped caps
/// (`SSRF`, `HEADER_INJECTION`, `OPEN_REDIRECT`, `UNVALIDATED_REDIRECT`,
/// `LDAP_INJECTION`, `XPATH_INJECTION`) map to `ssrf` since they share the
/// "outbound allowed; host secrets denied" shape. Caps with no shared
/// shape (CRYPTO, AUTH, RACE, MEMORY_SAFETY, XSS, XXE) fall back to `base`
/// — XXE in particular would want a network-deny profile for entity
/// resolution, which the bundled `.sb` set does not yet ship.
pub fn profile_for_caps(caps: u32) -> &'static str {
// Mirror the bit positions declared in `src/labels/mod.rs`.
const FILE_IO: u32 = 1 << 5;
const SQL_QUERY: u32 = 1 << 7;
const DESERIALIZE: u32 = 1 << 8;
const SSRF: u32 = 1 << 9;
const CODE_EXEC: u32 = 1 << 10;
const LDAP_INJECTION: u32 = 1 << 14;
const XPATH_INJECTION: u32 = 1 << 15;
const HEADER_INJECTION: u32 = 1 << 16;
const OPEN_REDIRECT: u32 = 1 << 17;
const UNVALIDATED_REDIRECT: u32 = 1 << 18;
if caps & FILE_IO != 0 {
const FS_SHAPED: u32 = FILE_IO | SQL_QUERY;
const NET_SHAPED: u32 =
SSRF | LDAP_INJECTION | XPATH_INJECTION | HEADER_INJECTION | OPEN_REDIRECT | UNVALIDATED_REDIRECT;
if caps & FS_SHAPED != 0 {
"path_traversal"
} else if caps & SSRF != 0 {
} else if caps & NET_SHAPED != 0 {
"ssrf"
} else if caps & CODE_EXEC != 0 {
"cmdi"
@ -323,6 +339,48 @@ mod tests {
assert_eq!(profile_for_caps(0), "base");
}
#[test]
fn profile_for_caps_routes_filesystem_shaped_caps_to_path_traversal() {
// SQL_QUERY shares the `file-write into WORKDIR / file-read of
// host secrets denied` shape with FILE_IO (SQLite DBs live as
// files in the workdir), so it routes to the same profile.
const SQL_QUERY: u32 = 1 << 7;
const CODE_EXEC: u32 = 1 << 10;
assert_eq!(profile_for_caps(SQL_QUERY), "path_traversal");
// Filesystem shape outranks the lesser-restrictive cmdi profile.
assert_eq!(profile_for_caps(SQL_QUERY | CODE_EXEC), "path_traversal");
}
#[test]
fn profile_for_caps_routes_outbound_network_caps_to_ssrf() {
// Outbound HTTP request sinks (HEADER_INJECTION / OPEN_REDIRECT /
// UNVALIDATED_REDIRECT) and other network-traffic injection caps
// (LDAP_INJECTION / XPATH_INJECTION) all share the SSRF shape:
// outbound allowed, host-secret reads denied.
const LDAP_INJECTION: u32 = 1 << 14;
const XPATH_INJECTION: u32 = 1 << 15;
const HEADER_INJECTION: u32 = 1 << 16;
const OPEN_REDIRECT: u32 = 1 << 17;
const UNVALIDATED_REDIRECT: u32 = 1 << 18;
assert_eq!(profile_for_caps(LDAP_INJECTION), "ssrf");
assert_eq!(profile_for_caps(XPATH_INJECTION), "ssrf");
assert_eq!(profile_for_caps(HEADER_INJECTION), "ssrf");
assert_eq!(profile_for_caps(OPEN_REDIRECT), "ssrf");
assert_eq!(profile_for_caps(UNVALIDATED_REDIRECT), "ssrf");
}
#[test]
fn profile_for_caps_falls_back_to_base_for_unmapped_caps() {
// CRYPTO / AUTH / RACE / MEMORY_SAFETY / XSS / XXE do not yet
// have a cap-specific .sb profile. XXE in particular would want
// a network-deny profile (entity resolution), but the bundled .sb
// set does not ship one — track in deferred.md.
const CRYPTO: u32 = 1 << 11;
const XXE: u32 = 1 << 19;
assert_eq!(profile_for_caps(CRYPTO), "base");
assert_eq!(profile_for_caps(XXE), "base");
}
#[test]
fn profile_path_materialises_baked_source() {
let path = profile_path("base").expect("base profile");