Exercise committed decryption claims and retryable SQL completions

This commit is contained in:
evgeny committed 2026-10-27 01:37:30 +01:00
1 parent b71c3040d4
commit d7ffa1031f
2 files changed
+143 -11

No files matched your search

+1 -1
View File
@@ -489,7 +489,7 @@ test_chat_attachment_SOURCES = test_chat_attachment.c
test_chat_attachment_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_chat_attachment_LDFLAGS = -Wl,--wrap=sqlite3_exec -Wl,--wrap=sqlite3_prepare_v2 -Wl,--wrap=sqlite3_bind_blob \
-Wl,--wrap=sqlite3_bind_text -Wl,--wrap=sqlite3_bind_int64 -Wl,--wrap=sqlite3_step -Wl,--wrap=sqlite3_finalize \
-Wl,--wrap=media_async_submit
-Wl,--wrap=media_async_submit -Wl,--wrap=sqlite3_changes -Wl,--wrap=uasync_set_timeout
endif
test_attachment_send_SOURCES = test_attachment_send.c
test_attachment_send_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
+142 -10
View File
@@ -3,9 +3,11 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "../src/utun_instance.h"
#include "../src/chat/chat_core.h"
#include "../src/chat/chat_attachment.h"
#include "../src/chat/chat_core_priv.h"
#include "../src/chat/chat_event.h"
#include "../src/media_async/media_async.h"
#include "../lib/platform_compat.h"
@@ -13,11 +15,14 @@
#include "../lib/mem.h"
enum fault { NONE, BEGIN, PREPARE, BIND_1, BIND_2, BIND_3, BIND_4, BIND_5, BIND_6, BIND_7, BIND_8,
STEP, FINALIZE, COMMIT, FAULT_COUNT };
STEP, FINALIZE, COMMIT, FAULT_COUNT, CHANGES };
static enum fault fault;
static unsigned hits[FAULT_COUNT], rollback_fault;
static int fail_rollback, fail_submit;
static const char* insert_prefix = "INSERT INTO attachment_keys(channel,crypto_id,author,key,";
static const char* fault_prefix;
static int target_written;
static timeout_callback_t attachment_tick;
static const char content[] = "attachment source must survive SQL failure";
struct fixture {
@@ -26,14 +31,14 @@ struct fixture {
sqlite3* reader;
char directory[256], source[512], channel[32];
uint8_t id[16], key[32];
unsigned rows, events, submissions;
int success, inspect_submit;
unsigned rows, events, submissions, decrypt_submissions, key_events;
int success, inspect_submit, decrypt_mode, allow_worker, result_error;
};
static struct fixture* active;
static int take(enum fault target) {
if (fault != target) return 0;
fault = NONE; hits[target]++;
fault = NONE; if (target < FAULT_COUNT) hits[target]++;
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment test: injected stage=%d", target); return 1;
}
@@ -41,6 +46,17 @@ static int is_preparation(sqlite3_stmt* st) {
return st && !strncmp(sqlite3_sql(st), insert_prefix, strlen(insert_prefix));
}
static int is_target(sqlite3_stmt* st) {
const char* prefix = fault_prefix ? fault_prefix : insert_prefix;
return st && !strncmp(sqlite3_sql(st), prefix, strlen(prefix));
}
extern void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*);
void* __wrap_uasync_set_timeout(struct UASYNC* ua, int delay, void* arg, timeout_callback_t callback, const char* name) {
if (name && !strcmp(name, "attachment keys")) attachment_tick = callback;
return __real_uasync_set_timeout(ua, delay, arg, callback, name);
}
extern int __real_sqlite3_exec(sqlite3*, const char*, int (*)(void*, int, char**, char**), void*, char**);
int __wrap_sqlite3_exec(sqlite3* db, const char* sql, int (*cb)(void*, int, char**, char**), void* arg, char** error) {
if (!strcmp(sql, "BEGIN IMMEDIATE") && take(BEGIN)) return SQLITE_BUSY;
@@ -54,7 +70,8 @@ int __wrap_sqlite3_exec(sqlite3* db, const char* sql, int (*cb)(void*, int, char
extern int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**);
int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int length, sqlite3_stmt** st, const char** tail) {
if (!strncmp(sql, insert_prefix, strlen(insert_prefix)) && take(PREPARE)) { *st = NULL; return SQLITE_NOMEM; }
const char* prefix = fault_prefix ? fault_prefix : insert_prefix;
if (!strncmp(sql, prefix, strlen(prefix)) && take(PREPARE)) { *st = NULL; return SQLITE_NOMEM; }
return __real_sqlite3_prepare_v2(db, sql, length, st, tail);
}
@@ -63,35 +80,42 @@ int __wrap_sqlite3_bind_blob(sqlite3_stmt* st, int index, const void* data, int
if (is_preparation(st)) {
if (index == 2) { assert(length == 16); memcpy(active->id, data, 16); }
if (index == 4) { assert(length == 32); memcpy(active->key, data, 32); }
if (take((enum fault)(BIND_1 + index - 1))) return SQLITE_NOMEM;
}
if (is_target(st) && take((enum fault)(BIND_1 + index - 1))) return SQLITE_NOMEM;
return __real_sqlite3_bind_blob(st, index, data, length, destroy);
}
extern int __real_sqlite3_bind_text(sqlite3_stmt*, int, const char*, int, void (*)(void*));
int __wrap_sqlite3_bind_text(sqlite3_stmt* st, int index, const char* data, int length, void (*destroy)(void*)) {
if (is_preparation(st) && take((enum fault)(BIND_1 + index - 1))) return SQLITE_NOMEM;
if (is_target(st) && take((enum fault)(BIND_1 + index - 1))) return SQLITE_NOMEM;
return __real_sqlite3_bind_text(st, index, data, length, destroy);
}
extern int __real_sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);
int __wrap_sqlite3_bind_int64(sqlite3_stmt* st, int index, sqlite3_int64 value) {
if (is_preparation(st) && take((enum fault)(BIND_1 + index - 1))) return SQLITE_NOMEM;
if (is_target(st) && take((enum fault)(BIND_1 + index - 1))) return SQLITE_NOMEM;
return __real_sqlite3_bind_int64(st, index, value);
}
extern int __real_sqlite3_step(sqlite3_stmt*);
int __wrap_sqlite3_step(sqlite3_stmt* st) {
if (is_preparation(st) && take(STEP)) return SQLITE_IOERR;
target_written = is_target(st);
if (target_written && take(STEP)) return SQLITE_IOERR;
return __real_sqlite3_step(st);
}
extern int __real_sqlite3_finalize(sqlite3_stmt*);
int __wrap_sqlite3_finalize(sqlite3_stmt* st) {
int failed = is_preparation(st) && take(FINALIZE);
int failed = is_target(st) && take(FINALIZE);
int rc = __real_sqlite3_finalize(st); return failed ? SQLITE_IOERR : rc;
}
extern int __real_sqlite3_changes(sqlite3*);
int __wrap_sqlite3_changes(sqlite3* db) {
if (target_written && take(CHANGES)) return 0;
return __real_sqlite3_changes(db);
}
static void sql(sqlite3* db, const char* text) {
int rc = sqlite3_exec(db, text, NULL, NULL, NULL);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment test SQL rc=%d: %s", rc, sqlite3_errmsg(db));
@@ -130,6 +154,8 @@ static void committed(struct fixture* f, int published) {
assert(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK);
}
static void decryption_state(struct fixture* f, int ready, int decrypting, int error);
extern void __real_media_async_submit(struct media_async*, struct UASYNC*, ma_work_fn, void*, ma_done_fn, void*);
void __wrap_media_async_submit(struct media_async* ma, struct UASYNC* ua, ma_work_fn work, void* data, ma_done_fn done, void* arg) {
if (active && active->inspect_submit) {
@@ -137,10 +163,18 @@ void __wrap_media_async_submit(struct media_async* ma, struct UASYNC* ua, ma_wor
committed(active, 0);
if (fail_submit) { fail_submit = 0; done(arg, -1); return; }
}
if (active && active->decrypt_mode) {
assert(active->allow_worker && sqlite3_get_autocommit(active->inst->topo_sqlite_db));
decryption_state(active, 0, 1, 0); active->decrypt_submissions++;
}
__real_media_async_submit(ma, ua, work, data, done, arg);
}
static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int length) {
if (type == CHAT_EVT_ATTACHMENT_KEYS_CHANGED && active && active->decrypt_mode) {
assert(inst == active->inst && sqlite3_get_autocommit(inst->topo_sqlite_db));
decryption_state(active, !active->result_error, 0, active->result_error); active->key_events++; return;
}
if (type != CHAT_EVT_MSG_SUBMITTED) return;
assert(active && active->inst == inst && length == 9);
active->events++; active->success = data[8];
@@ -275,12 +309,110 @@ static void test_rollback_failure(void) {
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS attachment: failed rollback requests owner stop, no worker/source transfer, close rolls back uncommitted row");
}
static void decryption_state(struct fixture* f, int ready, int decrypting, int error) {
sqlite3_stmt* st = NULL;
assert(sqlite3_prepare_v2(f->reader, "SELECT key,info,content_type,ready,decrypting,error FROM attachment_keys"
" WHERE channel=? AND crypto_id=?", -1, &st, NULL) == SQLITE_OK);
assert(sqlite3_bind_text(st, 1, f->channel, -1, SQLITE_STATIC) == SQLITE_OK);
assert(sqlite3_bind_blob(st, 2, f->id, 16, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
assert(sqlite3_column_bytes(st, 0) == 32 && !memcmp(sqlite3_column_blob(st, 0), f->key, 32));
struct attachment_info info = {.kind = ATTACHMENT_FILE, .name = "source.bin"};
uint8_t wire[ATTACHMENT_WIRE_MAX]; int bytes = attachment_encode(&info, wire, sizeof(wire));
assert(bytes > 0 && sqlite3_column_bytes(st, 1) == bytes && !memcmp(sqlite3_column_blob(st, 1), wire, (size_t)bytes));
assert(!strcmp((const char*)sqlite3_column_text(st, 2), "application/octet-stream"));
int actual_ready = sqlite3_column_int(st, 3), actual_decrypting = sqlite3_column_int(st, 4), actual_error = sqlite3_column_int(st, 5);
if (actual_ready != ready || actual_decrypting != decrypting || actual_error != error)
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "decryption test: reader ready/decrypting/error=%d/%d/%d expected=%d/%d/%d",
actual_ready, actual_decrypting, actual_error, ready, decrypting, error);
assert(actual_ready == ready && actual_decrypting == decrypting && actual_error == error);
assert(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK);
}
static void plaintext_path(struct fixture* f, char path[512]) {
char hex[33];
for (unsigned i = 0; i < 16; i++) snprintf(hex + i * 2, 3, "%02x", f->id[i]);
snprintf(path, 512, "%s/decrypted/%s/%s.file", f->directory, f->channel, hex);
}
static void decryption_reset(struct fixture* f) {
assert(!CC(f->inst)->attachment_file_count && !CC(f->inst)->attachment_files);
char path[512], query[256]; plaintext_path(f, path);
assert(!remove(path) || errno == ENOENT);
snprintf(query, sizeof(query), "UPDATE attachment_keys SET ready=0,decrypting=0,error=0,wanted=1,downloaded=1 WHERE channel='%s'", f->channel);
sql(f->inst->topo_sqlite_db, query);
f->decrypt_mode = 1; f->allow_worker = 0; f->key_events = f->decrypt_submissions = 0; f->result_error = 0;
decryption_state(f, 0, 0, 0);
}
static void tick(struct fixture* f) {
assert(attachment_tick && CC(f->inst)->attachment_timer);
uasync_cancel_timeout(f->ua, CC(f->inst)->attachment_timer); attachment_tick(f->inst);
}
static void decryption_done(struct fixture* f) {
for (unsigned poll = 0; poll < 2000 && !f->key_events; poll++) uasync_poll(f->ua, 10);
assert(f->key_events == 1 && f->decrypt_submissions == 1 && !CC(f->inst)->attachment_file_count && !CC(f->inst)->attachment_files);
decryption_state(f, !f->result_error, 0, f->result_error);
if (!f->result_error) {
char path[512], buffer[sizeof(content)]; plaintext_path(f, path); FILE* file = fopen(path, "rb"); assert(file);
assert(fread(buffer, 1, sizeof(buffer), file) == sizeof(buffer) && !memcmp(buffer, content, sizeof(buffer)));
assert(fgetc(file) == EOF && !fclose(file));
}
}
static void test_decryption_claim(void) {
struct fixture f; setup(&f); source(&f); publish(&f);
const enum fault points[] = {BEGIN, PREPARE, BIND_1, BIND_2, BIND_3, STEP, FINALIZE, CHANGES, COMMIT};
fault_prefix = "UPDATE attachment_keys SET decrypting=1 WHERE";
for (unsigned i = 0; i < sizeof(points) / sizeof(points[0]); i++) {
decryption_reset(&f); fault = points[i]; tick(&f);
assert(fault == NONE && !f.decrypt_submissions && !f.key_events && !CC(f.inst)->attachment_file_count);
decryption_state(&f, 0, 0, 0); f.allow_worker = 1; tick(&f); decryption_done(&f);
}
const char* triggers[] = {
"CREATE TEMP TRIGGER reject_claim BEFORE UPDATE ON attachment_keys WHEN NEW.decrypting=1 BEGIN SELECT RAISE(IGNORE); END",
"CREATE TEMP TRIGGER reject_claim BEFORE UPDATE ON attachment_keys WHEN NEW.decrypting=1 BEGIN SELECT RAISE(ABORT,'claim'); END"
};
for (unsigned i = 0; i < 2; i++) {
decryption_reset(&f); sql(f.inst->topo_sqlite_db, triggers[i]); tick(&f);
assert(!f.decrypt_submissions && !f.key_events); decryption_state(&f, 0, 0, 0);
sql(f.inst->topo_sqlite_db, "DROP TRIGGER reject_claim"); f.allow_worker = 1; tick(&f); decryption_done(&f);
}
fault_prefix = NULL; cleanup(&f);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS decryption claim:9 SQL faults, ABORT/IGNORE, no worker before committed single-row claim; real decrypt on retry");
}
static void test_decryption_completion(void) {
struct fixture f; setup(&f); source(&f); publish(&f); unsigned cases = 0;
const char* prefixes[] = {"SELECT 1 FROM attachment_keys WHERE", "UPDATE attachment_keys SET info="};
for (unsigned op = 0; op < 2; op++) {
int binds = op ? 4 : 2;
for (int i = 0; i < 3 + binds + (op ? 3 : 0); i++) {
decryption_reset(&f); f.allow_worker = 1; tick(&f); assert(f.decrypt_submissions == 1);
fault_prefix = prefixes[op];
fault = i == 0 ? PREPARE : i <= binds ? (enum fault)(BIND_1 + i - 1) : i == binds + 1 ? STEP :
i == binds + 2 ? FINALIZE : i == binds + 3 ? CHANGES : i == binds + 4 ? BEGIN : COMMIT;
for (unsigned poll = 0; poll < 2000 && fault != NONE; poll++) uasync_poll(f.ua, 10);
assert(fault == NONE && !f.key_events && CC(f.inst)->attachment_file_count == 1 && CC(f.inst)->attachment_files);
decryption_state(&f, 0, 1, 0); tick(&f); decryption_done(&f); cases++;
}
}
decryption_reset(&f); f.allow_worker = 1; tick(&f);
sql(f.inst->topo_sqlite_db, "BEGIN;UPDATE attachment_keys SET ready=8 WHERE channel='900'");
for (unsigned poll = 0; poll < 50; poll++) uasync_poll(f.ua, 10);
assert(!f.key_events && !sqlite3_get_autocommit(f.inst->topo_sqlite_db) && CC(f.inst)->attachment_file_count == 1);
sql(f.inst->topo_sqlite_db, "ROLLBACK"); tick(&f); decryption_done(&f);
fault_prefix = NULL; cleanup(&f);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS decryption completion:%u SQL faults retain result/key, retry commits without second worker; foreign transaction untouched", cases);
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_INFO);
utun_instance_set_tun_init_enabled(0); size_t baseline = u_get_allocated_count();
test_sql_faults(); test_sql_controls(); test_inline_failure(); test_rollback_failure();
assert(fault == NONE && !fail_rollback && !fail_submit && rollback_fault == 1);
for (unsigned i = BEGIN; i < FAULT_COUNT; i++) assert(hits[i] == (i == COMMIT ? 2 : 1));
test_decryption_claim(); test_decryption_completion();
assert(u_get_allocated_count() == baseline);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS attachment: independent committed key/metadata before worker, allocations restored");
return 0;