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.
This commit is contained in:
evgeny committed 2026-09-14 15:10:04 +03:00
1 parent fc3295ce12
commit 9ee61dce6c
4 files changed
+19 -32

No files matched your search

@@ -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,
@@ -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("[]");