From a62540800b1aa4d2122fc6401be2210cdea2c635 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 11 Oct 2026 00:23:10 +0200 Subject: [PATCH] Cancel local streams when signed membership is invalidated --- src/chat/member_sync.c | 1 + src/stream/stream.c | 10 +++++-- tests/test_stream_service.c | 16 +++++++++-- tools/chatgui/package_win.sh | 55 +++++++++++++++++++++--------------- 4 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index 60951c82..333a0ac1 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -888,6 +888,7 @@ int member_sync_verify_and_purge(struct UTUN_INSTANCE* inst, const char* ch_id) topo_node_sqlite_member_del(db, ch_id, nid); radio_forget_member_mute(inst, strtoull(ch_id, NULL, 10), nid); merkle_sync_recompute_path(inst, MT_DATA_MEMBERS, ch_id, nid); + _fire_props_changed(inst, nid, NULL, ch_id); /* пассивное удаление: если узел подключён напрямую — убрать из CHAT-группы */ if (inst->topo_groups) { uint64_t gid = strtoull(ch_id, NULL, 10); diff --git a/src/stream/stream.c b/src/stream/stream.c index 28eb89bd..a72f3a46 100644 --- a/src/stream/stream.c +++ b/src/stream/stream.c @@ -224,14 +224,20 @@ static void member_changed(uint64_t node_id, const char *tags, const char *chann struct stream_service *service = arg; if (service->stopping || !service->sessions) return; for (struct stream_session *s = service->sessions; s; s = s->next) { - if (s->closing || s->source || s->descriptor.source_node_id != node_id) continue; + int own = node_id == service->inst->node_id; + if (s->closing || (!own && (s->source || s->descriptor.source_node_id != node_id))) continue; struct TOPO_GROUP *group = stream_group(service->inst, s->descriptor.group_id); if (!group || !channel || strcmp(group->channel_id, channel)) continue; struct stream_member_state state; uint8_t key[32]; int result = stream_member_load(service->inst, group->group_id, node_id, &state, key); if (result == STREAM_ERR_NOT_MEMBER || result == STREAM_ERR_SIGNATURE || result == STREAM_ERR_FORMAT) { s->invalidated = 1; s->reason = STREAM_CLOSE_MEMBER; - } else if (result == STREAM_OK && (memcmp(key, s->public_key, 32) + DEBUG_WARN(DEBUG_CATEGORY_STREAM, "stream membership invalidated group=%llx member=%llx own=%d result=%d", + (unsigned long long)group->group_id, (unsigned long long)node_id, own, result); + } else if (own && result == STREAM_OK && memcmp(key, service->inst->my_ed25519_pubkey, 32)) { + s->invalidated = 1; s->reason = STREAM_CLOSE_MEMBER; + DEBUG_WARN(DEBUG_CATEGORY_STREAM, "stream own member key changed group=%llx", (unsigned long long)group->group_id); + } else if (!own && result == STREAM_OK && (memcmp(key, s->public_key, 32) || (state.published && (state.source.source_epoch != s->descriptor.source_epoch || state.source.n != s->descriptor.n || state.source.r != s->descriptor.r || state.source.block_size != s->descriptor.block_size)))) { diff --git a/tests/test_stream_service.c b/tests/test_stream_service.c index 908813b8..423ea556 100644 --- a/tests/test_stream_service.c +++ b/tests/test_stream_service.c @@ -40,7 +40,7 @@ static void on_stream(struct UTUN_INSTANCE *inst, const struct stream_event *eve (void)arg; unsigned node = 0; while (node < 4 && nodes[node] != inst) node++; - CHECK(node < 4 && event->descriptor.group_id == GROUP1); + CHECK(node < 4 && (event->descriptor.group_id == GROUP1 || event->type == STREAM_EVENT_CLOSED)); if (event->type == STREAM_EVENT_DATA) { CHECK(event->length == sizeof(expected) && !memcmp(event->data, expected, sizeof(expected))); data_events[node]++; @@ -289,10 +289,22 @@ int main(void) { CHECK(!stream_handle_active(mobile) && !stream_handle_active(desktop)); CHECK(stream_handle_reason(mobile) == STREAM_CLOSE_SOURCE_CHANGED && stream_handle_reason(desktop) == STREAM_CLOSE_SOURCE_CHANGED); stream_close(mobile); stream_close(desktop); + /* Повреждённое собственное членство удаляет обычная проверка Merkle; буфер не должен остаться активным. */ + sqlite3_stmt *broken = NULL; uint8_t bad_signature[64]; memset(bad_signature, 1, sizeof(bad_signature)); + CHECK(sqlite3_prepare_v2(nodes[1]->topo_sqlite_db, "UPDATE peers_710002 SET update_sig=? WHERE node_id=?", + -1, &broken, NULL) == SQLITE_OK); + sqlite3_bind_blob(broken, 1, bad_signature, sizeof(bad_signature), SQLITE_STATIC); + sqlite3_bind_int64(broken, 2, (sqlite3_int64)nodes[1]->node_id); + CHECK(sqlite3_step(broken) == SQLITE_DONE); sqlite3_finalize(broken); + CHECK(member_sync_verify_and_purge(nodes[1], "710002") == 1); + CHECK(!stream_handle_active(other_group) && stream_handle_reason(other_group) == STREAM_CLOSE_MEMBER); + run_until(&closed_events[1], 2); stream_close(other_group); other_group = NULL; + put_member(1, 1, GROUP2); + CHECK(stream_subscribe_open(nodes[1], GROUP2, nodes[0]->node_id, &other_group) == STREAM_OK); chat_core_delete_channel(nodes[1], "710002"); CHECK(!stream_handle_active(other_group) && stream_handle_reason(other_group) == STREAM_CLOSE_GROUP); stream_close(other_group); - DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] source epoch invalidation and channel deletion detach local handles"); + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] source epoch, lost own membership and channel deletion detach local handles"); char stale[MS_USERINFO_MAX + 1]; uint8_t stale_signature[64]; uint64_t stale_version; profile(0, GROUP1, stale, stale_signature, &stale_version); diff --git a/tools/chatgui/package_win.sh b/tools/chatgui/package_win.sh index b812edb2..5e3d20b0 100644 --- a/tools/chatgui/package_win.sh +++ b/tools/chatgui/package_win.sh @@ -53,20 +53,7 @@ done STAGE=$(mktemp -d "$BUILD_DIR/installer-stage.XXXXXX") PACKAGE_DIR="$STAGE/package" mkdir -p "$PACKAGE_DIR/licenses" "$STAGE/output" "$BUILD_DIR/installer" -cp "$BUILD_DIR/vibechat.exe" "$PACKAGE_DIR/" shopt -s nullglob nocasematch -for file in "$BUILD_DIR/"*.dll; do - name=${file##*/} - if [[ "$name" = Qt[56]*.dll && "$name" != "$QT_PACKAGE"*.dll ]]; then - echo "INFO: excluding DLL from other Qt version: $name" - continue - fi - cp "$file" "$PACKAGE_DIR/" -done -for file in platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll; do - mkdir -p "$PACKAGE_DIR/${file%/*}" - cp "$BUILD_DIR/$file" "$PACKAGE_DIR/$file" -done for file in LICENSE LICENSING THIRD_PARTY_NOTICES; do cp "$SRC_DIR/../../$file" "$PACKAGE_DIR/licenses/" done @@ -76,16 +63,31 @@ cp -R "$SRC_DIR/rlottie_src/licenses" "$PACKAGE_DIR/licenses/rlottie/" # Проверяем архитектуру/полноту уже собранного комплекта, включая динамические Qt-плагины. SYSTEM_DIR=$(cygpath -u "${WINDIR:-C:\\Windows}/System32") +source "$SRC_DIR/compiler_runtime.sh" declare -A LICENSE_PACKAGES=() -for file in "$PACKAGE_DIR/vibechat.exe" "$PACKAGE_DIR/"*.dll "$PACKAGE_DIR/platforms/"*.dll "$PACKAGE_DIR/imageformats/"*.dll; do +declare -A INCLUDED=() +FILES=(vibechat.exe platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll) +HAS_FFMPEG=0 +for ((i = 0; i < ${#FILES[@]}; i++)); do + relative=${FILES[i]} + [[ -z "${INCLUDED[${relative,,}]:-}" ]] || continue + INCLUDED[${relative,,}]=1 + name=${relative##*/} + if [[ "$name" = Qt[56]*.dll && "$name" != "$QT_PACKAGE"*.dll ]]; then + echo "ERROR: mixed Qt runtime: $relative does not belong to $QT_PACKAGE" >&2 + exit 1 + fi + file="$PACKAGE_DIR/$relative" + mkdir -p "${file%/*}" + cp "$BUILD_DIR/$relative" "$file" name=${file##*/} objdump -f "$file" > "$STAGE/headers.txt" - rg_output=$(awk '/file format/ {print $NF}' "$STAGE/headers.txt") - [[ "$rg_output" = pei-x86-64 ]] || { echo "ERROR: $name is not a Windows x64 binary: $rg_output" >&2; exit 1; } + binary_format=$(awk '/file format/ {print $NF}' "$STAGE/headers.txt") + [[ "$binary_format" = pei-x86-64 ]] || { echo "ERROR: $name is not a Windows x64 binary: $binary_format" >&2; exit 1; } objdump -p "$file" > "$STAGE/imports.txt" while read -r dep; do [[ -n "$dep" ]] || continue - [[ -f "$PACKAGE_DIR/$dep" ]] && continue + if [[ -f "$BUILD_DIR/$dep" ]]; then FILES+=("$dep"); continue; fi case "$dep" in api-ms-win-*.dll|ext-ms-win-*.dll) continue ;; esac if [[ -f "$SYSTEM_DIR/$dep" && "$dep" != msvcp*.dll && "$dep" != vcruntime*.dll ]]; then continue; fi echo "ERROR: incomplete installer runtime: $name requires $dep" >&2 @@ -95,7 +97,10 @@ for file in "$PACKAGE_DIR/vibechat.exe" "$PACKAGE_DIR/"*.dll "$PACKAGE_DIR/platf # Лицензии внешних библиотек берём из пакетов той же UCRT64-сборки. [[ "$name" != vibechat.exe && "$name" != librlottie.dll ]] || continue - [[ ! -f "$SRC_DIR/ffmpeg/ffmpeg_build/install/bin/$name" ]] || continue + if [[ -f "$SRC_DIR/ffmpeg/ffmpeg_build/install/bin/$name" ]] && ! chatgui_is_compiler_runtime_dll "$name"; then + HAS_FFMPEG=1 + continue + fi runtime_file="$RUNTIME_PREFIX/bin/$name" if [[ "$file" = "$PACKAGE_DIR/platforms/"* || "$file" = "$PACKAGE_DIR/imageformats/"* ]]; then relative=${file#"$PACKAGE_DIR/"} @@ -117,11 +122,15 @@ for pkg in "${!LICENSE_PACKAGES[@]}"; do done < "$STAGE/package-files.txt" echo "INFO: included runtime license notices: $pkg" done -if [[ -d "$SRC_DIR/ffmpeg/ffmpeg_build" ]]; then - # Архив исходников FFmpeg остаётся распакованным после build_ffmpeg.sh. - for file in "$SRC_DIR/ffmpeg/ffmpeg_build/ffmpeg-"*/COPYING* "$SRC_DIR/ffmpeg/ffmpeg_build/ffmpeg-"*/LICENSE.md; do - mkdir -p "$PACKAGE_DIR/licenses/ffmpeg" - cp "$file" "$PACKAGE_DIR/licenses/ffmpeg/" +if [[ "$HAS_FFMPEG" = 1 ]]; then + FFMPEG_SOURCE="$SRC_DIR/ffmpeg/ffmpeg_build/src" + [[ -f "$FFMPEG_SOURCE/ffmpeg/COPYING.GPLv3" ]] || { echo "ERROR: FFmpeg source license notices missing: $FFMPEG_SOURCE" >&2; exit 1; } + # Сохраняем уведомления FFmpeg и его встроенных статических кодеков. + for file in "$FFMPEG_SOURCE/"*/COPYING* "$FFMPEG_SOURCE/"*/LICENSE* "$FFMPEG_SOURCE/"*/PATENTS; do + [[ -f "$file" ]] || continue + relative=${file#"$FFMPEG_SOURCE/"} + mkdir -p "$PACKAGE_DIR/licenses/ffmpeg/${relative%/*}" + cp "$file" "$PACKAGE_DIR/licenses/ffmpeg/$relative" done fi