-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsqlite_trace.c
More file actions
843 lines (760 loc) · 27.3 KB
/
Copy pathsqlite_trace.c
File metadata and controls
843 lines (760 loc) · 27.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <time.h>
#include <getopt.h>
#include <linux/types.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <bpf/btf.h>
#include "sqlite_trace.skel.h"
#include "sqlite_trace.h"
#include "detect_version.h"
#include "target_resolve.h"
#include "btf_merge.h"
#include "app_context.h"
/* Last-resort install location of the bundled per-version SQLite BTF blobs, used
* only when --btf-dir, $SQLITE_TRACE_BTF_DIR, and the .btf-cache next to the
* binary (see btf_dir_next_to_exe) all come up empty. */
#ifndef DEFAULT_BTF_DIR
#define DEFAULT_BTF_DIR "/usr/share/sqlite-trace/btf"
#endif
/* The committed BTF blobs + sqlite-sources.tsv live in the repo's .btf-cache
* (the canonical store; build/btf is just a symlink into it). The loader builds
* to <repo>/build/sqlite_trace, so a freshly-cloned, freshly-built tree can find
* the blobs with no --btf-dir / env var. Probe these paths relative to the dir
* of /proc/self/exe, in order, and return the first whose manifest is readable:
* ../.btf-cache — canonical store, one level up from build/ (clone-and-run)
* ./btf — build/btf symlink, or an installed sibling layout
* Returns a pointer into the static buffer, or NULL when none match. */
static const char *btf_dir_next_to_exe(void) {
static char dir[PATH_MAX];
ssize_t n = readlink("/proc/self/exe", dir, sizeof(dir) - 1);
if (n <= 0 || (size_t)n >= sizeof(dir) - 1)
return NULL;
dir[n] = '\0';
char *slash = strrchr(dir, '/');
if (!slash)
return NULL;
*slash = '\0'; /* dir is now the executable's directory. */
static const char *rel[] = { "../.btf-cache", "btf" };
for (size_t i = 0; i < sizeof(rel) / sizeof(rel[0]); i++) {
char probe[PATH_MAX];
int len = snprintf(probe, sizeof(probe), "%s/%s/sqlite-sources.tsv",
dir, rel[i]);
if (len < 0 || (size_t)len >= sizeof(probe))
continue;
if (access(probe, R_OK) != 0)
continue;
static char found[PATH_MAX];
len = snprintf(found, sizeof(found), "%s/%s", dir, rel[i]);
if (len < 0 || (size_t)len >= sizeof(found))
continue;
return found;
}
return NULL;
}
static volatile int exiting;
static void sig_handler(int sig) { exiting = 1; }
/* Added to an event's start_boot_ns (CLOCK_BOOTTIME) to get a CLOCK_REALTIME
* instant: realtime - boottime, computed once at startup. */
static __s64 boot_realtime_off_ns;
static void init_boot_realtime_offset(void)
{
struct timespec rt, bt;
if (clock_gettime(CLOCK_REALTIME, &rt) != 0 ||
clock_gettime(CLOCK_BOOTTIME, &bt) != 0) {
boot_realtime_off_ns = 0;
return;
}
__s64 rt_ns = (__s64)rt.tv_sec * 1000000000LL + rt.tv_nsec;
__s64 bt_ns = (__s64)bt.tv_sec * 1000000000LL + bt.tv_nsec;
boot_realtime_off_ns = rt_ns - bt_ns;
}
/* Write the event start time as an RFC 3339 UTC string with nanoseconds into
* buf. Returns 0 if no timestamp is available. */
static int fmt_event_time(__u64 start_boot_ns, char *buf, size_t n)
{
if (start_boot_ns == 0 || boot_realtime_off_ns == 0)
return 0;
__s64 real_ns = (__s64)start_boot_ns + boot_realtime_off_ns;
time_t secs = real_ns / 1000000000LL;
long frac = real_ns % 1000000000LL;
struct tm tm;
gmtime_r(&secs, &tm);
char base[32];
strftime(base, sizeof(base), "%Y-%m-%dT%H:%M:%S", &tm);
snprintf(buf, n, "%s.%09uZ", base, (unsigned)(frac % 1000000000LL));
return 1;
}
/* When set (--ndjson), each sql_event is written as one NDJSON object here
* instead of the human-readable form. */
static FILE *ndjson_out;
/* Emit a quoted JSON string literal for s[0..n), escaping per RFC 8259. n is an
* explicit length because captured TEXT/BLOB data is not NUL-terminated. */
static void json_str(FILE *f, const char *s, size_t n)
{
fputc('"', f);
for (size_t i = 0; i < n; i++) {
unsigned char c = (unsigned char)s[i];
switch (c) {
case '"': fputs("\\\"", f); break;
case '\\': fputs("\\\\", f); break;
case '\b': fputs("\\b", f); break;
case '\f': fputs("\\f", f); break;
case '\n': fputs("\\n", f); break;
case '\r': fputs("\\r", f); break;
case '\t': fputs("\\t", f); break;
default:
if (c < 0x20)
fprintf(f, "\\u%04x", c);
else
fputc(c, f);
}
}
fputc('"', f);
}
static void json_cstr(FILE *f, const char *s)
{
json_str(f, s, strlen(s));
}
static const char *rc_name(int rc)
{
switch (rc) {
case 101: return "DONE";
case 0: return "OK";
case 1: return "ERROR";
case 5: return "BUSY";
case 9: return "INTERRUPT";
case 7: return "NOMEM";
case 10: return "IOERR";
case 11: return "CORRUPT";
case 21: return "MISUSE";
default: return "?";
}
}
static void fmt_dur(__u64 ns, char *buf, size_t n)
{
if (ns < 1000ULL)
snprintf(buf, n, "%lluns", (unsigned long long)ns);
else if (ns < 1000000ULL)
snprintf(buf, n, "%.1fus", ns / 1000.0);
else if (ns < 1000000000ULL)
snprintf(buf, n, "%.1fms", ns / 1000000.0);
else
snprintf(buf, n, "%.2fs", ns / 1000000000.0);
}
static const char *sql_state_name(__u32 s)
{
switch (s) {
case SQLSTATE_OK: return "ok";
case SQLSTATE_NULLPTR: return "zSql=NULL";
case SQLSTATE_READERR: return "readerr";
case SQLSTATE_FROM_PREPARE: return "from-prepare";
default: return "?";
}
}
struct handler_ctx {
int stackmap_fd;
struct app_cache *apps;
/* Bound values arrive (at the statement's first step) before the sql_event
* that flushes at statement end, so buffer them and print them grouped under
* their statement when the matching stmt_ptr event is handled. */
struct bound_value pending[MAX_VALS_EMIT];
int npending;
__u64 pending_stmt;
};
static void print_app_context(struct handler_ctx *hc, const struct sql_event *e)
{
const struct app_meta *m = app_cache_get(hc->apps, e->tgid, e->comm);
printf(" app: service=%s exe=%s uid=%u gid=%u ns_pid=%u cgroup=0x%llx",
m->service_name[0] ? m->service_name : "?",
m->exe[0] ? m->exe : "?",
e->uid, e->gid, e->ns_pid,
(unsigned long long)e->cgroup_id);
if (m->container_id[0])
printf(" container=%s", m->container_id);
if (m->cmdline[0])
printf(" cmd=\"%s\"", m->cmdline);
printf("\n");
}
/* Print the captured user stack as raw addresses. stack_id < 0 means the kernel
* couldn't capture a stack; trailing zero frames are padding. */
static void print_stack(struct handler_ctx *hc, const struct sql_event *e)
{
if (e->stack_id < 0) {
printf(" stack: (unavailable id=%d)\n", e->stack_id);
return;
}
__u64 frames[MAX_STACK_DEPTH] = { 0 };
__u32 key = (__u32)e->stack_id;
if (bpf_map_lookup_elem(hc->stackmap_fd, &key, frames) != 0) {
printf(" stack: (lookup failed id=%d)\n", e->stack_id);
return;
}
printf(" stack:");
for (int i = 0; i < MAX_STACK_DEPTH && frames[i]; i++)
printf(" 0x%llx", (unsigned long long)frames[i]);
printf("\n");
}
static void print_bound_value(const struct bound_value *bv)
{
switch (bv->type) {
case BOUND_NULL:
printf(" [%u] NULL\n", bv->idx);
break;
case BOUND_INT:
printf(" [%u] INT: %lld\n", bv->idx, (long long)bv->scalar.i);
break;
case BOUND_REAL:
printf(" [%u] REAL: %g\n", bv->idx, bv->scalar.r);
break;
case BOUND_TEXT:
printf(" [%u] TEXT(%u): \"", bv->idx, bv->full_len);
for (__u32 i = 0; i < bv->len; i++) {
unsigned char ch = (unsigned char)bv->data[i];
if (ch == '"' || ch == '\\')
printf("\\%c", ch);
else if (ch >= 0x20 && ch < 0x7f)
putchar(ch);
else
printf("\\x%02x", ch);
}
printf("\"%s\n", bv->truncated ? " ...(truncated)" : "");
break;
case BOUND_BLOB:
printf(" [%u] BLOB(%u): 0x", bv->idx, bv->full_len);
for (__u32 i = 0; i < bv->len; i++)
printf("%02x", (unsigned char)bv->data[i]);
printf("%s\n", bv->truncated ? " ...(truncated)" : "");
break;
default:
printf(" [%u] (unknown type)\n", bv->idx);
break;
}
}
static void flush_bound_values(struct handler_ctx *hc, __u64 stmt_ptr)
{
if (hc->npending == 0 || hc->pending_stmt != stmt_ptr)
return;
printf(" values:\n");
for (int i = 0; i < hc->npending; i++)
print_bound_value(&hc->pending[i]);
hc->npending = 0;
hc->pending_stmt = 0;
}
static const char *bound_type_name(__u32 t)
{
switch (t) {
case BOUND_NULL: return "null";
case BOUND_INT: return "int";
case BOUND_REAL: return "real";
case BOUND_TEXT: return "text";
case BOUND_BLOB: return "blob";
default: return "unknown";
}
}
/* Blobs carry lowercase hex in "hex" since raw bytes aren't valid in a JSON
* string; everything else uses "value". */
static void emit_bound_value_json(FILE *f, const struct bound_value *bv)
{
fprintf(f, "{\"idx\":%u,\"type\":", bv->idx);
json_cstr(f, bound_type_name(bv->type));
switch (bv->type) {
case BOUND_INT:
fprintf(f, ",\"value\":%lld", (long long)bv->scalar.i);
break;
case BOUND_REAL:
fprintf(f, ",\"value\":%.17g", bv->scalar.r);
break;
case BOUND_TEXT:
fputs(",\"value\":", f);
json_str(f, bv->data, bv->len);
break;
case BOUND_BLOB:
fputs(",\"hex\":\"", f);
for (__u32 i = 0; i < bv->len; i++)
fprintf(f, "%02x", (unsigned char)bv->data[i]);
fputc('"', f);
break;
default:
break;
}
fprintf(f, ",\"full_len\":%u,\"truncated\":%s}",
bv->full_len, bv->truncated ? "true" : "false");
}
/* Write the whole event as a single NDJSON object: the machine-readable mirror
* of handle_event's stdout block. */
static void emit_event_json(struct handler_ctx *hc, const struct sql_event *e)
{
FILE *f = ndjson_out;
const struct app_meta *m = app_cache_get(hc->apps, e->tgid, e->comm);
fputc('{', f);
fputs("\"sql\":", f);
json_str(f, e->sql, strnlen(e->sql, MAX_SQL));
fputs(",\"sql_state\":", f);
json_cstr(f, sql_state_name(e->sql_state));
fprintf(f, ",\"pid\":%u,\"tgid\":%u,\"comm\":", e->pid, e->tgid);
json_cstr(f, e->comm);
fputs(",\"db\":", f);
json_cstr(f, e->db_path);
fprintf(f, ",\"rows\":%u,\"rc\":%d,\"rc_name\":", e->rows, e->rc);
json_cstr(f, rc_name(e->rc));
/* "ts" is wall-clock RFC 3339 (omitted if unavailable); "start_boot_ns" is
* the raw CLOCK_BOOTTIME stamp consumers can recompute from. */
char tsbuf[64];
if (fmt_event_time(e->start_boot_ns, tsbuf, sizeof(tsbuf))) {
fputs(",\"ts\":", f);
json_cstr(f, tsbuf);
}
fprintf(f, ",\"start_boot_ns\":%llu", (unsigned long long)e->start_boot_ns);
fprintf(f, ",\"duration_ns\":%llu", (unsigned long long)e->ns);
fprintf(f, ",\"stmt_ptr\":%llu,\"zsql_ptr\":%llu,\"read_ret\":%d,\"btree_mask\":%u",
(unsigned long long)e->stmt_ptr, (unsigned long long)e->zsql_ptr,
e->read_ret, e->btreeMask);
/* truncated=true means bound_bytes/total are lower bounds: the parameter
* scan hit the kernel cap (nvar_scanned < nvar). */
fprintf(f, ",\"in\":{\"sql_bytes\":%u,\"bound_bytes\":%u,\"total_bytes\":%u,"
"\"nvar\":%u,\"nvar_scanned\":%u,\"truncated\":%s}",
e->sql_bytes, e->bound_bytes, e->sql_bytes + e->bound_bytes,
e->nvar, e->nvar_scanned,
(e->nvar_scanned < e->nvar) ? "true" : "false");
fprintf(f, ",\"app\":{\"service\":");
json_cstr(f, m->service_name);
fputs(",\"exe\":", f);
json_cstr(f, m->exe);
fprintf(f, ",\"uid\":%u,\"gid\":%u,\"ns_pid\":%u,\"ns_tgid\":%u,\"cgroup_id\":%llu",
e->uid, e->gid, e->ns_pid, e->ns_tgid,
(unsigned long long)e->cgroup_id);
if (m->container_id[0]) {
fputs(",\"container\":", f);
json_cstr(f, m->container_id);
}
if (m->cmdline[0]) {
fputs(",\"cmdline\":", f);
json_cstr(f, m->cmdline);
}
fputc('}', f);
fprintf(f, ",\"stack_id\":%d", e->stack_id);
/* Bound values, only when they belong to this statement; empty array when
* capture was off. */
if (hc->npending > 0 && hc->pending_stmt == e->stmt_ptr) {
fputs(",\"values\":[", f);
for (int i = 0; i < hc->npending; i++) {
if (i)
fputc(',', f);
emit_bound_value_json(f, &hc->pending[i]);
}
fputc(']', f);
} else {
fputs(",\"values\":[]", f);
}
fputs("}\n", f);
fflush(f);
}
static int handle_event(void *ctx, void *data, size_t len)
{
struct handler_ctx *hc = ctx;
/* Demux the two record types sharing the ringbuf. A bound_value is buffered
* until its statement's event arrives; a new statement's first value
* supersedes any stale buffer. */
const __u32 *kindp = data;
if (len >= sizeof(__u32) && *kindp == REC_BOUND_VALUE) {
const struct bound_value *bv = data;
if (hc->pending_stmt != bv->stmt_ptr) {
hc->npending = 0;
hc->pending_stmt = bv->stmt_ptr;
}
if (hc->npending < MAX_VALS_EMIT)
hc->pending[hc->npending++] = *bv;
return 0;
}
const struct sql_event *e = data;
if (ndjson_out) {
emit_event_json(hc, e);
if (hc->pending_stmt == e->stmt_ptr) {
hc->npending = 0;
hc->pending_stmt = 0;
}
return 0;
}
char dur[16];
fmt_dur(e->ns, dur, sizeof(dur));
const char *db = e->db_path[0] ? e->db_path : "(memory/temp)";
char tsbuf[64], when[72] = "";
if (fmt_event_time(e->start_boot_ns, tsbuf, sizeof(tsbuf)))
snprintf(when, sizeof(when), "%s ", tsbuf);
if (e->sql[0]) {
/* Text recovered from the prepare hook (stmt->zSql was NULL) is marked
* so its provenance is visible. */
const char *src = e->sql_state == SQLSTATE_FROM_PREPARE
? " [from prepare]" : "";
printf("\n%s%s\n", e->sql, src);
printf(" %s%s pid=%u db=%s rows=%u rc=%d(%s) t=%s stmt=0x%llx btreeMask=0x%x\n",
when, e->comm, e->pid, db, e->rows, e->rc, rc_name(e->rc), dur,
(unsigned long long)e->stmt_ptr, e->btreeMask);
} else {
/* No SQL text: show why and the raw pointers so an empty line can be told
* apart (zSql NULL vs. read fault vs. pointer to an empty string). */
printf("\n(no-sql: %s zsql=0x%llx read_ret=%d)\n",
sql_state_name(e->sql_state),
(unsigned long long)e->zsql_ptr, e->read_ret);
printf(" %s%s pid=%u db=%s rows=%u rc=%d(%s) t=%s stmt=0x%llx btreeMask=0x%x\n",
when, e->comm, e->pid, db, e->rows, e->rc, rc_name(e->rc), dur,
(unsigned long long)e->stmt_ptr, e->btreeMask);
}
/* A trailing '+' marks bound/total as undercounts: nvar_scanned < nvar means
* the scan hit the kernel cap and only summed the first nvar_scanned params. */
const char *trunc = (e->nvar_scanned < e->nvar) ? "+" : "";
printf(" in: sql=%uB bound=%uB%s total=%uB%s (vars=%u scanned=%u)\n",
e->sql_bytes, e->bound_bytes, trunc,
e->sql_bytes + e->bound_bytes, trunc, e->nvar, e->nvar_scanned);
flush_bound_values(hc, e->stmt_ptr);
print_app_context(hc, e);
/* Stack logging temporarily disabled. */
(void)print_stack;
return 0;
}
/* Real sizeof(struct Mem == sqlite3_value) from the version-matched SQLite BTF.
* The BPF program strides stmt->aVar[] by this because its truncated CO-RE view
* has the wrong sizeof. Returns 0 on failure, disabling the bound-byte scan. */
static __u32 mem_size_from_btf(const char *sqlite_btf_path)
{
struct btf *btf = btf__parse(sqlite_btf_path, NULL);
if (!btf)
return 0;
__u32 sz = 0;
/* The struct is tagged sqlite3_value; Mem is a typedef of it. */
__s32 id = btf__find_by_name_kind(btf, "sqlite3_value", BTF_KIND_STRUCT);
if (id < 0)
id = btf__find_by_name_kind(btf, "Mem", BTF_KIND_TYPEDEF);
if (id >= 0) {
long n = btf__resolve_size(btf, id);
if (n > 0)
sz = (__u32)n;
}
btf__free(btf);
return sz;
}
static void usage(const char *prog)
{
fprintf(stderr,
"usage: %s --pid PID [options]\n"
" %s --lib PATH [options]\n"
"\n"
"Traces executed SQL by attaching a uprobe on sqlite3_step.\n"
"Auto-detects the SQLite version and selects the matching BTF.\n"
"\n"
" --pid PID trace this process (resolves its libsqlite3.so)\n"
" --lib PATH trace all processes using this libsqlite3.so / binary\n"
" --btf-dir DIR directory of bundled sqlite-<ver>.btf blobs\n"
" (default: $SQLITE_TRACE_BTF_DIR, else the .btf-cache\n"
" next to the binary, else %s)\n"
" --capture-values capture and print the REAL bound parameter values\n"
" (may include passwords/tokens/PII; text & blobs are\n"
" truncated to %d bytes, first %d params per statement)\n"
" --ndjson FILE write findings as NDJSON (one JSON object per line) to\n"
" FILE instead of the human-readable form; \"-\" = stdout.\n"
" FILE is opened for append. Combine with --capture-values\n"
" to include a \"values\" array per record.\n"
" -h, --help this help\n",
prog, prog, DEFAULT_BTF_DIR, MAX_VAL_BYTES, MAX_VALS_EMIT);
}
int main(int argc, char **argv)
{
int pid = -1;
const char *lib_arg = NULL;
int capture_values = 0;
const char *ndjson_arg = NULL;
init_boot_realtime_offset();
const char *btf_dir = getenv("SQLITE_TRACE_BTF_DIR");
if (!btf_dir)
btf_dir = btf_dir_next_to_exe();
if (!btf_dir)
btf_dir = DEFAULT_BTF_DIR;
static struct option opts[] = {
{ "pid", required_argument, 0, 'p' },
{ "lib", required_argument, 0, 'l' },
{ "btf-dir", required_argument, 0, 'b' },
{ "capture-values", no_argument, 0, 'V' },
{ "ndjson", required_argument, 0, 'j' },
{ "help", no_argument, 0, 'h' },
{ 0, 0, 0, 0 },
};
int c;
while ((c = getopt_long(argc, argv, "p:l:b:Vj:h", opts, NULL)) != -1) {
switch (c) {
case 'p': pid = atoi(optarg); break;
case 'l': lib_arg = optarg; break;
case 'b': btf_dir = optarg; break;
case 'V': capture_values = 1; break;
case 'j': ndjson_arg = optarg; break;
case 'h': usage(argv[0]); return 0;
default: usage(argv[0]); return 2;
}
}
/* Open the NDJSON sink before any BPF setup so a bad path fails fast. "-" is
* stdout; a filename is opened for append. */
if (ndjson_arg) {
if (strcmp(ndjson_arg, "-") == 0) {
ndjson_out = stdout;
} else {
ndjson_out = fopen(ndjson_arg, "a");
if (!ndjson_out) {
fprintf(stderr, "cannot open ndjson file %s: %s\n",
ndjson_arg, strerror(errno));
return 1;
}
}
}
if (pid < 0 && !lib_arg) {
usage(argv[0]);
return 2;
}
/* 1. Resolve the SQLite-bearing file: explicit --lib, else from the PID. */
char target_path[PATH_MAX];
if (lib_arg) {
strncpy(target_path, lib_arg, sizeof(target_path) - 1);
target_path[sizeof(target_path) - 1] = '\0';
} else if (resolve_sqlite_file(pid, target_path, sizeof(target_path)) != 0) {
fprintf(stderr, "could not find SQLite in pid %d "
"(no libsqlite3.so mapped and exe unreadable)\n", pid);
return 1;
}
/* 2. Detect the SQLite version and linkage flavor. For a stripped static
* binary the version is resolved from its SOURCE_ID via this manifest. */
char manifest[PATH_MAX];
snprintf(manifest, sizeof(manifest), "%s/sqlite-sources.tsv", btf_dir);
/* Check the btf dir and manifest up front: without them detection loses its
* SOURCE_ID->version fallback and mis-blames the target for a wrong --btf-dir. */
if (access(btf_dir, R_OK | X_OK) != 0) {
fprintf(stderr,
"BTF directory not accessible: %s\n"
"Set --btf-dir or $SQLITE_TRACE_BTF_DIR to the dir holding the\n"
"sqlite-<ver>.btf blobs (run `make btf` to generate them).\n",
btf_dir);
return 1;
}
if (access(manifest, R_OK) != 0)
fprintf(stderr,
"warning: no source-id manifest at %s; version detection for\n"
" stripped/snapshot binaries (no version symbol) will fail.\n",
manifest);
struct sqlite_version ver;
int detected = detect_sqlite_version(target_path, manifest, &ver);
/* A program that dynamically links SQLite (e.g. newsboat) imports the symbols
* as UND, so detection finds no hookable C symbol. Follow its libsqlite3
* DT_NEEDED to the real shared object and re-detect there. Only retarget when
* a libsqlite3 dependency exists, so modernc / "no SQLite" still error out. */
if (detected != 0 || ver.flavor != SQLITE_FLAVOR_C) {
char resolved[PATH_MAX];
struct sqlite_version rver;
if (resolve_dynamic_libsqlite(target_path, resolved, sizeof(resolved)) == 0 &&
detect_sqlite_version(resolved, manifest, &rver) == 0 &&
rver.flavor == SQLITE_FLAVOR_C) {
fprintf(stderr,
"%s dynamically links SQLite; tracing %s instead\n",
target_path, resolved);
strncpy(target_path, resolved, sizeof(target_path) - 1);
target_path[sizeof(target_path) - 1] = '\0';
ver = rver;
detected = 0;
}
}
if (detected != 0) {
fprintf(stderr, "no SQLite found in %s\n", target_path);
return 1;
}
/* The pure-Go transpilation has SQLite's source strings (detection succeeds)
* but no C sqlite3_step to attach a uprobe to. Refuse rather than emit garbage. */
if (ver.flavor == SQLITE_FLAVOR_MODERNC) {
fprintf(stderr,
"%s uses modernc.org/sqlite (pure-Go SQLite). There is no C\n"
"sqlite3_step to attach a uprobe to, so this backend cannot trace it.\n"
"Detected SQLite source: %s\n",
target_path, ver.source_id);
return 1;
}
if (ver.number < 0) {
fprintf(stderr, "could not determine SQLite version in %s "
"(source_id %s not in build DB)\n",
target_path, ver.source_id[0] ? ver.source_id : "unknown");
return 1;
}
/* Approximate match: a prerelease/snapshot SOURCE_ID with no exact release in
* the build DB fell back to the nearest release. The BTF is layout-close but
* not guaranteed identical; warn so a silent CO-RE mis-relocation is visible. */
if (ver.approx_btf)
fprintf(stderr,
"warning: %s embeds a non-release SQLite snapshot (source_id %s);\n"
" no exact BTF, using nearest release %s. Field offsets may\n"
" differ if the snapshot changed a struct after that release.\n",
target_path, ver.source_id, ver.triple);
/* 3. Select the bundled per-version BTF blob. */
char sqlite_btf[PATH_MAX];
snprintf(sqlite_btf, sizeof(sqlite_btf), "%s/sqlite-%d.btf", btf_dir, ver.number);
if (access(sqlite_btf, R_OK) != 0) {
fprintf(stderr,
"no bundled BTF for SQLite %s (%d): %s not found.\n"
"This version isn't supported by this build.\n",
ver.triple, ver.number, sqlite_btf);
return 1;
}
/* 4. Merge with THIS host's kernel BTF (satisfies pt_regs + Vdbe relocs). */
char merged_btf[PATH_MAX];
if (merge_btf_with_kernel(sqlite_btf, merged_btf, sizeof(merged_btf)) != 0) {
fprintf(stderr, "failed to build merged BTF\n");
return 1;
}
struct sqlite_trace_bpf *skel;
struct ring_buffer *rb = NULL;
struct bpf_link *link = NULL;
struct bpf_link *ret_link = NULL;
struct bpf_link *prep_link = NULL;
struct bpf_link *prep_v3_link = NULL;
struct bpf_link *prep_ret_link = NULL;
struct bpf_link *prep_v3_ret_link = NULL;
struct handler_ctx hc = { .stackmap_fd = -1, .apps = NULL };
int err;
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
LIBBPF_OPTS(bpf_object_open_opts, open_opts,
.btf_custom_path = merged_btf);
skel = sqlite_trace_bpf__open_opts(&open_opts);
if (!skel) {
fprintf(stderr, "open skeleton failed\n");
err = 1;
goto cleanup;
}
/* Real sizeof(Mem) for striding stmt->aVar[]. Must be set after open and
* before load (rodata is frozen into a read-only map at load). */
skel->rodata->mem_stride = mem_size_from_btf(sqlite_btf);
if (skel->rodata->mem_stride == 0)
fprintf(stderr, "warning: could not read sizeof(Mem) from %s; "
"bound-parameter sizes will be unavailable\n", sqlite_btf);
skel->rodata->capture_values = capture_values ? 1 : 0;
if (capture_values)
fprintf(stderr, "capturing bound parameter VALUES (may include "
"secrets/PII); text/blob truncated to %d bytes, first %d "
"params per statement.\n", MAX_VAL_BYTES, MAX_VALS_EMIT);
err = sqlite_trace_bpf__load(skel);
if (err) {
fprintf(stderr, "BPF load failed: %d\n", err);
goto cleanup;
}
/* Locate sqlite3_step by symbol name, or by file offset for stripped static
* embedders (fossil, language runtimes) where no symbol exists. A step_offset
* from the build DB means attach by offset with no func_name. */
const char *step_name = ver.step_offset ? NULL : "sqlite3_step";
unsigned long step_off = ver.step_offset; /* 0 => libbpf uses func_name */
if (ver.step_offset)
fprintf(stderr, "no sqlite3_step symbol; attaching by offset 0x%lx\n",
ver.step_offset);
/* LINK attach mode registers an inode-wide uprobe that also fires for
* processes started AFTER attach; the default PERF mode does not. */
LIBBPF_OPTS(bpf_uprobe_opts, uopts,
.func_name = step_name,
.retprobe = false,
.attach_mode = PROBE_ATTACH_MODE_LINK);
link = bpf_program__attach_uprobe_opts(
skel->progs.handle_step, pid, target_path, step_off, &uopts);
if (!link) {
fprintf(stderr, "attach uprobe failed: %d (%s)\n",
-errno, strerror(errno));
err = 1;
goto cleanup;
}
/* Return probe on the same function: captures the step return code to count
* rows and flush one event per execution. */
LIBBPF_OPTS(bpf_uprobe_opts, ret_uopts,
.func_name = step_name,
.retprobe = true,
.attach_mode = PROBE_ATTACH_MODE_LINK);
ret_link = bpf_program__attach_uprobe_opts(
skel->progs.handle_step_ret, pid, target_path, step_off, &ret_uopts);
if (!ret_link) {
fprintf(stderr, "attach uretprobe failed: %d (%s)\n",
-errno, strerror(errno));
err = 1;
goto cleanup;
}
/* Optional prepare hook: captures SQL text at prepare time, keyed by the
* returned stmt pointer, so step events whose stmt->zSql is NULL can still
* report their SQL. Both v2 and v3 entry points are hooked (modern callers,
* incl. the CLI, use v3). By symbol name only, so stripped static embedders
* have no prepare symbol; best-effort, failures are logged and skipped. */
LIBBPF_OPTS(bpf_uprobe_opts, prep_uopts,
.func_name = "sqlite3_prepare_v2",
.retprobe = false,
.attach_mode = PROBE_ATTACH_MODE_LINK);
prep_link = bpf_program__attach_uprobe_opts(
skel->progs.handle_prepare, pid, target_path, 0, &prep_uopts);
LIBBPF_OPTS(bpf_uprobe_opts, prep_v3_uopts,
.func_name = "sqlite3_prepare_v3",
.retprobe = false,
.attach_mode = PROBE_ATTACH_MODE_LINK);
prep_v3_link = bpf_program__attach_uprobe_opts(
skel->progs.handle_prepare_v3, pid, target_path, 0, &prep_v3_uopts);
/* uretprobes are per-function, so v2 and v3 each need their own return probe,
* attached only where the entry point resolved. */
LIBBPF_OPTS(bpf_uprobe_opts, prep_ret_uopts,
.func_name = "sqlite3_prepare_v2",
.retprobe = true,
.attach_mode = PROBE_ATTACH_MODE_LINK);
if (prep_link)
prep_ret_link = bpf_program__attach_uprobe_opts(
skel->progs.handle_prepare_ret, pid, target_path, 0,
&prep_ret_uopts);
LIBBPF_OPTS(bpf_uprobe_opts, prep_v3_ret_uopts,
.func_name = "sqlite3_prepare_v3",
.retprobe = true,
.attach_mode = PROBE_ATTACH_MODE_LINK);
if (prep_v3_link)
prep_v3_ret_link = bpf_program__attach_uprobe_opts(
skel->progs.handle_prepare_ret, pid, target_path, 0,
&prep_v3_ret_uopts);
if (!prep_link && !prep_v3_link) {
fprintf(stderr,
"note: no sqlite3_prepare_v2/v3 symbol to hook; SQL for statements "
"with a NULL zSql won't be recovered (stripped static build).\n");
}
hc.stackmap_fd = bpf_map__fd(skel->maps.stackmap);
hc.apps = app_cache_new();
if (!hc.apps) {
fprintf(stderr, "app cache alloc failed\n");
err = 1;
goto cleanup;
}
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, &hc, NULL);
if (!rb) {
fprintf(stderr, "ring buffer create failed\n");
err = 1;
goto cleanup;
}
fprintf(stderr, "tracing SQLite %s in %s. Ctrl-C to stop.\n",
ver.triple, target_path);
while (!exiting) {
err = ring_buffer__poll(rb, 100);
if (err == -EINTR) { err = 0; break; }
if (err < 0) break;
}
cleanup:
app_cache_free(hc.apps);
ring_buffer__free(rb);
bpf_link__destroy(prep_v3_ret_link);
bpf_link__destroy(prep_ret_link);
bpf_link__destroy(prep_v3_link);
bpf_link__destroy(prep_link);
bpf_link__destroy(ret_link);
bpf_link__destroy(link);
if (skel)
sqlite_trace_bpf__destroy(skel);
if (ndjson_out && ndjson_out != stdout)
fclose(ndjson_out);
unlink(merged_btf);
return err < 0 ? -err : err;
}