Browse Source

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.
v2
evgeny 2 weeks ago
parent
commit
9ee61dce6c
  1. 1
      src/routing_layer/topo_group_invite.c
  2. 5
      src/routing_layer/topo_recovery.c
  3. 37
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt
  4. 8
      tools/chatgui-android/jni_bridge/android_jni_bridge.c

1
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);
}
}

5
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);
}

37
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,

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

Loading…
Cancel
Save