Skip to content

Commit a8406bd

Browse files
testdLo999
andcommitted
fix: implement overflow pages in sqlite_writer to prevent SIGBUS on large records
When a single record payload exceeds 65KB (max_local=65501), the writer now splits it across overflow pages per SQLite's B-tree spec instead of underflowing the content_offset and corrupting memory. Closes #139, closes #187. Based on #175 by dLo999. Co-Authored-By: Dustin Obrecht <dustin@kurtnoble.com>
1 parent 404b5f8 commit a8406bd

2 files changed

Lines changed: 169 additions & 1 deletion

File tree

internal/cbm/sqlite_writer.c

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,77 @@ static uint8_t *build_table_cell(int64_t rowid, const uint8_t *payload, int payl
823823
return cell;
824824
}
825825

826+
// Build a table leaf cell with overflow: stores only the first local_len bytes of
827+
// payload inline, followed by a 4-byte overflow page number.
828+
// total_payload_len is the FULL original payload length (written as the payload-size
829+
// varint so SQLite knows the real record size).
830+
static uint8_t *build_table_cell_overflow(int64_t rowid, const uint8_t *payload,
831+
int total_payload_len, int local_len,
832+
uint32_t overflow_page, int *out_cell_len) {
833+
int rl = varint_len(total_payload_len);
834+
int kl = varint_len(rowid);
835+
// cell = varint(total_payload_len) + varint(rowid) + payload[0..local_len) + uint32(overflow)
836+
int total = rl + kl + local_len + BTREE_PTR_SIZE;
837+
uint8_t *cell = (uint8_t *)malloc(total);
838+
if (!cell) {
839+
return NULL;
840+
}
841+
int pos = 0;
842+
pos += put_varint(cell + pos, total_payload_len);
843+
pos += put_varint(cell + pos, rowid);
844+
memcpy(cell + pos, payload, local_len);
845+
pos += local_len;
846+
put_u32(cell + pos, overflow_page);
847+
pos += BTREE_PTR_SIZE;
848+
*out_cell_len = pos;
849+
return cell;
850+
}
851+
852+
// --- Overflow page writer ---
853+
// Writes overflow pages for payload bytes that exceed local storage.
854+
// Returns the first overflow page number (embedded in the leaf cell).
855+
// Each overflow page: 4-byte next-page pointer + up to (CBM_PAGE_SIZE-4) bytes of data.
856+
static uint32_t write_overflow_pages(FILE *fp, uint32_t *next_page, const uint8_t *data,
857+
int data_len) {
858+
int per_page = CBM_PAGE_SIZE - BTREE_PTR_SIZE;
859+
uint32_t first_page = 0;
860+
long prev_next_ptr_offset = -SKIP_ONE;
861+
862+
int offset = 0;
863+
while (offset < data_len) {
864+
uint32_t pnum = (*next_page)++;
865+
if (first_page == 0) {
866+
first_page = pnum;
867+
}
868+
869+
// Backpatch previous overflow page's next-page pointer
870+
if (prev_next_ptr_offset >= 0) {
871+
uint8_t ptr[BTREE_PTR_SIZE];
872+
put_u32(ptr, pnum);
873+
(void)fseek(fp, prev_next_ptr_offset, SEEK_SET);
874+
(void)fwrite(ptr, SKIP_ONE, BTREE_PTR_SIZE, fp);
875+
}
876+
877+
int chunk = data_len - offset;
878+
if (chunk > per_page) {
879+
chunk = per_page;
880+
}
881+
882+
uint8_t page[CBM_PAGE_SIZE];
883+
memset(page, 0, CBM_PAGE_SIZE);
884+
put_u32(page, 0); // next-page pointer — 0 for now, backpatched on next iteration
885+
memcpy(page + BTREE_PTR_SIZE, data + offset, chunk);
886+
887+
long page_offset = (long)(pnum - SKIP_ONE) * CBM_PAGE_SIZE;
888+
prev_next_ptr_offset = page_offset;
889+
(void)fseek(fp, page_offset, SEEK_SET);
890+
(void)fwrite(page, SKIP_ONE, CBM_PAGE_SIZE, fp);
891+
892+
offset += chunk;
893+
}
894+
return first_page;
895+
}
896+
826897
// --- Index record builders ---
827898

828899
// Build an index entry for a 2-column TEXT index (project, col) + rowid.
@@ -975,11 +1046,43 @@ static bool pb_ensure_leaf_cap(PageBuilder *pb) {
9751046
return true;
9761047
}
9771048

