From de139b07d259b6a2f535d9a7debd0919736b84e8 Mon Sep 17 00:00:00 2001 From: Karan Sharma <112211550+karans4@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:56:55 -0500 Subject: [PATCH] probes: Evict finalized statement pointers --- src/bpf/sqlite.bpf.c | 24 ++++++++++++++++++++++++ src/probes/sqlite.js | 18 ++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/bpf/sqlite.bpf.c b/src/bpf/sqlite.bpf.c index cd1df60..96b2602 100644 --- a/src/bpf/sqlite.bpf.c +++ b/src/bpf/sqlite.bpf.c @@ -50,6 +50,7 @@ enum ev_kind { EV_BIND = 2, EV_STEP = 3, EV_EXEC = 4, // sqlite3_exec() one-shot: SQL + total latency + result code + EV_FINALIZE = 5, // sqlite3_finalize(): the stmt pointer is dead after this }; // Bound-value type. REAL is captured as a *type only*: doubles arrive in an @@ -320,6 +321,29 @@ int BPF_URETPROBE(exec_return, int rc) return 0; } +// ── finalize(stmt) — entry-only, no pairing needed ─────────────────────────── +// A finalized stmt pointer returns to the allocator and WILL be reused for a +// future statement. Without eviction the reused pointer aliases the dead one: +// the `known` entry suppresses zSql recovery for the new statement, and +// userspace keeps attributing the old SQL to it. Emit the death so both sides +// forget the pointer. +SEC("uprobe") +int BPF_UPROBE(finalize_entry, void *stmt) +{ + if (!stmt) { + return 0; + } + __u64 key = (__u64)stmt; + bpf_map_delete_elem(&known, &key); + + struct sqlite_event *e = new_event(EV_FINALIZE); + if (e) { + e->stmt = key; + bpf_ringbuf_submit(e, 0); + } + return 0; +} + // ── bind_*(stmt, idx, value, …) — entry-only, no pairing needed ─────────────── static __always_inline void emit_bind(void *stmt, int idx, enum bind_type bt, __s64 ival, const char *text) diff --git a/src/probes/sqlite.js b/src/probes/sqlite.js index bd258a7..751010b 100644 --- a/src/probes/sqlite.js +++ b/src/probes/sqlite.js @@ -15,7 +15,7 @@ import { BpfObject, RingBuf } from "yeet:bpf"; import { from, signal } from "yeet:tui"; // ── constants shared with sqlite.bpf.c ────────────────────────────────────── -const EV = { PREPARE: 1, BIND: 2, STEP: 3, EXEC: 4 }; +const EV = { PREPARE: 1, BIND: 2, STEP: 3, EXEC: 4, FINALIZE: 5 }; const BT = { NULL: 0, INT: 1, TEXT: 2, REAL: 3 }; // sqlite result codes we distinguish (see lib/format.js for names/colors). const SQLITE_ROW = 100; @@ -54,6 +54,7 @@ const ATTACH = [ ["bind_text", up("sqlite3_bind_text")], ["bind_null", up("sqlite3_bind_null")], ["bind_double", up("sqlite3_bind_double")], + ["finalize_entry", up("sqlite3_finalize")], ]; const load = () => { @@ -203,6 +204,18 @@ export const statements = from((state) => { return; } + // The stmt pointer is dead — the allocator will hand it to a future + // statement, so forget everything tied to it. A lingering execution + // that did real work is complete by definition (nothing can step a + // finalized stmt); flush it rather than drop it. + if (ev.kind === EV.FINALIZE) { + const e = cur.get(stmtId); + if (e && (e.steps > 0 || e.params.size > 0)) finalize(e); + cur.delete(stmtId); + sqlByStmt.delete(stmtId); + return; + } + if (ev.kind === EV.BIND) { // A bind after the previous execution already stepped is a // reset+rebind — a new execution of a cached statement. @@ -268,7 +281,7 @@ export const statements = from((state) => { if (import.meta.main) { const ctl = await load(); const rb = new RingBuf(ctl, "sql_events"); - const kindName = { 1: "PREPARE", 2: "BIND", 3: "STEP", 4: "EXEC" }; + const kindName = { 1: "PREPARE", 2: "BIND", 3: "STEP", 4: "EXEC", 5: "FINALIZE" }; console.log(`[sqlite] attached ${ATTACH.length} probes on ${SQLITE_LIB} — waiting…`); rb.subscribe((w) => { const e = unwrap(w); @@ -276,6 +289,7 @@ if (import.meta.main) { if (e.kind === EV.PREPARE) console.log(`${head} rc=${e.rc} sql=${JSON.stringify(cstr(e.text))}`); else if (e.kind === EV.EXEC) console.log(`${head} rc=${e.rc} latency=${Number(e.latency_ns)}ns sql=${JSON.stringify(cstr(e.text))}${e.rc ? ` err=${JSON.stringify(cstr(e.errmsg))}` : ""}`); else if (e.kind === EV.BIND) console.log(`${head} bind #${e.param_idx} = ${JSON.stringify(bindValue(e).text)}`); + else if (e.kind === EV.FINALIZE) console.log(head); else console.log(`${head} rc=${e.rc} latency=${Number(e.latency_ns)}ns`); }); await new Promise(() => {});