mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
[pitboss] phase 08: Track C.4 + C.5 — SinkCrash oracle + per-probe witness capture
This commit is contained in:
parent
4eccbd48b4
commit
93eb98edda
21 changed files with 1988 additions and 115 deletions
|
|
@ -23,12 +23,101 @@ const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
|||
/// the only dep on libc / stdio.
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
/* ── __nyx_probe shim (Phase 06 — Track C.1) ─────────────────────────────── */
|
||||
/* ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ── */
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef __NYX_PAYLOAD_LIMIT
|
||||
#define __NYX_PAYLOAD_LIMIT (16 * 1024)
|
||||
#endif
|
||||
#define __NYX_REDACTED "<redacted-by-nyx-policy>"
|
||||
|
||||
extern char **environ;
|
||||
|
||||
static const char *__nyx_deny[] = {
|
||||
"TOKEN","SECRET","PASSWORD","PASSWD","API_KEY","APIKEY","PRIVATE_KEY",
|
||||
"CREDENTIAL","SESSION","COOKIE","AUTH","BEARER","AWS_ACCESS","AWS_SESSION",
|
||||
"GH_TOKEN","GITHUB_TOKEN","NPM_TOKEN","PYPI_TOKEN","DOCKER_PASS",
|
||||
NULL,
|
||||
};
|
||||
|
||||
static int __nyx_is_denied_upper(const char *k_upper) {
|
||||
for (int i = 0; __nyx_deny[i]; ++i) {
|
||||
if (strstr(k_upper, __nyx_deny[i])) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void __nyx_write_witness(FILE *f, const char *sink_callee, int nargs, const char **args) {
|
||||
fputs("{\"env_snapshot\":{", f);
|
||||
int first = 1;
|
||||
for (char **e = environ; *e; ++e) {
|
||||
const char *eq = strchr(*e, '=');
|
||||
if (!eq) continue;
|
||||
size_t klen = (size_t)(eq - *e);
|
||||
char *kup = (char *)malloc(klen + 1);
|
||||
if (!kup) continue;
|
||||
for (size_t i = 0; i < klen; ++i) {
|
||||
char c = (*e)[i];
|
||||
if (c >= 'a' && c <= 'z') c -= 32;
|
||||
kup[i] = c;
|
||||
}
|
||||
kup[klen] = '\0';
|
||||
int denied = __nyx_is_denied_upper(kup);
|
||||
if (!first) fputc(',', f);
|
||||
first = 0;
|
||||
fputc('"', f);
|
||||
fwrite(*e, 1, klen, f);
|
||||
fputs("\":\"", f);
|
||||
if (denied) {
|
||||
fputs(__NYX_REDACTED, f);
|
||||
} else {
|
||||
const char *v = eq + 1;
|
||||
for (; *v; ++v) {
|
||||
switch (*v) {
|
||||
case '"': fputs("\\\"", f); break;
|
||||
case '\\': fputs("\\\\", f); break;
|
||||
case '\n': fputs("\\n", f); break;
|
||||
case '\r': fputs("\\r", f); break;
|
||||
case '\t': fputs("\\t", f); break;
|
||||
default: fputc(*v, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
fputc('"', f);
|
||||
free(kup);
|
||||
}
|
||||
fputs("},\"cwd\":\"", f);
|
||||
char cwdbuf[4096];
|
||||
if (getcwd(cwdbuf, sizeof(cwdbuf))) {
|
||||
fputs(cwdbuf, f);
|
||||
}
|
||||
fputs("\",\"payload_bytes\":[", f);
|
||||
const char *payload = getenv("NYX_PAYLOAD");
|
||||
if (payload) {
|
||||
size_t plen = strlen(payload);
|
||||
if (plen > __NYX_PAYLOAD_LIMIT) plen = __NYX_PAYLOAD_LIMIT;
|
||||
for (size_t i = 0; i < plen; ++i) {
|
||||
if (i > 0) fputc(',', f);
|
||||
fprintf(f, "%d", (unsigned char)payload[i]);
|
||||
}
|
||||
}
|
||||
fputs("],\"callee\":\"", f);
|
||||
fputs(sink_callee, f);
|
||||
fputs("\",\"args_repr\":[", f);
|
||||
for (int i = 0; i < nargs; ++i) {
|
||||
if (i > 0) fputc(',', f);
|
||||
fputc('"', f);
|
||||
if (args && args[i]) fputs(args[i], f);
|
||||
fputc('"', f);
|
||||
}
|
||||
fputs("]}", f);
|
||||
}
|
||||
|
||||
static void __nyx_probe(const char *sink_callee, int nargs, ...) {
|
||||
const char *p = getenv("NYX_PROBE_PATH");
|
||||
|
|
@ -44,16 +133,77 @@ static void __nyx_probe(const char *sink_callee, int nargs, ...) {
|
|||
fprintf(f, "{\"sink_callee\":\"%s\",\"args\":[", sink_callee);
|
||||
va_list ap;
|
||||
va_start(ap, nargs);
|
||||
const char *args_arr[32];
|
||||
int captured = nargs > 32 ? 32 : nargs;
|
||||
for (int i = 0; i < nargs; ++i) {
|
||||
const char *arg = va_arg(ap, const char *);
|
||||
if (!arg) arg = "";
|
||||
if (i < captured) args_arr[i] = arg;
|
||||
if (i > 0) fputc(',', f);
|
||||
fprintf(f, "{\"kind\":\"String\",\"value\":\"%s\"}", arg);
|
||||
}
|
||||
va_end(ap);
|
||||
fprintf(f, "],\"captured_at_ns\":%llu,\"payload_id\":\"%s\"}\n", ns, pid);
|
||||
fprintf(f, "],\"captured_at_ns\":%llu,\"payload_id\":\"%s\",", ns, pid);
|
||||
fputs("\"kind\":{\"kind\":\"Normal\"},\"witness\":", f);
|
||||
__nyx_write_witness(f, sink_callee, captured, args_arr);
|
||||
fputs("}\n", f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* Phase 08: sink-site signal handler. __nyx_install_crash_guard sets a
|
||||
* sigaction(2) handler over SIGSEGV / SIGABRT / SIGBUS / SIGFPE / SIGILL
|
||||
* that writes a Crash probe with witness before restoring SIG_DFL and
|
||||
* re-raising the signal — the process still dies with the same exit
|
||||
* code, but the probe channel now carries the forensic record. */
|
||||
static const char *__nyx_crash_sink_callee = "";
|
||||
|
||||
static void __nyx_crash_handler(int sig) {
|
||||
const char *p = getenv("NYX_PROBE_PATH");
|
||||
if (p && *p) {
|
||||
FILE *f = fopen(p, "a");
|
||||
if (f) {
|
||||
const char *name = "SIGABRT";
|
||||
switch (sig) {
|
||||
case SIGSEGV: name = "SIGSEGV"; break;
|
||||
case SIGABRT: name = "SIGABRT"; break;
|
||||
case SIGBUS: name = "SIGBUS"; break;
|
||||
case SIGFPE: name = "SIGFPE"; break;
|
||||
case SIGILL: name = "SIGILL"; break;
|
||||
}
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
unsigned long long ns = (unsigned long long)ts.tv_sec * 1000000000ULL +
|
||||
(unsigned long long)ts.tv_nsec;
|
||||
const char *pid = getenv("NYX_PAYLOAD_ID");
|
||||
if (!pid) pid = "";
|
||||
fprintf(f,
|
||||
"{\"sink_callee\":\"%s\",\"args\":[],\"captured_at_ns\":%llu,"
|
||||
"\"payload_id\":\"%s\",\"kind\":{\"kind\":\"Crash\",\"signal\":\"%s\"},"
|
||||
"\"witness\":",
|
||||
__nyx_crash_sink_callee, ns, pid, name);
|
||||
__nyx_write_witness(f, __nyx_crash_sink_callee, 0, NULL);
|
||||
fputs("}\n", f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
struct sigaction dfl;
|
||||
memset(&dfl, 0, sizeof(dfl));
|
||||
dfl.sa_handler = SIG_DFL;
|
||||
sigaction(sig, &dfl, NULL);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
static void __nyx_install_crash_guard(const char *sink_callee) {
|
||||
__nyx_crash_sink_callee = sink_callee;
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = __nyx_crash_handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
int sigs[] = { SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL };
|
||||
for (size_t i = 0; i < sizeof(sigs)/sizeof(sigs[0]); ++i) {
|
||||
sigaction(sigs[i], &sa, NULL);
|
||||
}
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,31 @@ const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
|||
/// JSON-emit format matches [`crate::dynamic::probe::SinkProbe`].
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
/* ── __nyx_probe shim (Phase 06 — Track C.1) ─────────────────────────────── */
|
||||
/* ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ── */
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef __NYX_PAYLOAD_LIMIT
|
||||
#define __NYX_PAYLOAD_LIMIT (16 * 1024)
|
||||
#endif
|
||||
#define __NYX_REDACTED "<redacted-by-nyx-policy>"
|
||||
|
||||
extern char **environ;
|
||||
|
||||
static const char *__nyx_deny_substrings_cpp[] = {
|
||||
"TOKEN","SECRET","PASSWORD","PASSWD","API_KEY","APIKEY","PRIVATE_KEY",
|
||||
"CREDENTIAL","SESSION","COOKIE","AUTH","BEARER","AWS_ACCESS","AWS_SESSION",
|
||||
"GH_TOKEN","GITHUB_TOKEN","NPM_TOKEN","PYPI_TOKEN","DOCKER_PASS",
|
||||
};
|
||||
|
||||
inline void __nyx_probe_one(std::ostringstream &out, const std::string &v) {
|
||||
out << "{\"kind\":\"String\",\"value\":\"";
|
||||
|
|
@ -45,6 +64,63 @@ inline void __nyx_probe_one(std::ostringstream &out, const std::string &v) {
|
|||
out << "\"}";
|
||||
}
|
||||
|
||||
inline void __nyx_esc(std::ostringstream &out, const std::string &v) {
|
||||
for (char c : v) {
|
||||
switch (c) {
|
||||
case '"': out << "\\\""; break;
|
||||
case '\\': out << "\\\\"; break;
|
||||
case '\n': out << "\\n"; break;
|
||||
case '\r': out << "\\r"; break;
|
||||
case '\t': out << "\\t"; break;
|
||||
default: out << c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string __nyx_witness_json(const char *sink_callee, const std::vector<std::string> &args_repr) {
|
||||
std::ostringstream out;
|
||||
out << "{\"env_snapshot\":{";
|
||||
bool first = true;
|
||||
for (char **e = environ; *e; ++e) {
|
||||
const char *eq = std::strchr(*e, '=');
|
||||
if (!eq) continue;
|
||||
std::string k(*e, static_cast<size_t>(eq - *e));
|
||||
std::string ku = k;
|
||||
std::transform(ku.begin(), ku.end(), ku.begin(), [](unsigned char c){ return (char)std::toupper(c); });
|
||||
bool denied = false;
|
||||
for (const char *needle : __nyx_deny_substrings_cpp) {
|
||||
if (ku.find(needle) != std::string::npos) { denied = true; break; }
|
||||
}
|
||||
if (!first) out << ',';
|
||||
first = false;
|
||||
out << '"'; __nyx_esc(out, k); out << "\":\"";
|
||||
if (denied) out << __NYX_REDACTED;
|
||||
else __nyx_esc(out, std::string(eq + 1));
|
||||
out << '"';
|
||||
}
|
||||
out << "},\"cwd\":\"";
|
||||
char cwdbuf[4096];
|
||||
if (::getcwd(cwdbuf, sizeof(cwdbuf))) __nyx_esc(out, std::string(cwdbuf));
|
||||
out << "\",\"payload_bytes\":[";
|
||||
const char *payload = std::getenv("NYX_PAYLOAD");
|
||||
if (payload) {
|
||||
size_t plen = std::strlen(payload);
|
||||
if (plen > __NYX_PAYLOAD_LIMIT) plen = __NYX_PAYLOAD_LIMIT;
|
||||
for (size_t i = 0; i < plen; ++i) {
|
||||
if (i > 0) out << ',';
|
||||
out << static_cast<int>(static_cast<unsigned char>(payload[i]));
|
||||
}
|
||||
}
|
||||
out << "],\"callee\":\""; __nyx_esc(out, std::string(sink_callee));
|
||||
out << "\",\"args_repr\":[";
|
||||
for (size_t i = 0; i < args_repr.size(); ++i) {
|
||||
if (i > 0) out << ',';
|
||||
out << '"'; __nyx_esc(out, args_repr[i]); out << '"';
|
||||
}
|
||||
out << "]}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
inline void __nyx_probe(const char *sink_callee, Args... args) {
|
||||
const char *p = std::getenv("NYX_PROBE_PATH");
|
||||
|
|
@ -52,10 +128,12 @@ inline void __nyx_probe(const char *sink_callee, Args... args) {
|
|||
std::ostringstream out;
|
||||
out << "{\"sink_callee\":\"" << sink_callee << "\",\"args\":[";
|
||||
bool first = true;
|
||||
std::vector<std::string> repr;
|
||||
auto emit = [&](const std::string &s) {
|
||||
if (!first) out << ',';
|
||||
first = false;
|
||||
__nyx_probe_one(out, s);
|
||||
repr.push_back(s);
|
||||
};
|
||||
(emit(std::string(args)), ...);
|
||||
const char *pid = std::getenv("NYX_PAYLOAD_ID");
|
||||
|
|
@ -63,10 +141,62 @@ inline void __nyx_probe(const char *sink_callee, Args... args) {
|
|||
std::chrono::system_clock::now().time_since_epoch()
|
||||
).count();
|
||||
out << "],\"captured_at_ns\":" << now << ",\"payload_id\":\""
|
||||
<< (pid ? pid : "") << "\"}\n";
|
||||
<< (pid ? pid : "") << "\",";
|
||||
out << "\"kind\":{\"kind\":\"Normal\"},\"witness\":"
|
||||
<< __nyx_witness_json(sink_callee, repr) << "}\n";
|
||||
std::ofstream f(p, std::ios::app);
|
||||
if (f.is_open()) f << out.str();
|
||||
}
|
||||
|
||||
/* Phase 08: sink-site sigaction handler. Mirrors the C variant; the
|
||||
* captured `sink_callee` is held in a file-scope const char* so the
|
||||
* async-signal-unsafe write path can pull it without TLS. */
|
||||
static const char *__nyx_crash_sink_callee = "";
|
||||
|
||||
inline void __nyx_crash_handler(int sig) {
|
||||
const char *p = std::getenv("NYX_PROBE_PATH");
|
||||
if (p && *p) {
|
||||
std::ofstream f(p, std::ios::app);
|
||||
if (f.is_open()) {
|
||||
const char *name = "SIGABRT";
|
||||
switch (sig) {
|
||||
case SIGSEGV: name = "SIGSEGV"; break;
|
||||
case SIGABRT: name = "SIGABRT"; break;
|
||||
case SIGBUS: name = "SIGBUS"; break;
|
||||
case SIGFPE: name = "SIGFPE"; break;
|
||||
case SIGILL: name = "SIGILL"; break;
|
||||
}
|
||||
auto now = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()
|
||||
).count();
|
||||
const char *pid = std::getenv("NYX_PAYLOAD_ID");
|
||||
std::ostringstream out;
|
||||
out << "{\"sink_callee\":\"" << __nyx_crash_sink_callee
|
||||
<< "\",\"args\":[],\"captured_at_ns\":" << now
|
||||
<< ",\"payload_id\":\"" << (pid ? pid : "")
|
||||
<< "\",\"kind\":{\"kind\":\"Crash\",\"signal\":\"" << name
|
||||
<< "\"},\"witness\":"
|
||||
<< __nyx_witness_json(__nyx_crash_sink_callee, {}) << "}\n";
|
||||
f << out.str();
|
||||
}
|
||||
}
|
||||
struct sigaction dfl;
|
||||
std::memset(&dfl, 0, sizeof(dfl));
|
||||
dfl.sa_handler = SIG_DFL;
|
||||
sigaction(sig, &dfl, nullptr);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
inline void __nyx_install_crash_guard(const char *sink_callee) {
|
||||
__nyx_crash_sink_callee = sink_callee;
|
||||
struct sigaction sa;
|
||||
std::memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = __nyx_crash_handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
for (int sig : { SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL }) {
|
||||
sigaction(sig, &sa, nullptr);
|
||||
}
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,12 +58,71 @@ impl LangEmitter for GoEmitter {
|
|||
/// captured args at the sink site.
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1) ──────────────────────────────────
|
||||
func __nyx_probe(sinkCallee string, args ...string) {
|
||||
p := os.Getenv("NYX_PROBE_PATH")
|
||||
if p == "" {
|
||||
return
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──────
|
||||
var __nyx_deny_substrings = []string{
|
||||
"TOKEN","SECRET","PASSWORD","PASSWD","API_KEY","APIKEY","PRIVATE_KEY",
|
||||
"CREDENTIAL","SESSION","COOKIE","AUTH","BEARER","AWS_ACCESS","AWS_SESSION",
|
||||
"GH_TOKEN","GITHUB_TOKEN","NPM_TOKEN","PYPI_TOKEN","DOCKER_PASS",
|
||||
}
|
||||
|
||||
const __nyx_payload_limit = 16 * 1024
|
||||
const __nyx_redacted = "<redacted-by-nyx-policy>"
|
||||
|
||||
func __nyx_scrub_env() map[string]string {
|
||||
out := map[string]string{}
|
||||
for _, e := range os.Environ() {
|
||||
idx := -1
|
||||
for i, c := range e {
|
||||
if c == '=' { idx = i; break }
|
||||
}
|
||||
if idx < 0 { continue }
|
||||
k := e[:idx]
|
||||
v := e[idx+1:]
|
||||
ku := strings.ToUpper(k)
|
||||
denied := false
|
||||
for _, n := range __nyx_deny_substrings {
|
||||
if strings.Contains(ku, n) { denied = true; break }
|
||||
}
|
||||
if denied {
|
||||
out[k] = __nyx_redacted
|
||||
} else {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func __nyx_witness(sinkCallee string, args []string) map[string]interface{} {
|
||||
payload := os.Getenv("NYX_PAYLOAD")
|
||||
pb := []byte(payload)
|
||||
if len(pb) > __nyx_payload_limit { pb = pb[:__nyx_payload_limit] }
|
||||
repr := make([]string, len(args))
|
||||
for i, a := range args { repr[i] = a }
|
||||
cwd, _ := os.Getwd()
|
||||
bytes_int := make([]int, len(pb))
|
||||
for i, b := range pb { bytes_int[i] = int(b) }
|
||||
return map[string]interface{}{
|
||||
"env_snapshot": __nyx_scrub_env(),
|
||||
"cwd": cwd,
|
||||
"payload_bytes": bytes_int,
|
||||
"callee": sinkCallee,
|
||||
"args_repr": repr,
|
||||
}
|
||||
}
|
||||
|
||||
func __nyx_emit(rec map[string]interface{}) {
|
||||
p := os.Getenv("NYX_PROBE_PATH")
|
||||
if p == "" { return }
|
||||
b, err := json.Marshal(rec)
|
||||
if err != nil { return }
|
||||
f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil { return }
|
||||
defer f.Close()
|
||||
f.Write(b)
|
||||
f.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
func __nyx_probe(sinkCallee string, args ...string) {
|
||||
serArgs := make([]map[string]interface{}, 0, len(args))
|
||||
for _, a := range args {
|
||||
serArgs = append(serArgs, map[string]interface{}{
|
||||
|
|
@ -71,23 +130,61 @@ func __nyx_probe(sinkCallee string, args ...string) {
|
|||
"value": a,
|
||||
})
|
||||
}
|
||||
rec := map[string]interface{}{
|
||||
__nyx_emit(map[string]interface{}{
|
||||
"sink_callee": sinkCallee,
|
||||
"args": serArgs,
|
||||
"captured_at_ns": uint64(time.Now().UnixNano()),
|
||||
"payload_id": os.Getenv("NYX_PAYLOAD_ID"),
|
||||
"kind": map[string]interface{}{"kind": "Normal"},
|
||||
"witness": __nyx_witness(sinkCallee, args),
|
||||
})
|
||||
}
|
||||
|
||||
// Phase 08: install a sink-site signal listener via `signal.Notify`. Go
|
||||
// can intercept SIGABRT but not SIGSEGV (the Go runtime panics on
|
||||
// memory faults before user handlers see them); for SIGSEGV we rely on
|
||||
// the runtime's panic catch via `recover()` inside __nyx_run_sink.
|
||||
func __nyx_install_crash_guard(sinkCallee string) {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, syscall.SIGABRT, syscall.SIGBUS, syscall.SIGFPE, syscall.SIGILL)
|
||||
go func() {
|
||||
sig := <-ch
|
||||
name := "SIGABRT"
|
||||
switch sig {
|
||||
case syscall.SIGBUS: name = "SIGBUS"
|
||||
case syscall.SIGFPE: name = "SIGFPE"
|
||||
case syscall.SIGILL: name = "SIGILL"
|
||||
}
|
||||
__nyx_emit(map[string]interface{}{
|
||||
"sink_callee": sinkCallee,
|
||||
"args": []interface{}{},
|
||||
"captured_at_ns": uint64(time.Now().UnixNano()),
|
||||
"payload_id": os.Getenv("NYX_PAYLOAD_ID"),
|
||||
"kind": map[string]interface{}{"kind": "Crash", "signal": name},
|
||||
"witness": __nyx_witness(sinkCallee, nil),
|
||||
})
|
||||
signal.Reset(sig)
|
||||
syscall.Kill(syscall.Getpid(), sig.(syscall.Signal))
|
||||
}()
|
||||
}
|
||||
|
||||
// Phase 08: panic-recover hook for Go runtime-caught faults (SIGSEGV nil-
|
||||
// deref, divide-by-zero treated as panic). Call as `defer __nyx_recover_crash("callee")()`
|
||||
// around the instrumented sink invocation.
|
||||
func __nyx_recover_crash(sinkCallee string) func() {
|
||||
return func() {
|
||||
if r := recover(); r != nil {
|
||||
__nyx_emit(map[string]interface{}{
|
||||
"sink_callee": sinkCallee,
|
||||
"args": []interface{}{},
|
||||
"captured_at_ns": uint64(time.Now().UnixNano()),
|
||||
"payload_id": os.Getenv("NYX_PAYLOAD_ID"),
|
||||
"kind": map[string]interface{}{"kind": "Crash", "signal": "SIGSEGV"},
|
||||
"witness": __nyx_witness(sinkCallee, nil),
|
||||
})
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(rec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
f.Write(b)
|
||||
f.Write([]byte("\n"))
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,16 +64,78 @@ impl LangEmitter for JavaEmitter {
|
|||
/// [`crate::dynamic::probe::SinkProbe`] wire format.
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1) ──────────────────────────────────
|
||||
static void __nyx_probe(String sinkCallee, String... args) {
|
||||
String p = System.getenv("NYX_PROBE_PATH");
|
||||
if (p == null || p.isEmpty()) {
|
||||
return;
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──
|
||||
private static final String[] __NYX_DENY = {
|
||||
"TOKEN","SECRET","PASSWORD","PASSWD","API_KEY","APIKEY","PRIVATE_KEY",
|
||||
"CREDENTIAL","SESSION","COOKIE","AUTH","BEARER","AWS_ACCESS","AWS_SESSION",
|
||||
"GH_TOKEN","GITHUB_TOKEN","NPM_TOKEN","PYPI_TOKEN","DOCKER_PASS"
|
||||
};
|
||||
private static final int __NYX_PAYLOAD_LIMIT = 16 * 1024;
|
||||
private static final String __NYX_REDACTED = "<redacted-by-nyx-policy>";
|
||||
|
||||
private static boolean nyxIsDeniedKey(String k) {
|
||||
String ku = k.toUpperCase();
|
||||
for (String n : __NYX_DENY) {
|
||||
if (ku.contains(n)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String nyxWitnessJson(String sinkCallee, String[] args) {
|
||||
StringBuilder out = new StringBuilder(256);
|
||||
out.append("{\"env_snapshot\":{");
|
||||
boolean first = true;
|
||||
java.util.TreeMap<String,String> envSorted = new java.util.TreeMap<>(System.getenv());
|
||||
for (java.util.Map.Entry<String,String> e : envSorted.entrySet()) {
|
||||
if (!first) out.append(',');
|
||||
first = false;
|
||||
out.append('"'); nyxJsonEscape(e.getKey(), out); out.append("\":\"");
|
||||
if (nyxIsDeniedKey(e.getKey())) {
|
||||
out.append(__NYX_REDACTED);
|
||||
} else {
|
||||
nyxJsonEscape(e.getValue() == null ? "" : e.getValue(), out);
|
||||
}
|
||||
out.append('"');
|
||||
}
|
||||
out.append("},\"cwd\":\"");
|
||||
nyxJsonEscape(System.getProperty("user.dir", ""), out);
|
||||
out.append("\",\"payload_bytes\":[");
|
||||
String payload = System.getenv("NYX_PAYLOAD");
|
||||
if (payload != null) {
|
||||
byte[] pb = payload.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
int cap = Math.min(pb.length, __NYX_PAYLOAD_LIMIT);
|
||||
for (int i = 0; i < cap; i++) {
|
||||
if (i > 0) out.append(',');
|
||||
out.append(((int) pb[i]) & 0xff);
|
||||
}
|
||||
}
|
||||
out.append("],\"callee\":\""); nyxJsonEscape(sinkCallee, out);
|
||||
out.append("\",\"args_repr\":[");
|
||||
if (args != null) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (i > 0) out.append(',');
|
||||
out.append('"'); nyxJsonEscape(args[i] == null ? "" : args[i], out); out.append('"');
|
||||
}
|
||||
}
|
||||
out.append("]}");
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static void nyxEmit(String line) {
|
||||
String p = System.getenv("NYX_PROBE_PATH");
|
||||
if (p == null || p.isEmpty()) return;
|
||||
try (java.io.FileWriter fw = new java.io.FileWriter(p, true)) {
|
||||
fw.write(line);
|
||||
} catch (java.io.IOException e) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
static void __nyx_probe(String sinkCallee, String... args) {
|
||||
long now = System.nanoTime();
|
||||
String payloadId = System.getenv("NYX_PAYLOAD_ID");
|
||||
if (payloadId == null) payloadId = "";
|
||||
StringBuilder line = new StringBuilder(128);
|
||||
StringBuilder line = new StringBuilder(256);
|
||||
line.append("{\"sink_callee\":\"");
|
||||
nyxJsonEscape(sinkCallee, line);
|
||||
line.append("\",\"args\":[");
|
||||
|
|
@ -85,12 +147,33 @@ pub fn probe_shim() -> &'static str {
|
|||
}
|
||||
line.append("],\"captured_at_ns\":").append(now).append(",\"payload_id\":\"");
|
||||
nyxJsonEscape(payloadId, line);
|
||||
line.append("\"}\n");
|
||||
try (java.io.FileWriter fw = new java.io.FileWriter(p, true)) {
|
||||
fw.write(line.toString());
|
||||
} catch (java.io.IOException e) {
|
||||
// best-effort
|
||||
}
|
||||
line.append("\",\"kind\":{\"kind\":\"Normal\"},\"witness\":");
|
||||
line.append(nyxWitnessJson(sinkCallee, args));
|
||||
line.append("}\n");
|
||||
nyxEmit(line.toString());
|
||||
}
|
||||
|
||||
// Phase 08: install a sink-site Throwable handler. Java cannot catch
|
||||
// SIGSEGV / SIGFPE directly (JVM aborts), but it can intercept the
|
||||
// uncaught-exception path which fires for any Error / RuntimeException
|
||||
// escaping the sink call. Map them onto SIGABRT for the oracle.
|
||||
static void __nyx_install_crash_guard(String sinkCallee) {
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
long now = System.nanoTime();
|
||||
String payloadId = System.getenv("NYX_PAYLOAD_ID");
|
||||
if (payloadId == null) payloadId = "";
|
||||
StringBuilder line = new StringBuilder(256);
|
||||
line.append("{\"sink_callee\":\"");
|
||||
nyxJsonEscape(sinkCallee, line);
|
||||
line.append("\",\"args\":[],\"captured_at_ns\":").append(now)
|
||||
.append(",\"payload_id\":\"");
|
||||
nyxJsonEscape(payloadId, line);
|
||||
line.append("\",\"kind\":{\"kind\":\"Crash\",\"signal\":\"SIGABRT\"},\"witness\":");
|
||||
line.append(nyxWitnessJson(sinkCallee, new String[0]));
|
||||
line.append("}\n");
|
||||
nyxEmit(line.toString());
|
||||
System.exit(134);
|
||||
});
|
||||
}
|
||||
|
||||
private static void nyxJsonEscape(String s, StringBuilder out) {
|
||||
|
|
|
|||
|
|
@ -58,11 +58,62 @@ impl LangEmitter for JavaScriptEmitter {
|
|||
/// unset.
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1) ──────────────────────────────────
|
||||
function __nyx_probe(sinkCallee, ...args) {
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──────
|
||||
const _NYX_DENY_SUBSTRINGS = [
|
||||
'TOKEN','SECRET','PASSWORD','PASSWD','API_KEY','APIKEY','PRIVATE_KEY',
|
||||
'CREDENTIAL','SESSION','COOKIE','AUTH','BEARER','AWS_ACCESS','AWS_SESSION',
|
||||
'GH_TOKEN','GITHUB_TOKEN','NPM_TOKEN','PYPI_TOKEN','DOCKER_PASS'
|
||||
];
|
||||
const _NYX_PAYLOAD_LIMIT = 16 * 1024;
|
||||
const _NYX_REDACTED = '<redacted-by-nyx-policy>';
|
||||
|
||||
function __nyx_scrub_env() {
|
||||
const out = {};
|
||||
const env = process.env || {};
|
||||
for (const k of Object.keys(env)) {
|
||||
const ku = String(k).toUpperCase();
|
||||
if (_NYX_DENY_SUBSTRINGS.some((n) => ku.indexOf(n) !== -1)) {
|
||||
out[k] = _NYX_REDACTED;
|
||||
} else {
|
||||
out[k] = env[k];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function __nyx_witness(sinkCallee, args) {
|
||||
let payload = process.env.NYX_PAYLOAD || '';
|
||||
let buf = Buffer.from(String(payload), 'utf8');
|
||||
if (buf.length > _NYX_PAYLOAD_LIMIT) buf = buf.slice(0, _NYX_PAYLOAD_LIMIT);
|
||||
const argsRepr = args.map(function (a) {
|
||||
if (a && typeof a === 'object' && (a instanceof Buffer || a instanceof Uint8Array)) {
|
||||
return '<bytes:' + a.length + '>';
|
||||
}
|
||||
return String(a);
|
||||
});
|
||||
let cwd = '';
|
||||
try { cwd = process.cwd(); } catch (e) {}
|
||||
return {
|
||||
env_snapshot: __nyx_scrub_env(),
|
||||
cwd: cwd,
|
||||
payload_bytes: Array.from(buf),
|
||||
callee: String(sinkCallee),
|
||||
args_repr: argsRepr,
|
||||
};
|
||||
}
|
||||
|
||||
function __nyx_emit(rec) {
|
||||
const _fs = require('fs');
|
||||
const _p = process.env.NYX_PROBE_PATH;
|
||||
if (!_p) return;
|
||||
try {
|
||||
_fs.appendFileSync(_p, JSON.stringify(rec) + '\n');
|
||||
} catch (e) {
|
||||
// best-effort: probe channel write failure is non-fatal.
|
||||
}
|
||||
}
|
||||
|
||||
function __nyx_probe(sinkCallee, ...args) {
|
||||
const _ser = args.map(function (a) {
|
||||
if (a && typeof a === 'object' && (a instanceof Buffer || a instanceof Uint8Array)) {
|
||||
return { kind: 'Bytes', value: Array.from(a) };
|
||||
|
|
@ -75,16 +126,49 @@ function __nyx_probe(sinkCallee, ...args) {
|
|||
}
|
||||
return { kind: 'String', value: String(a) };
|
||||
});
|
||||
const _rec = {
|
||||
__nyx_emit({
|
||||
sink_callee: String(sinkCallee),
|
||||
args: _ser,
|
||||
captured_at_ns: Number(process.hrtime.bigint()),
|
||||
payload_id: String(process.env.NYX_PAYLOAD_ID || ''),
|
||||
kind: { kind: 'Normal' },
|
||||
witness: __nyx_witness(sinkCallee, args),
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 08: V8 cannot catch native SIGSEGV in pure JS, but it can intercept
|
||||
// `uncaughtException` / `unhandledRejection` plus the synchronously
|
||||
// deliverable signals (SIGABRT via process.kill). __nyx_install_crash_guard
|
||||
// registers both: the uncaught path maps Error-shaped failures to a SIGABRT
|
||||
// crash probe; explicit process.on('SIG*') registers the others where the
|
||||
// runtime exposes them. Re-raise via process.exit(134) so the outcome's
|
||||
// exit_code still reflects an abort-style death.
|
||||
function __nyx_install_crash_guard(sinkCallee) {
|
||||
const _emit_crash = function (signalName) {
|
||||
__nyx_emit({
|
||||
sink_callee: String(sinkCallee),
|
||||
args: [],
|
||||
captured_at_ns: Number(process.hrtime.bigint()),
|
||||
payload_id: String(process.env.NYX_PAYLOAD_ID || ''),
|
||||
kind: { kind: 'Crash', signal: signalName },
|
||||
witness: __nyx_witness(sinkCallee, []),
|
||||
});
|
||||
};
|
||||
try {
|
||||
_fs.appendFileSync(_p, JSON.stringify(_rec) + '\n');
|
||||
} catch (e) {
|
||||
// best-effort: probe channel write failure is non-fatal.
|
||||
process.on('uncaughtException', function (_err) {
|
||||
_emit_crash('SIGABRT');
|
||||
process.exit(134);
|
||||
});
|
||||
process.on('unhandledRejection', function (_reason) {
|
||||
_emit_crash('SIGABRT');
|
||||
process.exit(134);
|
||||
});
|
||||
for (const nm of ['SIGSEGV','SIGABRT','SIGBUS','SIGFPE','SIGILL']) {
|
||||
try {
|
||||
process.on(nm, function () {
|
||||
_emit_crash(nm);
|
||||
process.exit(128 + (nm === 'SIGABRT' ? 6 : 11));
|
||||
});
|
||||
} catch (e) { /* runtime refused signal handler */ }
|
||||
}
|
||||
}
|
||||
"#
|
||||
|
|
|
|||
|
|
@ -51,12 +51,53 @@ impl LangEmitter for PhpEmitter {
|
|||
/// Track C.1).
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1) ──────────────────────────────────
|
||||
function __nyx_probe(string $sinkCallee, ...$args): void {
|
||||
$p = getenv('NYX_PROBE_PATH');
|
||||
if ($p === false || $p === '') {
|
||||
return;
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──────
|
||||
const __NYX_DENY_SUBSTRINGS = [
|
||||
'TOKEN','SECRET','PASSWORD','PASSWD','API_KEY','APIKEY','PRIVATE_KEY',
|
||||
'CREDENTIAL','SESSION','COOKIE','AUTH','BEARER','AWS_ACCESS','AWS_SESSION',
|
||||
'GH_TOKEN','GITHUB_TOKEN','NPM_TOKEN','PYPI_TOKEN','DOCKER_PASS',
|
||||
];
|
||||
const __NYX_PAYLOAD_LIMIT = 16 * 1024;
|
||||
const __NYX_REDACTED = '<redacted-by-nyx-policy>';
|
||||
|
||||
function __nyx_is_denied_key(string $k): bool {
|
||||
$ku = strtoupper($k);
|
||||
foreach (__NYX_DENY_SUBSTRINGS as $n) {
|
||||
if (strpos($ku, $n) !== false) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function __nyx_witness(string $sinkCallee, array $args): array {
|
||||
$env = [];
|
||||
foreach ($_ENV as $k => $v) {
|
||||
$env[(string)$k] = __nyx_is_denied_key((string)$k) ? __NYX_REDACTED : (string)$v;
|
||||
}
|
||||
// Sort for deterministic output.
|
||||
ksort($env);
|
||||
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
|
||||
$pb = substr($payload, 0, __NYX_PAYLOAD_LIMIT);
|
||||
$bytes = [];
|
||||
for ($i = 0; $i < strlen($pb); $i++) $bytes[] = ord($pb[$i]);
|
||||
$repr = [];
|
||||
foreach ($args as $a) $repr[] = is_string($a) ? $a : (string) $a;
|
||||
return [
|
||||
'env_snapshot' => $env,
|
||||
'cwd' => @getcwd() ?: '',
|
||||
'payload_bytes' => $bytes,
|
||||
'callee' => $sinkCallee,
|
||||
'args_repr' => $repr,
|
||||
];
|
||||
}
|
||||
|
||||
function __nyx_emit(array $rec): void {
|
||||
$p = getenv('NYX_PROBE_PATH');
|
||||
if ($p === false || $p === '') return;
|
||||
$line = json_encode($rec) . "\n";
|
||||
@file_put_contents($p, $line, FILE_APPEND);
|
||||
}
|
||||
|
||||
function __nyx_probe(string $sinkCallee, ...$args): void {
|
||||
$ser = [];
|
||||
foreach ($args as $a) {
|
||||
if (is_int($a)) {
|
||||
|
|
@ -65,14 +106,57 @@ function __nyx_probe(string $sinkCallee, ...$args): void {
|
|||
$ser[] = ['kind' => 'String', 'value' => (string) $a];
|
||||
}
|
||||
}
|
||||
$rec = [
|
||||
'sink_callee' => $sinkCallee,
|
||||
'args' => $ser,
|
||||
__nyx_emit([
|
||||
'sink_callee' => $sinkCallee,
|
||||
'args' => $ser,
|
||||
'captured_at_ns' => (int) (microtime(true) * 1e9),
|
||||
'payload_id' => (string) (getenv('NYX_PAYLOAD_ID') ?: ''),
|
||||
];
|
||||
$line = json_encode($rec) . "\n";
|
||||
@file_put_contents($p, $line, FILE_APPEND);
|
||||
'payload_id' => (string) (getenv('NYX_PAYLOAD_ID') ?: ''),
|
||||
'kind' => ['kind' => 'Normal'],
|
||||
'witness' => __nyx_witness($sinkCallee, $args),
|
||||
]);
|
||||
}
|
||||
|
||||
// Phase 08: PHP cannot catch SIGSEGV from userland, but pcntl_signal and
|
||||
// register_shutdown_function intercept SIGABRT-class fatal errors.
|
||||
function __nyx_install_crash_guard(string $sinkCallee): void {
|
||||
$emit_crash = function (string $signalName) use ($sinkCallee) {
|
||||
__nyx_emit([
|
||||
'sink_callee' => $sinkCallee,
|
||||
'args' => [],
|
||||
'captured_at_ns' => (int) (microtime(true) * 1e9),
|
||||
'payload_id' => (string) (getenv('NYX_PAYLOAD_ID') ?: ''),
|
||||
'kind' => ['kind' => 'Crash', 'signal' => $signalName],
|
||||
'witness' => __nyx_witness($sinkCallee, []),
|
||||
]);
|
||||
};
|
||||
set_error_handler(function ($errno, $errstr) use ($emit_crash) {
|
||||
if ($errno & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR)) {
|
||||
$emit_crash('SIGABRT');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
register_shutdown_function(function () use ($emit_crash) {
|
||||
$err = error_get_last();
|
||||
if ($err && ($err['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR))) {
|
||||
$emit_crash('SIGABRT');
|
||||
}
|
||||
});
|
||||
if (function_exists('pcntl_signal') && function_exists('pcntl_async_signals')) {
|
||||
pcntl_async_signals(true);
|
||||
foreach ([SIGABRT, SIGBUS ?? null, SIGFPE ?? null, SIGILL ?? null] as $sig) {
|
||||
if ($sig === null) continue;
|
||||
pcntl_signal($sig, function ($s) use ($emit_crash) {
|
||||
$name = 'SIGABRT';
|
||||
if (defined('SIGABRT') && $s === SIGABRT) $name = 'SIGABRT';
|
||||
if (defined('SIGBUS') && $s === SIGBUS) $name = 'SIGBUS';
|
||||
if (defined('SIGFPE') && $s === SIGFPE) $name = 'SIGFPE';
|
||||
if (defined('SIGILL') && $s === SIGILL) $name = 'SIGILL';
|
||||
$emit_crash($name);
|
||||
pcntl_signal($s, SIG_DFL);
|
||||
posix_kill(posix_getpid(), $s);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,12 +51,66 @@ impl LangEmitter for PythonEmitter {
|
|||
/// configured a probe channel.
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
# ── __nyx_probe shim (Phase 06 — Track C.1) ──────────────────────────────────
|
||||
def __nyx_probe(sink_callee, *args):
|
||||
import os, time, json
|
||||
# ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──────
|
||||
# Deny-substring list mirrors crate::dynamic::policy::DENY_KEY_SUBSTRINGS; keep
|
||||
# in sync when the host-side policy gains new entries.
|
||||
_NYX_DENY_SUBSTRINGS = (
|
||||
"TOKEN", "SECRET", "PASSWORD", "PASSWD", "API_KEY", "APIKEY",
|
||||
"PRIVATE_KEY", "CREDENTIAL", "SESSION", "COOKIE", "AUTH", "BEARER",
|
||||
"AWS_ACCESS", "AWS_SESSION", "GH_TOKEN", "GITHUB_TOKEN", "NPM_TOKEN",
|
||||
"PYPI_TOKEN", "DOCKER_PASS",
|
||||
)
|
||||
_NYX_PAYLOAD_LIMIT = 16 * 1024
|
||||
_NYX_REDACTED = "<redacted-by-nyx-policy>"
|
||||
|
||||
def __nyx_scrub_env():
|
||||
import os
|
||||
out = {}
|
||||
for k, v in os.environ.items():
|
||||
ku = str(k).upper()
|
||||
if any(n in ku for n in _NYX_DENY_SUBSTRINGS):
|
||||
out[k] = _NYX_REDACTED
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
def __nyx_witness(sink_callee, args):
|
||||
import os
|
||||
payload = os.environ.get("NYX_PAYLOAD", "")
|
||||
payload_bytes = payload.encode("utf-8", "replace") if isinstance(payload, str) else bytes(payload)
|
||||
if len(payload_bytes) > _NYX_PAYLOAD_LIMIT:
|
||||
payload_bytes = payload_bytes[:_NYX_PAYLOAD_LIMIT]
|
||||
args_repr = []
|
||||
for a in args:
|
||||
if isinstance(a, (bytes, bytearray)):
|
||||
args_repr.append("<bytes:%d>" % len(a))
|
||||
else:
|
||||
args_repr.append(str(a))
|
||||
try:
|
||||
cwd = os.getcwd()
|
||||
except OSError:
|
||||
cwd = ""
|
||||
return {
|
||||
"env_snapshot": __nyx_scrub_env(),
|
||||
"cwd": cwd,
|
||||
"payload_bytes": list(payload_bytes),
|
||||
"callee": str(sink_callee),
|
||||
"args_repr": args_repr,
|
||||
}
|
||||
|
||||
def __nyx_emit(rec):
|
||||
import os, json
|
||||
p = os.environ.get("NYX_PROBE_PATH")
|
||||
if not p:
|
||||
return
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write(json.dumps(rec) + "\n")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def __nyx_probe(sink_callee, *args):
|
||||
import os, time
|
||||
serialised = []
|
||||
for a in args:
|
||||
if isinstance(a, (bytes, bytearray)):
|
||||
|
|
@ -72,12 +126,45 @@ def __nyx_probe(sink_callee, *args):
|
|||
"args": serialised,
|
||||
"captured_at_ns": time.time_ns(),
|
||||
"payload_id": os.environ.get("NYX_PAYLOAD_ID", ""),
|
||||
"kind": {"kind": "Normal"},
|
||||
"witness": __nyx_witness(sink_callee, args),
|
||||
}
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write(json.dumps(rec) + "\n")
|
||||
except OSError:
|
||||
pass
|
||||
__nyx_emit(rec)
|
||||
|
||||
# Phase 08: sink-site signal handler. Call __nyx_install_crash_guard before
|
||||
# invoking the instrumented sink so a SIGSEGV / SIGABRT / etc. is captured as
|
||||
# a Crash probe (with witness) before the process aborts. The shim re-raises
|
||||
# the signal on the default handler after writing so process-level outcome
|
||||
# observers (exit_code) still see the death.
|
||||
_NYX_SIGNAL_NAMES = {}
|
||||
|
||||
def __nyx_install_crash_guard(sink_callee):
|
||||
import signal, os, time
|
||||
catchable = []
|
||||
for nm in ("SIGSEGV", "SIGABRT", "SIGBUS", "SIGFPE", "SIGILL"):
|
||||
s = getattr(signal, nm, None)
|
||||
if s is not None:
|
||||
catchable.append((nm, s))
|
||||
_NYX_SIGNAL_NAMES[s] = nm
|
||||
def _handler(signum, frame):
|
||||
nm = _NYX_SIGNAL_NAMES.get(signum, "SIG?")
|
||||
rec = {
|
||||
"sink_callee": str(sink_callee),
|
||||
"args": [],
|
||||
"captured_at_ns": time.time_ns(),
|
||||
"payload_id": os.environ.get("NYX_PAYLOAD_ID", ""),
|
||||
"kind": {"kind": "Crash", "signal": nm},
|
||||
"witness": __nyx_witness(sink_callee, []),
|
||||
}
|
||||
__nyx_emit(rec)
|
||||
# Reset to default and re-raise so the process actually dies.
|
||||
signal.signal(signum, signal.SIG_DFL)
|
||||
os.kill(os.getpid(), signum)
|
||||
for _nm, s in catchable:
|
||||
try:
|
||||
signal.signal(s, _handler)
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
"#
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,11 +25,50 @@ const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
|||
/// even though `emit` returns `LangUnsupported` until Phase 15 lands.
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
# ── __nyx_probe shim (Phase 06 — Track C.1) ──────────────────────────────────
|
||||
def __nyx_probe(sink_callee, *args)
|
||||
# ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──────
|
||||
__NYX_DENY_SUBSTRINGS = %w[
|
||||
TOKEN SECRET PASSWORD PASSWD API_KEY APIKEY PRIVATE_KEY CREDENTIAL SESSION
|
||||
COOKIE AUTH BEARER AWS_ACCESS AWS_SESSION GH_TOKEN GITHUB_TOKEN NPM_TOKEN
|
||||
PYPI_TOKEN DOCKER_PASS
|
||||
].freeze
|
||||
__NYX_PAYLOAD_LIMIT = 16 * 1024
|
||||
__NYX_REDACTED = '<redacted-by-nyx-policy>'
|
||||
|
||||
def __nyx_is_denied_key(k)
|
||||
ku = k.to_s.upcase
|
||||
__NYX_DENY_SUBSTRINGS.any? { |n| ku.include?(n) }
|
||||
end
|
||||
|
||||
def __nyx_witness(sink_callee, args)
|
||||
env_snapshot = {}
|
||||
ENV.each do |k, v|
|
||||
env_snapshot[k] = __nyx_is_denied_key(k) ? __NYX_REDACTED : v
|
||||
end
|
||||
payload = ENV['NYX_PAYLOAD'] || ''
|
||||
pb = payload.bytes
|
||||
pb = pb[0, __NYX_PAYLOAD_LIMIT] if pb.length > __NYX_PAYLOAD_LIMIT
|
||||
repr = args.map { |a| a.is_a?(String) ? a : a.to_s }
|
||||
cwd = (Dir.pwd rescue '')
|
||||
{
|
||||
env_snapshot: env_snapshot,
|
||||
cwd: cwd,
|
||||
payload_bytes: pb,
|
||||
callee: sink_callee.to_s,
|
||||
args_repr: repr,
|
||||
}
|
||||
end
|
||||
|
||||
def __nyx_emit(rec)
|
||||
require 'json'
|
||||
p = ENV['NYX_PROBE_PATH']
|
||||
return if p.nil? || p.empty?
|
||||
begin
|
||||
File.open(p, 'a') { |f| f.puts(rec.to_json) }
|
||||
rescue StandardError
|
||||
end
|
||||
end
|
||||
|
||||
def __nyx_probe(sink_callee, *args)
|
||||
ser = args.map do |a|
|
||||
case a
|
||||
when Integer then { kind: 'Int', value: a }
|
||||
|
|
@ -37,15 +76,36 @@ def __nyx_probe(sink_callee, *args)
|
|||
else { kind: 'String', value: a.to_s }
|
||||
end
|
||||
end
|
||||
rec = {
|
||||
__nyx_emit({
|
||||
sink_callee: sink_callee.to_s,
|
||||
args: ser,
|
||||
captured_at_ns: (Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)),
|
||||
payload_id: (ENV['NYX_PAYLOAD_ID'] || ''),
|
||||
}
|
||||
begin
|
||||
File.open(p, 'a') { |f| f.puts(rec.to_json) }
|
||||
rescue StandardError
|
||||
kind: { kind: 'Normal' },
|
||||
witness: __nyx_witness(sink_callee, args),
|
||||
})
|
||||
end
|
||||
|
||||
# Phase 08: install a sink-site signal trap. Ruby traps run in interrupt
|
||||
# context but can write to a file before re-raising via Process.kill.
|
||||
def __nyx_install_crash_guard(sink_callee)
|
||||
%w[SEGV ABRT BUS FPE ILL].each do |nm|
|
||||
begin
|
||||
Signal.trap(nm) do
|
||||
__nyx_emit({
|
||||
sink_callee: sink_callee.to_s,
|
||||
args: [],
|
||||
captured_at_ns: (Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)),
|
||||
payload_id: (ENV['NYX_PAYLOAD_ID'] || ''),
|
||||
kind: { kind: 'Crash', signal: "SIG#{nm}" },
|
||||
witness: __nyx_witness(sink_callee, []),
|
||||
})
|
||||
Signal.trap(nm, 'DEFAULT')
|
||||
Process.kill(nm, Process.pid)
|
||||
end
|
||||
rescue ArgumentError, Errno::EINVAL
|
||||
# signal not supported on this platform
|
||||
end
|
||||
end
|
||||
end
|
||||
"#
|
||||
|
|
|
|||
|
|
@ -61,58 +61,197 @@ impl LangEmitter for RustEmitter {
|
|||
/// [`crate::dynamic::probe::SinkProbe`] wire format.
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1) ──────────────────────────────────
|
||||
// ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──────
|
||||
#[allow(dead_code)]
|
||||
fn __nyx_probe(sink_callee: &str, args: &[&str]) {
|
||||
const __NYX_DENY_SUBSTRINGS: &[&str] = &[
|
||||
"TOKEN","SECRET","PASSWORD","PASSWD","API_KEY","APIKEY","PRIVATE_KEY",
|
||||
"CREDENTIAL","SESSION","COOKIE","AUTH","BEARER","AWS_ACCESS","AWS_SESSION",
|
||||
"GH_TOKEN","GITHUB_TOKEN","NPM_TOKEN","PYPI_TOKEN","DOCKER_PASS",
|
||||
];
|
||||
#[allow(dead_code)]
|
||||
const __NYX_PAYLOAD_LIMIT: usize = 16 * 1024;
|
||||
#[allow(dead_code)]
|
||||
const __NYX_REDACTED: &str = "<redacted-by-nyx-policy>";
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn __nyx_esc(s: &str, out: &mut String) {
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn __nyx_witness_json(sink_callee: &str, args: &[&str]) -> String {
|
||||
let mut out = String::with_capacity(256);
|
||||
out.push_str("{\"env_snapshot\":{");
|
||||
let mut first = true;
|
||||
let mut keys: Vec<(String, String)> = std::env::vars().collect();
|
||||
keys.sort();
|
||||
for (k, v) in keys {
|
||||
let ku = k.to_ascii_uppercase();
|
||||
let denied = __NYX_DENY_SUBSTRINGS.iter().any(|n| ku.contains(n));
|
||||
let val = if denied { __NYX_REDACTED } else { v.as_str() };
|
||||
if !first { out.push(','); }
|
||||
first = false;
|
||||
out.push('"');
|
||||
__nyx_esc(&k, &mut out);
|
||||
out.push_str("\":\"");
|
||||
__nyx_esc(val, &mut out);
|
||||
out.push('"');
|
||||
}
|
||||
out.push_str("},\"cwd\":\"");
|
||||
let cwd = std::env::current_dir()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
__nyx_esc(&cwd, &mut out);
|
||||
out.push_str("\",\"payload_bytes\":[");
|
||||
let payload = std::env::var("NYX_PAYLOAD").unwrap_or_default();
|
||||
let bytes = payload.as_bytes();
|
||||
let cap = bytes.len().min(__NYX_PAYLOAD_LIMIT);
|
||||
for i in 0..cap {
|
||||
if i > 0 { out.push(','); }
|
||||
out.push_str(&format!("{}", bytes[i]));
|
||||
}
|
||||
out.push_str("],\"callee\":\"");
|
||||
__nyx_esc(sink_callee, &mut out);
|
||||
out.push_str("\",\"args_repr\":[");
|
||||
for (i, a) in args.iter().enumerate() {
|
||||
if i > 0 { out.push(','); }
|
||||
out.push('"');
|
||||
__nyx_esc(a, &mut out);
|
||||
out.push('"');
|
||||
}
|
||||
out.push_str("]}");
|
||||
out
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn __nyx_emit(line: &str) {
|
||||
use std::io::Write;
|
||||
let p = match std::env::var("NYX_PROBE_PATH") {
|
||||
Ok(v) => v,
|
||||
Err(_) => return,
|
||||
};
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let payload_id = std::env::var("NYX_PAYLOAD_ID").unwrap_or_default();
|
||||
fn esc(s: &str, out: &mut String) {
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut line = String::with_capacity(128);
|
||||
line.push_str("{\"sink_callee\":\"");
|
||||
esc(sink_callee, &mut line);
|
||||
line.push_str("\",\"args\":[");
|
||||
for (i, a) in args.iter().enumerate() {
|
||||
if i > 0 {
|
||||
line.push(',');
|
||||
}
|
||||
line.push_str("{\"kind\":\"String\",\"value\":\"");
|
||||
esc(a, &mut line);
|
||||
line.push_str("\"}");
|
||||
}
|
||||
line.push_str(&format!(
|
||||
"],\"captured_at_ns\":{},\"payload_id\":\"",
|
||||
now
|
||||
));
|
||||
esc(&payload_id, &mut line);
|
||||
line.push_str("\"}\n");
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&p)
|
||||
{
|
||||
let _ = f.write_all(line.as_bytes());
|
||||
let _ = f.write_all(b"\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn __nyx_probe(sink_callee: &str, args: &[&str]) {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let payload_id = std::env::var("NYX_PAYLOAD_ID").unwrap_or_default();
|
||||
let mut line = String::with_capacity(256);
|
||||
line.push_str("{\"sink_callee\":\"");
|
||||
__nyx_esc(sink_callee, &mut line);
|
||||
line.push_str("\",\"args\":[");
|
||||
for (i, a) in args.iter().enumerate() {
|
||||
if i > 0 { line.push(','); }
|
||||
line.push_str("{\"kind\":\"String\",\"value\":\"");
|
||||
__nyx_esc(a, &mut line);
|
||||
line.push_str("\"}");
|
||||
}
|
||||
line.push_str(&format!(
|
||||
"],\"captured_at_ns\":{},\"payload_id\":\"",
|
||||
now
|
||||
));
|
||||
__nyx_esc(&payload_id, &mut line);
|
||||
line.push_str("\",\"kind\":{\"kind\":\"Normal\"},\"witness\":");
|
||||
line.push_str(&__nyx_witness_json(sink_callee, args));
|
||||
line.push('}');
|
||||
__nyx_emit(&line);
|
||||
}
|
||||
|
||||
// Phase 08: install a sink-site signal handler via `libc::sigaction` so a
|
||||
// SIGSEGV / SIGABRT / etc. inside the sink call is captured as a Crash
|
||||
// probe before the kernel re-delivers it via SIG_DFL. The shim is
|
||||
// no-op on non-Unix targets (the dynamic-verification supported set is
|
||||
// Unix-only) so consumers can splice it unconditionally.
|
||||
#[cfg(unix)]
|
||||
#[allow(dead_code)]
|
||||
fn __nyx_install_crash_guard(sink_callee: &'static str) {
|
||||
use std::sync::atomic::{AtomicPtr, Ordering};
|
||||
static SINK_CALLEE: AtomicPtr<u8> = AtomicPtr::new(std::ptr::null_mut());
|
||||
SINK_CALLEE.store(sink_callee.as_ptr() as *mut u8, Ordering::SeqCst);
|
||||
let len = sink_callee.len();
|
||||
static CALLEE_LEN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
CALLEE_LEN.store(len, Ordering::SeqCst);
|
||||
extern "C" fn handler(sig: i32) {
|
||||
// async-signal-unsafe code is unavoidable here (file I/O); we
|
||||
// accept the risk because the process is already dying and we
|
||||
// need the forensic record.
|
||||
let name = match sig {
|
||||
libc::SIGSEGV => "SIGSEGV",
|
||||
libc::SIGABRT => "SIGABRT",
|
||||
libc::SIGBUS => "SIGBUS",
|
||||
libc::SIGFPE => "SIGFPE",
|
||||
libc::SIGILL => "SIGILL",
|
||||
_ => "SIGABRT",
|
||||
};
|
||||
let p = SINK_CALLEE.load(Ordering::SeqCst);
|
||||
let len = CALLEE_LEN.load(Ordering::SeqCst);
|
||||
let sink_callee: &str = unsafe {
|
||||
if p.is_null() {
|
||||
""
|
||||
} else {
|
||||
let slice = std::slice::from_raw_parts(p as *const u8, len);
|
||||
std::str::from_utf8_unchecked(slice)
|
||||
}
|
||||
};
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let payload_id = std::env::var("NYX_PAYLOAD_ID").unwrap_or_default();
|
||||
let mut line = String::with_capacity(256);
|
||||
line.push_str("{\"sink_callee\":\"");
|
||||
__nyx_esc(sink_callee, &mut line);
|
||||
line.push_str("\",\"args\":[],\"captured_at_ns\":");
|
||||
line.push_str(&format!("{now},\"payload_id\":\""));
|
||||
__nyx_esc(&payload_id, &mut line);
|
||||
line.push_str("\",\"kind\":{\"kind\":\"Crash\",\"signal\":\"");
|
||||
line.push_str(name);
|
||||
line.push_str("\"},\"witness\":");
|
||||
line.push_str(&__nyx_witness_json(sink_callee, &[]));
|
||||
line.push('}');
|
||||
__nyx_emit(&line);
|
||||
// Restore default handler and re-raise so process actually dies.
|
||||
unsafe {
|
||||
let mut sa: libc::sigaction = std::mem::zeroed();
|
||||
sa.sa_sigaction = libc::SIG_DFL;
|
||||
libc::sigaction(sig, &sa, std::ptr::null_mut());
|
||||
libc::raise(sig);
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
let mut sa: libc::sigaction = std::mem::zeroed();
|
||||
sa.sa_sigaction = handler as usize;
|
||||
libc::sigemptyset(&mut sa.sa_mask);
|
||||
for sig in [libc::SIGSEGV, libc::SIGABRT, libc::SIGBUS, libc::SIGFPE, libc::SIGILL] {
|
||||
libc::sigaction(sig, &sa, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[allow(dead_code)]
|
||||
fn __nyx_install_crash_guard(_sink_callee: &'static str) {}
|
||||
"#
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue