fix(sdk): Use TimelineEvent::timestamp when building an IndexableEvent.

This patch prevents a bug when a malformed `origin_server_ts`
injected in an event can crash Tantivy if the value is too large, [it
overflows][1]:

```
tantivy-common-0.11.0/src/datetime.rs:65:30:
attempt to multiply with overflow
```

The fix consists of using the result of `TimelineEvent::timestamp` which
already deals with malformed `origin_server_ts`.

[1]: 31ca1a8ba2/common/src/datetime.rs (L62-L67)
This commit is contained in:
Ivan Enderlin
2026-07-28 12:59:39 +02:00
parent 4c5bac3ae1
commit 6e9f696b24
3 changed files with 53 additions and 23 deletions

View File

@@ -49,7 +49,11 @@ pub struct IndexableEvent {
/// The sender of the event.
pub(crate) sender: OwnedUserId,
/// The origin server timestamp of the event.
pub(crate) timestamp: MilliSecondsSinceUnixEpoch,
///
/// Please use the `matrix_sdk_common::TimelineEvent::timestamp` as much as
/// possible as it protects against malformed `origin_server_ts`. At worst,
/// use the `matrix_sdk_common::serde_helpers::extract_timestamp` function.
pub(crate) timestamp: Option<MilliSecondsSinceUnixEpoch>,
/// The text to index for this event.
pub(crate) body: String,
}
@@ -60,7 +64,7 @@ impl IndexableEvent {
event_id: OwnedEventId,
original_event_id: OwnedEventId,
sender: OwnedUserId,
timestamp: MilliSecondsSinceUnixEpoch,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
body: String,
) -> Self {
Self { event_id, original_event_id, sender, timestamp, body }
@@ -427,7 +431,7 @@ mod tests {
event.event_id.clone(),
original_event_id,
event.sender.clone(),
event.origin_server_ts,
Some(event.origin_server_ts),
content.body.clone(),
)
}

View File

