From 0b81342b5cd1e0948d73a8f6582d7ea0098be0d7 Mon Sep 17 00:00:00 2001 From: Zaki Manian Date: Thu, 12 Mar 2026 11:10:31 -0700 Subject: [PATCH] Fix UTF-8 unsafe truncation in WASM emit_message (#1015) Co-authored-by: Lawyered --- src/channels/wasm/host.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/channels/wasm/host.rs b/src/channels/wasm/host.rs index 9f09455f..eeaccb20 100644 --- a/src/channels/wasm/host.rs +++ b/src/channels/wasm/host.rs @@ -63,7 +63,11 @@ const ALLOWED_MIME_PREFIXES: &[&str] = &[ "application/x-tar", "application/octet-stream", ]; - +/// Truncate a string to at most `max_bytes` without splitting UTF-8 code points. +fn truncate_utf8(s: &str, max_bytes: usize) -> &str { + let end = crate::util::floor_char_boundary(s, max_bytes); + &s[..end] +} /// A message emitted by a WASM channel to be sent to the agent. #[derive(Debug, Clone)] pub struct EmittedMessage { @@ -264,7 +268,7 @@ impl ChannelHostState { max = MAX_MESSAGE_CONTENT_SIZE, "Message content too large, truncating" ); - let mut truncated = msg.content[..MAX_MESSAGE_CONTENT_SIZE].to_string(); + let mut truncated = truncate_utf8(&msg.content, MAX_MESSAGE_CONTENT_SIZE).to_string(); truncated.push_str("... (truncated)"); let msg = EmittedMessage { content: truncated, @@ -631,6 +635,7 @@ mod tests { use crate::channels::wasm::host::{ Attachment, ChannelEmitRateLimiter, ChannelHostState, EmittedMessage, MAX_ATTACHMENT_TOTAL_SIZE, MAX_ATTACHMENTS_PER_MESSAGE, MAX_EMITS_PER_EXECUTION, + MAX_MESSAGE_CONTENT_SIZE, }; #[test] @@ -689,6 +694,25 @@ mod tests { assert_eq!(state.emits_dropped(), 1); } + #[test] + fn test_emit_message_truncates_utf8_safely() { + let caps = ChannelCapabilities::for_channel("test"); + let mut state = ChannelHostState::new("test", caps); + + let prefix = "a".repeat(MAX_MESSAGE_CONTENT_SIZE - 1); + let content = format!("{}🙂suffix", prefix); + let msg = EmittedMessage::new("user123", content); + + state.emit_message(msg).unwrap(); + let messages = state.take_emitted_messages(); + assert_eq!(messages.len(), 1); + + let emitted = &messages[0].content; + assert!(emitted.starts_with(&prefix)); + assert!(emitted.ends_with("... (truncated)")); + assert!(!emitted.contains("🙂")); + } + #[test] fn test_workspace_write_prefixing() { let caps = ChannelCapabilities::for_channel("slack");