From ddf570c791cbd43a27932d74f0f4ff3ea85feae8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 4 Nov 2026 01:05:04 +0100 Subject: [PATCH] Keep caller transactions intact on invalid receive commit and guard SQLite blob reads --- src/chat/member_sync.c | 10 ++++++++-- tests/test_member_versions.c | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index 4c50e260e..f80469ba2 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -1236,7 +1236,8 @@ static int member_read_saved(sqlite3* db, const char* ns, uint64_t node, int sta uint8_t* blobs[] = {out->x, out->ed, out->join, out->update, out->owner, out->tree}; for (size_t i = 0; i < 6; i++) { int bytes = sqlite3_column_bytes(st, cols[i]); - if (bytes == sizes[i]) memcpy(blobs[i], sqlite3_column_blob(st, cols[i]), (size_t)bytes); + const void* blob = sqlite3_column_blob(st, cols[i]); + if (bytes == sizes[i] && blob) memcpy(blobs[i], blob, (size_t)bytes); else if (bytes || i < 2) out->malformed = 1; } for (int i = 0; i < 2; i++) { @@ -1250,6 +1251,7 @@ static int member_read_saved(sqlite3* db, const char* ns, uint64_t node, int sta .userinfo=out->info,.adm_tags=out->tags,.adm_tags_sig=out->owner, .signed_by=(uint64_t)sqlite3_column_int64(st,9),.signature=out->tree}; out->source = sqlite3_column_int(st, 11); + if (sqlite3_errcode(db) == SQLITE_NOMEM) rc = SQLITE_NOMEM; } int final = sqlite3_finalize(st); if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) { @@ -1403,7 +1405,11 @@ static int member_commit_pull(void* ctx, void* state) { struct UTUN_INSTANCE* inst = ctx; struct member_pull* pull = state; sqlite3* db = _db(inst); char sql[2048]; sqlite3_stmt* st = NULL; const char* stage = "begin"; uint64_t* incoming = NULL; size_t count = 0; int result = MT_ERR_IO, rc; - if (!sqlite3_get_autocommit(db) || sqlite3_exec(db, "BEGIN IMMEDIATE /* member receive */", NULL, NULL, NULL) != SQLITE_OK) goto fail; + if (!sqlite3_get_autocommit(db)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "member receive commit requires own transaction ch=%s sync=%lld", + pull->channel, (long long)pull->id); return MT_ERR_IO; + } + if (sqlite3_exec(db, "BEGIN IMMEDIATE /* member receive */", NULL, NULL, NULL) != SQLITE_OK) goto fail; stage = "policy"; snprintf(sql, sizeof(sql), "SELECT sync_policy FROM peers_%s WHERE id=%lld AND record_state=1", pull->channel, (long long)pull->id); rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); diff --git a/tests/test_member_versions.c b/tests/test_member_versions.c index 4c8a89b2b..138704f8c 100644 --- a/tests/test_member_versions.c +++ b/tests/test_member_versions.c @@ -104,6 +104,9 @@ int main(void) { DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "PASS two peers: isolated staging, no early profile/hash change, stale completion preserves newer commit"); profile(&source, 2, 4); a = receive(&source, &target, 2, identities[1].id); b = receive(&source, &target, 2, identities[3].id); + assert(sqlite3_exec(target.topo_sqlite_db, "BEGIN", NULL, NULL, NULL) == SQLITE_OK); + assert(member_commit_pull(&target, a) == MT_ERR_IO && !sqlite3_get_autocommit(target.topo_sqlite_db)); + assert(sqlite3_exec(target.topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); member_abort_pull(&target, a); assert(scalar(&target, "SELECT COUNT(*) FROM peers_1001 WHERE record_state=1") == 2); finish_receive(&target, b); assert(version(&target) == 4);