@@ -96,11 +96,18 @@ impl MatrixSearchIndexSchema for RoomMessageSchema {
/// Given an [`IndexableEvent`] return a [`TantivyDocument`].
fn make_doc(&self, event: IndexableEvent) -> Result<TantivyDocument, IndexError> {
let timestamp = DateTime::from_timestamp_millis(
event
.timestamp
.map(|timestamp| timestamp.get().into())
// If the timestamp is missing, use 0 as the “no value”.
.unwrap_or(0),
);
let document = doc!(
self.event_id_field => event.event_id.to_string(),
self.body_field => event.body,
self.date_field =>
DateTime::from_timestamp_millis(event.timestamp.get().into()),
self.date_field => timestamp,
self.sender_field => event.sender.to_string(),
self.original_event_id_field => event.original_event_id.to_string(),
);

View File

@@ -27,7 +27,7 @@ use matrix_sdk_search::{
index::{IndexableEvent, RoomIndex, RoomIndexOperation, builder::RoomIndexBuilder},
};
use ruma::{
EventId, OwnedEventId, OwnedRoomId, RoomId,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, RoomId,
events::{
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
poll::{
@@ -284,7 +284,10 @@ fn room_message_body(msgtype: &MessageType) -> Option<String> {
/// Build an [`IndexableEvent`] from a room message, or `None` if its type
/// carries no searchable text.
fn indexable_from_room_message(event: &OriginalSyncRoomMessageEvent) -> Option<IndexableEvent> {
fn indexable_from_room_message(
event: &OriginalSyncRoomMessageEvent,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> Option<IndexableEvent> {
let body = room_message_body(&event.content.msgtype)?;
let original_event_id = match &event.content.relates_to {
Some(Relation::Replacement(replacement)) => replacement.event_id.clone(),
@@ -295,7 +298,7 @@ fn indexable_from_room_message(event: &OriginalSyncRoomMessageEvent) -> Option<I
event.event_id.clone(),
original_event_id,
event.sender.clone(),
event.origin_server_ts,
timestamp,
body,
))
}
@@ -305,12 +308,13 @@ fn indexable_from_room_message(event: &OriginalSyncRoomMessageEvent) -> Option<I
/// original.
async fn handle_possible_edit(
event: &OriginalSyncRoomMessageEvent,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
cache: &RoomEventCache,
) -> Option<RoomIndexOperation> {
if let Some(Relation::Replacement(replacement_data)) = &event.content.relates_to {
if let Some(recent) = get_most_recent_edit(cache, &replacement_data.event_id).await {
return Some(
indexable_from_room_message(&recent).map_or(
indexable_from_room_message(&recent, timestamp).map_or(
RoomIndexOperation::Noop,
|indexable| {
RoomIndexOperation::Edit(replacement_data.event_id.clone(), indexable)
@@ -328,15 +332,18 @@ async fn handle_possible_edit(
/// depending on the message.
async fn handle_room_message(
event: SyncRoomMessageEvent,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
cache: &RoomEventCache,
) -> Option<RoomIndexOperation> {
if let Some(event) = event.as_original() {
return handle_possible_edit(event, cache).await.or(get_most_recent_edit(
return handle_possible_edit(event, timestamp, cache).await.or(get_most_recent_edit(
cache,
&event.event_id,
)
.await
.and_then(|recent| indexable_from_room_message(&recent).map(RoomIndexOperation::Add)));
.and_then(|recent| {
indexable_from_room_message(&recent, timestamp).map(RoomIndexOperation::Add)
}));
}
None
}
@@ -345,6 +352,7 @@ async fn handle_room_message(
/// re-adding the most recent remaining version if an edit was redacted.
async fn handle_room_redaction(
event: SyncRoomRedactionEvent,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
cache: &RoomEventCache,
rules: &RedactionRules,
) -> Option<RoomIndexOperation> {
@@ -357,7 +365,7 @@ async fn handle_room_redaction(
redacted_event,
))) = redacted_event.raw().deserialize()
&& let Some(redacted_event) = redacted_event.as_original()
&& let Some(operation) = handle_possible_edit(redacted_event, cache).await
&& let Some(operation) = handle_possible_edit(redacted_event, timestamp, cache).await
{
return Some(operation);
}
@@ -368,14 +376,17 @@ async fn handle_room_redaction(
}
/// Return a [`RoomIndexOperation::Add`] indexing a sticker's descriptive text.
fn handle_sticker(event: SyncStickerEvent) -> Option<RoomIndexOperation> {
fn handle_sticker(
event: SyncStickerEvent,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> Option<RoomIndexOperation> {
let event = event.as_original()?;
Some(RoomIndexOperation::Add(IndexableEvent::new(
event.event_id.clone(),
event.event_id.clone(),
event.sender.clone(),
event.origin_server_ts,
timestamp,
event.content.body.clone(),
)))
}
@@ -386,7 +397,10 @@ fn handle_sticker(event: SyncStickerEvent) -> Option<RoomIndexOperation> {
/// ponytail: only indexes the initial `New` poll — edits (`Replacement`) and
/// poll ends are ignored. Add edit handling if editing a poll needs to update
/// search results.
fn handle_unstable_poll_start(event: SyncUnstablePollStartEvent) -> Option<RoomIndexOperation> {
fn handle_unstable_poll_start(
event: SyncUnstablePollStartEvent,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> Option<RoomIndexOperation> {
let event = event.as_original()?;
let UnstablePollStartEventContent::New(content) = &event.content else {
@@ -404,7 +418,7 @@ fn handle_unstable_poll_start(event: SyncUnstablePollStartEvent) -> Option<RoomI
event.event_id.clone(),
event.event_id.clone(),
event.sender.clone(),
event.origin_server_ts,
timestamp,
body,
)))
}
@@ -414,7 +428,10 @@ fn handle_unstable_poll_start(event: SyncUnstablePollStartEvent) -> Option<RoomI
///
/// ponytail: like [`handle_unstable_poll_start`], edits and poll ends are
/// ignored — only the initial poll is indexed.
fn handle_poll_start(event: SyncPollStartEvent) -> Option<RoomIndexOperation> {
fn handle_poll_start(
event: SyncPollStartEvent,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> Option<RoomIndexOperation> {
let event = event.as_original()?;
// Skip poll edits, matching the unstable poll handling.
@@ -435,7 +452,7 @@ fn handle_poll_start(event: SyncPollStartEvent) -> Option<RoomIndexOperation> {
event.event_id.clone(),
event.event_id.clone(),
event.sender.clone(),
event.origin_server_ts,
timestamp,
body,
)))
}
@@ -453,19 +470,21 @@ async fn parse_timeline_event(
return None;
}
let timestamp = event.timestamp();
match event.raw().deserialize() {
Ok(event) => match event {
AnySyncTimelineEvent::MessageLike(event) => match event {
AnySyncMessageLikeEvent::RoomMessage(event) => {
handle_room_message(event, cache).await
handle_room_message(event, timestamp, cache).await
}
AnySyncMessageLikeEvent::RoomRedaction(event) => {
handle_room_redaction(event, cache, redaction_rules).await
handle_room_redaction(event, timestamp, cache, redaction_rules).await
}
AnySyncMessageLikeEvent::Sticker(event) => handle_sticker(event),
AnySyncMessageLikeEvent::PollStart(event) => handle_poll_start(event),
AnySyncMessageLikeEvent::Sticker(event) => handle_sticker(event, timestamp),
AnySyncMessageLikeEvent::PollStart(event) => handle_poll_start(event, timestamp),
AnySyncMessageLikeEvent::UnstablePollStart(event) => {
handle_unstable_poll_start(event)
handle_unstable_poll_start(event, timestamp)
}
_ => None,
},