From 16d7fc4fb5e2ba4ecf59b5a68ad670597a3be0a8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 07:02:44 +0100 Subject: [PATCH] fix(chat): validate owner versions and complete member reads before mutation --- src/chat/member_sync.c | 37 ++++++++++++++++++++++++++++++------- src/chat/member_sync.h | 4 ++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index f652be4a..81d7f72d 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -52,6 +52,28 @@ static uint64_t member_read64(const uint8_t* p) { return n; } +/* Версия owner-блока — каноническая десятичная строка1..INT_MAX,без atoi overflow. */ +static int owner_version_pair(const char* key, const char* text, void* arg) { + if (strcmp(key, "ver")) return 0; + int value = 0; + if (*text < '1' || *text > '9') goto invalid; + for (const char* p = text; *p; p++) { + if (*p < '0' || *p > '9' || value > (INT_MAX - (*p - '0')) / 10) goto invalid; + value = value * 10 + (*p - '0'); + } + *(int*)arg = value; return 0; +invalid: + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "member owner: invalid version format/range"); return -1; +} + +int member_sync_owner_version(sqlite3* db, const char* tags) { + if (!tags || !tags[0]) return 0; + int version = 0; + if (chat_json_strings(db, tags, MS_ADM_TAGS_MAX, owner_version_pair, &version)) return -1; + if (!version) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "member owner: missing version"); return -1; } + return version; +} + int member_sync_build_owner_msg(uint64_t node_id, const char* tags, uint8_t* out, size_t capacity) { size_t len = tags ? strlen(tags) : 0; if (!tags || !out || len > MS_ADM_TAGS_MAX || capacity < len + 8) { @@ -577,10 +599,10 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, if (sqlite3_errcode(db) == SQLITE_NOMEM) rc = SQLITE_NOMEM; } } - sqlite3_finalize(st); - if (rc != SQLITE_ROW && rc != SQLITE_DONE) { - DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: local record read failed ns=%s node=%016llx rc=%d: %s", - MS_ID, ch_id, (unsigned long long)m->node_id, rc, sqlite3_errmsg(db)); return MT_ERR_IO; + int final = sqlite3_finalize(st); + if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: local record read failed ns=%s node=%016llx rc=%d finalize=%d: %s", + MS_ID, ch_id, (unsigned long long)m->node_id, rc, final, sqlite3_errmsg(db)); return MT_ERR_IO; } } @@ -666,7 +688,8 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, int adm_ver = 0, adm_storage = 0; if (m->adm_tags && m->adm_tags[0] && m->adm_tags_sig && !_sig_is_zero64(m->adm_tags_sig)) { char ver_str[32]; - adm_ver = json_flat_get(m->adm_tags, "ver", ver_str, sizeof(ver_str)) == 0 ? atoi(ver_str) : 0; + adm_ver = member_sync_owner_version(db, m->adm_tags); + if (adm_ver < 0) return MT_ERR_IO; adm_storage = (json_flat_get(m->adm_tags, "storage", ver_str, sizeof(ver_str)) == 0 && strcmp(ver_str, "yes") == 0) ? 1 : 0; uint8_t ch_ed_pub[32] = {0}; int lookup = topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL); @@ -689,8 +712,8 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, if (block_b_ok) { int local_ver = 0; if (has_local && local_tags[0]) { - char lv[32]; - if (json_flat_get(local_tags, "ver", lv, sizeof(lv)) == 0) local_ver = atoi(lv); + local_ver = member_sync_owner_version(db, local_tags); + if (local_ver < 0) return MT_ERR_IO; } if (adm_ver > local_ver) block_b_changed = 1; else if (adm_ver < local_ver) block_b_stale = 1; diff --git a/src/chat/member_sync.h b/src/chat/member_sync.h index cf4fdb97..f36d2853 100644 --- a/src/chat/member_sync.h +++ b/src/chat/member_sync.h @@ -56,6 +56,10 @@ int member_sync_verified_ed25519(struct UTUN_INSTANCE* inst, const char* channel /* UTF-8 JSON bytes, excluding NUL. A complete record fits in one Merkle page. */ #define MS_ADM_TAGS_MAX 8192 #define MS_USERINFO_MAX 1024 +/* Проверить весь строковый JSON owner-блока,уникальность ключей и ver1..INT_MAX без ведущих нулей. + * Возвращает версию,0 для NULL/пустого блока,-1 при ошибке формата/SQL. Подпись здесь не проверяется. */ +int member_sync_owner_version(struct sqlite3* db, const char* tags); + /* Байты для подписи владельца: tags без NUL, затем node_id[8] в порядке байтов хоста. * Возвращает длину или -1, если данные/буфер не подходят. */ int member_sync_build_owner_msg(uint64_t node_id, const char* tags, uint8_t* out, size_t capacity);