From 8b16f4009407b5cc2ea83b5effcc303eeef64bc9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 05:11:03 +0100 Subject: [PATCH] test(chat): add precise avatar SQL fault hooks and committed-state observer --- tests/Makefile.am | 3 +- tests/test_group_avatar_sql.c | 96 +++++++++++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/tests/Makefile.am b/tests/Makefile.am index 713b9085..fd7d87c9 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -322,7 +322,8 @@ test_chat_channel_LDFLAGS = -Wl,--wrap=EVP_DigestSign -Wl,--wrap=sqlite3_exec -W -Wl,--wrap=EVP_PKEY_get_raw_public_key -Wl,--wrap=EVP_PKEY_get_raw_private_key -Wl,--wrap=RAND_bytes test_group_avatar_sql_SOURCES = test_group_avatar_sql.c test_group_avatar_sql_LDADD = $(LIBUTUN) $(COMMON_LIBS) -test_group_avatar_sql_LDFLAGS = -Wl,--wrap=sqlite3_bind_blob +test_group_avatar_sql_LDFLAGS = -Wl,--wrap=sqlite3_bind_blob -Wl,--wrap=sqlite3_bind_int64 -Wl,--wrap=sqlite3_bind_text \ + -Wl,--wrap=sqlite3_exec -Wl,--wrap=sqlite3_prepare_v2 -Wl,--wrap=sqlite3_step -Wl,--wrap=sqlite3_changes -Wl,--wrap=sqlite3_finalize else test_socket_monitor_LDFLAGS += -Wl,--wrap=read endif diff --git a/tests/test_group_avatar_sql.c b/tests/test_group_avatar_sql.c index aa682b17..04d486b4 100644 --- a/tests/test_group_avatar_sql.c +++ b/tests/test_group_avatar_sql.c @@ -7,19 +7,107 @@ static int fail_hash; static unsigned events; +static sqlite3* reader; +static uint64_t group_id; +enum fault { F_NONE, F_PREPARE, F_BIND, F_STEP, F_CHANGES, F_FINALIZE, F_EXEC }; +static enum fault fault; +static const char* target; +static int bind_index, last_target, fail_rollback, fail_schema_rollback, releases_after_failed_rollback; +static unsigned image_number; + +static int matches(const char* sql) { return target && !strncmp(sql, target, strlen(target)); } +static int take(enum fault point, const char* sql, int index) { + if (fault != point || (sql && !matches(sql)) || (point == F_BIND && index != bind_index)) return 0; + fault = F_NONE; DEBUG_WARN(DEBUG_CATEGORY_CHAT, "avatar test: SQL fault stage=%d bind=%d target=%s", point, index, target); return 1; +} + +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 (*callback)(void*, int, char**, char**), void* arg, char** error) { + if (fail_rollback && !strcmp(sql, "ROLLBACK")) { fail_rollback = 0; return SQLITE_IOERR; } + if (fail_schema_rollback && !strcmp(sql, "ROLLBACK TO group_avatar_schema")) { fail_schema_rollback = 0; return SQLITE_IOERR; } + if (!fail_schema_rollback && releases_after_failed_rollback && !strcmp(sql, "RELEASE group_avatar_schema")) + releases_after_failed_rollback++; + if (take(F_EXEC, sql, 0)) return SQLITE_IOERR; + return __real_sqlite3_exec(db, sql, callback, arg, error); +} +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 bytes, sqlite3_stmt** st, const char** tail) { + if (take(F_PREPARE, sql, 0)) { *st = NULL; return SQLITE_NOMEM; } + return __real_sqlite3_prepare_v2(db, sql, bytes, st, tail); +} extern int __real_sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int, void (*)(void*)); int __wrap_sqlite3_bind_blob(sqlite3_stmt* st, int index, const void* value, int size, void (*destroy)(void*)) { - const char* target = "UPDATE channels SET avatar_version="; - if (fail_hash && index == 3 && !strncmp(sqlite3_sql(st), target, strlen(target))) { + const char* update = "UPDATE channels SET avatar_version="; + if (fail_hash && index == 3 && !strncmp(sqlite3_sql(st), update, strlen(update))) { fail_hash = 0; DEBUG_WARN(DEBUG_CATEGORY_CHAT, "avatar test: injected hash bind failure"); return SQLITE_NOMEM; } + if (take(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; return __real_sqlite3_bind_blob(st, index, value, size, 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 (take(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; + return __real_sqlite3_bind_int64(st, index, value); +} +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* value, int bytes, void (*destroy)(void*)) { + if (take(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; + return __real_sqlite3_bind_text(st, index, value, bytes, destroy); +} +extern int __real_sqlite3_step(sqlite3_stmt*); +int __wrap_sqlite3_step(sqlite3_stmt* st) { + last_target = matches(sqlite3_sql(st)); if (take(F_STEP, sqlite3_sql(st), 0)) return SQLITE_IOERR; + return __real_sqlite3_step(st); +} +extern int __real_sqlite3_changes(sqlite3*); +int __wrap_sqlite3_changes(sqlite3* db) { + if (last_target && take(F_CHANGES, NULL, 0)) return 0; + return __real_sqlite3_changes(db); +} +extern int __real_sqlite3_finalize(sqlite3_stmt*); +int __wrap_sqlite3_finalize(sqlite3_stmt* st) { + int fail = st && take(F_FINALIZE, sqlite3_sql(st), 0); + int rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc; +} + +static void snapshot(sqlite3* db, uint8_t out[32]) { + sqlite3_stmt* st = NULL; + assert(sqlite3_prepare_v2(db, "SELECT channel_id,avatar_version,avatar_data,avatar_hash,avatar_signature," + "x25519_privkey,ed25519_privkey FROM channels ORDER BY channel_id", -1, &st, NULL) == SQLITE_OK); + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); assert(ctx && EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) == 1); int rc; + while ((rc = sqlite3_step(st)) == SQLITE_ROW) for (int i = 0; i < 7; i++) { + int type = sqlite3_column_type(st, i), bytes = sqlite3_column_bytes(st, i); const void* value = sqlite3_column_blob(st, i); + assert(!bytes || value); + assert(EVP_DigestUpdate(ctx, &type, sizeof(type)) == 1 && EVP_DigestUpdate(ctx, &bytes, sizeof(bytes)) == 1); + if (bytes) assert(EVP_DigestUpdate(ctx, value, (size_t)bytes) == 1); + } + assert(rc == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK); unsigned length; + assert(EVP_DigestFinal_ex(ctx, out, &length) == 1 && length == 32); EVP_MD_CTX_free(ctx); +} + static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int size) { - (void)inst; (void)data; (void)size; - if (type == CHAT_EVT_CHANNEL_UPDATED) events++; + if (type != CHAT_EVT_CHANNEL_UPDATED) return; + assert(size > 1 && size == data[0] + 1 && sqlite3_get_autocommit(CC(inst)->db)); + struct group_avatar_record record; assert(!group_avatar_get(inst, group_id, &record) && record.version); + uint8_t writer[32], committed[32]; snapshot(CC(inst)->db, writer); snapshot(reader, committed); + assert(!memcmp(writer, committed, 32)); events++; +} + +static int publish(struct UTUN_INSTANCE* inst) { + uint32_t image = ++image_number; + return group_avatar_set(inst, group_id, (const uint8_t*)&image, sizeof(image)); +} + +static void refused(struct UTUN_INSTANCE* inst) { + uint8_t before[32], after[32]; snapshot(reader, before); unsigned count = events; + assert(publish(inst) < 0 && fault == F_NONE && sqlite3_get_autocommit(CC(inst)->db)); + snapshot(reader, after); assert(!memcmp(before, after, 32) && events == count); +} + +static void retry(struct UTUN_INSTANCE* inst) { + unsigned before = events; assert(!publish(inst) && events == before + 1); } int main(void) {