From 9ee61dce6c8d9b99f0afb88609bd9b89c41d2c57 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 14 Sep 2026 15:10:04 +0300 Subject: [PATCH] fix: ncd handle UAF in topo_recovery and topo_group_invite callbacks topo_recovery_callback: detach cb on UP handoff and close handle on DOWN/TIMEOUT so no handle with cb_arg==ctx outlives ctx (ASAN heap-buffer-overflow in topo_recovery_callback). tgi_to_channel_cb: force_close handle on NCD_EVENT_TIMEOUT so a late UP cannot fire into the freed tgi_conn_ctx. Also: android chatgui reverseLayout scroll + msg order/stable pagination. --- src/routing_layer/topo_group_invite.c | 1 + src/routing_layer/topo_recovery.c | 5 ++- .../com/utun/chat/ui/screens/ChatScreen.kt | 37 ++++--------------- .../jni_bridge/android_jni_bridge.c | 8 +++- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/routing_layer/topo_group_invite.c b/src/routing_layer/topo_group_invite.c index 5bbf37cb..831489d0 100644 --- a/src/routing_layer/topo_group_invite.c +++ b/src/routing_layer/topo_group_invite.c @@ -517,6 +517,7 @@ static void tgi_to_channel_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, voi node_conn_direct_set_callback(h, NULL, NULL); u_free(c); } else if (ev == NCD_EVENT_TIMEOUT) { + node_conn_direct_force_close(h); u_free(c); } } diff --git a/src/routing_layer/topo_recovery.c b/src/routing_layer/topo_recovery.c index da3daa72..c2c45ca2 100644 --- a/src/routing_layer/topo_recovery.c +++ b/src/routing_layer/topo_recovery.c @@ -65,14 +65,17 @@ static void topo_recovery_callback(struct NODE_CONN_DIRECT* h, enum ncd_event ev if (ctx->connect_timer) { uasync_cancel_timeout(ctx->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; } for (size_t i = 0; i < ctx->count; i++) if (ctx->nodes[i].node_id == node_id) { ctx->nodes[i] = ctx->nodes[ctx->count - 1]; ctx->count--; break; } + ctx->current_node_id = 0; + ctx->current_handle = NULL; if (event == NCD_EVENT_UP) { struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(ctx->group, node_id); if (nq) nq->handle = h; + node_conn_direct_set_callback(h, NULL, NULL); DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx reconnected to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); } else { + node_conn_direct_close(h); DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx connect to %016llx failed: %d", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id, event); } - ctx->current_node_id = 0; topo_recovery_try_next(ctx); } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt index f0727dbc..872f0892 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.utun.chat.data.Channel @@ -112,31 +111,10 @@ fun ChatScreen( val playbackState by viewModel.playbackState.collectAsState() - var initialScrolled by remember { mutableStateOf(false) } - + /* своё сообщение — подтягиваемся вниз (в reverseLayout индекс 0 — это низ) */ LaunchedEffect(messages.size) { - if (messages.isEmpty()) return@LaunchedEffect - val lastIndex = messages.size - 1 - if (!initialScrolled) { - /* первый вход — мгновенный прыжок вниз без анимации */ - listState.scrollToItem(lastIndex) - initialScrolled = true - return@LaunchedEffect - } - /* новое сообщение: скроллим вниз если своё, либо если были у нижнего края */ - val ownMessage = messages.lastOrNull()?.isOutgoing == true - val wasAtBottom = !listState.canScrollForward - if (ownMessage || wasAtBottom) { - listState.animateScrollToItem(lastIndex) - } - } - - /* при открытии клавиатуры прижимаем список к низу, чтобы последнее сообщение осталось видимым */ - val density = LocalDensity.current - val imeVisible = WindowInsets.ime.getBottom(density) > 0 - LaunchedEffect(imeVisible) { - if (imeVisible && messages.isNotEmpty()) { - listState.scrollToItem(messages.size - 1) + if (messages.lastOrNull()?.isOutgoing == true) { + listState.scrollToItem(0) } } @@ -155,10 +133,12 @@ fun ChatScreen( ) LazyColumn( modifier = Modifier.weight(1f).fillMaxWidth(), - state = listState + state = listState, + reverseLayout = true ) { - items(messages.size) { idx -> - MessageBubble(messages[idx], playbackState = playbackState, + item { Spacer(Modifier.height(8.dp)) } + items(messages.reversed()) { msg -> + MessageBubble(msg, playbackState = playbackState, onPlayVoice = { path, durMs -> viewModel.playVoiceMessage(path, durMs) }, onSeekVoice = { path, seekMs, durMs -> viewModel.seekVoice(path, seekMs, durMs) }, onDownloadFile = { msg -> viewModel.downloadAttachment(msg) }, @@ -171,7 +151,6 @@ fun ChatScreen( onOpenImage = { msg -> onViewPhoto(msg) }, onPlayVideo = { msg -> onPlayVideo(msg) }) } - item { Spacer(Modifier.height(8.dp)) } } InputBar( text = text, diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 1a6053c1..ba712a69 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -487,8 +487,12 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit) { char tbl_msg[80]; msg_table_name(channel_id, tbl_msg, sizeof(tbl_msg)); - char sql[256]; - snprintf(sql, sizeof(sql), "SELECT id, node_id, data, timestamp, local_attrs FROM \"%s\" ORDER BY timestamp ASC LIMIT %d", tbl_msg, limit); + char sql[512]; + snprintf(sql, sizeof(sql), + "SELECT id, node_id, data, timestamp, local_attrs FROM (" + "SELECT id, node_id, data, timestamp, local_attrs, author_signature FROM \"%s\"" + " ORDER BY timestamp DESC, author_signature DESC LIMIT %d" + ") ORDER BY timestamp ASC, author_signature ASC", tbl_msg, limit); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return u_strdup("[]");