1049+
// SQLite overflow thresholds for leaf table B-tree pages (PAGE_SIZE=65536, reserved=0):
1050+
// usable = PAGE_SIZE = 65536
1051+
// max_local = usable - 35 = 65501
1052+
// min_local = (usable - 12) * 32 / 255 - 23 = 8199 (C integer arithmetic, same as SQLite)
1053+
#define TABLE_OVERFLOW_MAX_LOCAL 65501
1054+
#define TABLE_OVERFLOW_MIN_LOCAL 8199
1055+
9781056
// Add a table cell to the PageBuilder, flushing leaf pages as needed.
1057+
// If the payload exceeds max_local, overflow pages are written and only the
1058+
// local portion plus a 4-byte overflow page pointer is stored in the leaf cell.
9791059
static void pb_add_table_cell_with_flush(PageBuilder *pb, int64_t rowid, const uint8_t *payload,
9801060
int payload_len, int64_t prev_rowid) {
9811061
int cell_len = 0;
982-
uint8_t *cell = build_table_cell(rowid, payload, payload_len, &cell_len);
1062+
uint8_t *cell = NULL;
1063+
1064+
if (payload_len > TABLE_OVERFLOW_MAX_LOCAL) {
1065+
// Compute local_len per SQLite spec for leaf table cells.
1066+
int ovfl_page_data = CBM_PAGE_SIZE - BTREE_PTR_SIZE;
1067+
int remainder = (payload_len - TABLE_OVERFLOW_MIN_LOCAL) % ovfl_page_data;
1068+
int local_len = TABLE_OVERFLOW_MIN_LOCAL + remainder;
1069+
if (local_len > TABLE_OVERFLOW_MAX_LOCAL) {
1070+
local_len = TABLE_OVERFLOW_MIN_LOCAL;
1071+
}
1072+
1073+
// Write overflow pages for the bytes that don't fit locally.
1074+
uint32_t overflow_page = write_overflow_pages(pb->fp, &pb->next_page, payload + local_len,
1075+
payload_len - local_len);
1076+
if (overflow_page == 0) {
1077+
return; // overflow write failed
1078+
}
1079+
1080+
cell = build_table_cell_overflow(rowid, payload, payload_len, local_len, overflow_page,
1081+
&cell_len);
1082+
} else {
1083+
cell = build_table_cell(rowid, payload, payload_len, &cell_len);
1084+
}
1085+
9831086
if (!cell) {
9841087
return;
9851088
}

tests/test_sqlite_writer.c

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,11 +374,76 @@ TEST(sw_multi_page) {
374374
PASS();
375375
}
376376

377+
/* ── Oversized node: properties JSON > 65KB triggers overflow pages ─ */
378+
379+
TEST(sw_oversized_node) {
380+
char path[256];
381+
ASSERT_EQ(make_temp_db(path, sizeof(path)), 0);
382+
383+
/* Build a properties JSON string that exceeds max_local (65501 bytes).
384+
* Use 70000 bytes of padding inside the JSON value so the full record,
385+
* which includes other text columns, is well above the threshold. */
386+
int prop_len = 70000;
387+
char *big_props = (char *)malloc(prop_len + 1);
388+
ASSERT_NOT_NULL(big_props);
389+
memset(big_props, 'x', prop_len);
390+
big_props[0] = '"';
391+
big_props[prop_len - 1] = '"';
392+
big_props[prop_len] = '\0';
393+
394+
CBMDumpNode nodes[1] = {{
395+
.id = 1,
396+
.project = "test",
397+
.label = "Function",
398+
.name = "huge_fn",
399+
.qualified_name = "test.huge_fn",
400+
.file_path = "huge.go",
401+
.start_line = 1,
402+
.end_line = 9999,
403+
.properties = big_props,
404+
}};
405+
406+
int rc = cbm_write_db(path, "test", "/tmp/test", "2026-03-28T00:00:00Z", nodes, 1, NULL, 0,
407+
NULL, 0, NULL, 0);
408+
free(big_props);
409+
ASSERT_EQ(rc, 0);
410+
411+
sqlite3 *db = NULL;
412+
rc = sqlite3_open(path, &db);
413+
ASSERT_EQ(rc, SQLITE_OK);
414+
415+
/* Integrity check — SQLite will validate overflow page chain */
416+
sqlite3_stmt *stmt = NULL;
417+
sqlite3_prepare_v2(db, "PRAGMA integrity_check", -1, &stmt, NULL);
418+
rc = sqlite3_step(stmt);
419+
ASSERT_EQ(rc, SQLITE_ROW);
420+
ASSERT_STR_EQ((const char *)sqlite3_column_text(stmt, 0), "ok");
421+
sqlite3_finalize(stmt);
422+
423+
/* Verify we can read the node back */
424+
sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM nodes", -1, &stmt, NULL);
425+
sqlite3_step(stmt);
426+
ASSERT_EQ(sqlite3_column_int(stmt, 0), 1);
427+
sqlite3_finalize(stmt);
428+
429+
/* Verify the name round-trips correctly */
430+
sqlite3_prepare_v2(db, "SELECT name FROM nodes WHERE id=1", -1, &stmt, NULL);
431+
rc = sqlite3_step(stmt);
432+
ASSERT_EQ(rc, SQLITE_ROW);
433+
ASSERT_STR_EQ((const char *)sqlite3_column_text(stmt, 0), "huge_fn");
434+
sqlite3_finalize(stmt);
435+
436+
sqlite3_close(db);
437+
unlink(path);
438+
PASS();
439+
}
440+
377441
/* ── Suite ─────────────────────────────────────────────────────── */
378442

379443
SUITE(sqlite_writer) {
380444
RUN_TEST(sw_minimal_data);
381445
RUN_TEST(sw_scale_and_indexes);
382446
RUN_TEST(sw_empty);
383447
RUN_TEST(sw_multi_page);
448+
RUN_TEST(sw_oversized_node);
384449
}

0 commit comments

Comments
 (0)