Filed from a whole-project review; verified against source.
Problem
runtime/src/cbor/cbor.cc:285 decodes a tag-1 (epoch) timestamp as:
return Document(TimestampValue{Timestamp::FromEpochMilliseconds(inner->as_int() * 1000),
TimestampFormat::kEpochSeconds});
as_int() can be any value up to INT64_MAX (the unsigned path only rejects values above INT64_MAX), so inner->as_int() * 1000 overflows int64_t — undefined behavior triggered by a ~9-byte payload. Under UBSan this aborts; in release it silently produces a garbage instant that then flows into Decompose/Format.
Failure scenario
A peer sends CBOR C1 1B 7F FF FF FF FF FF FF FF (tag 1 + a large uint64); * 1000 overflows. This is the one actual-UB finding in the runtime and is reachable from any untrusted CBOR body binding a timestamp member.
Related (same class, HIGH — fold in or split as you prefer)
runtime/src/core/document_serde.cc:22 + runtime/src/core/timestamp.cc:221-223: epoch-seconds timestamps forward any JSON/CBOR number to FromEpochSeconds, which does static_cast<int64_t>(std::llround(seconds * 1000.0)) with no range check; ParseEpochSeconds (timestamp.cc:190) accepts arbitrarily long digit strings (isfinite only). A value like 1e300 or 99999999999999999999 yields an unspecified instant instead of an error.
runtime/src/core/timestamp.cc:54-70,225-253: extreme instants format to 5+ digit / negative years via %04d, which no conformant peer (ParseDateTime requires 4-digit years) can round-trip — silent data corruption.
Suggested fix
Range-check before the multiply/cast in all three spots: reject (return Error::Serialization) when the epoch value, scaled to milliseconds, would fall outside the representable instant range, and bound the accepted year in Format/Decompose. A shared "checked seconds/millis → Timestamp" helper would cover the CBOR and JSON paths together.
Filed from a whole-project review; verified against source.
Problem
runtime/src/cbor/cbor.cc:285decodes a tag-1 (epoch) timestamp as:as_int()can be any value up toINT64_MAX(the unsigned path only rejects values aboveINT64_MAX), soinner->as_int() * 1000overflowsint64_t— undefined behavior triggered by a ~9-byte payload. Under UBSan this aborts; in release it silently produces a garbage instant that then flows intoDecompose/Format.Failure scenario
A peer sends CBOR
C1 1B 7F FF FF FF FF FF FF FF(tag 1 + a large uint64);* 1000overflows. This is the one actual-UB finding in the runtime and is reachable from any untrusted CBOR body binding a timestamp member.Related (same class, HIGH — fold in or split as you prefer)
runtime/src/core/document_serde.cc:22+runtime/src/core/timestamp.cc:221-223: epoch-seconds timestamps forward any JSON/CBOR number toFromEpochSeconds, which doesstatic_cast<int64_t>(std::llround(seconds * 1000.0))with no range check;ParseEpochSeconds(timestamp.cc:190) accepts arbitrarily long digit strings (isfiniteonly). A value like1e300or99999999999999999999yields an unspecified instant instead of an error.runtime/src/core/timestamp.cc:54-70,225-253: extreme instants format to 5+ digit / negative years via%04d, which no conformant peer (ParseDateTimerequires 4-digit years) can round-trip — silent data corruption.Suggested fix
Range-check before the multiply/cast in all three spots: reject (return
Error::Serialization) when the epoch value, scaled to milliseconds, would fall outside the representable instant range, and bound the accepted year inFormat/Decompose. A shared "checked seconds/millis → Timestamp" helper would cover the CBOR and JSON paths together.