# Assertion failure: ISODateTimeWithinLimits(endDateTime), at Duration.cpp:2226 via ZonedDateTime.prototype.until
Categories
(Core :: JavaScript Engine, defect, P1)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | wontfix |
| firefox149 | --- | wontfix |
| firefox150 | --- | wontfix |
| firefox151 | --- | fixed |
People
(Reporter: bugmon, Assigned: anba)
References
(Blocks 1 open bug)
Details
(4 keywords, Whiteboard: [prefs-checked][adv-main151-])
Attachments
(4 files, 1 external file)
Assertion failure: ISODateTimeWithinLimits(endDateTime), at Duration.cpp:2226 via ZonedDateTime.prototype.until
ZonedDateTime.prototype.until triggers a debug-build assertion failure when computing the difference between the maximum representable instant (8.64×10²¹ ns) and the minimum representable instant (-8.64×10²¹ ns) with day-granularity rounding under UTC. The assertion in ComputeNudgeWindow assumes that combining a date returned by CalendarDateAdd with the source ISODateTime's time component always yields a value within ISODateTimeWithinLimits.
This assumption fails at the lower boundary: ISODateTimeWithinLimits requires that when the date component equals ISODate::min() (-271821-04-19), the time component must be strictly past midnight. CalendarDateAdd only validates ISODateWithinLimits (date alone), so it happily returns ISODate::min(). When the source ZonedDateTime sits exactly at the maximum instant under UTC, its time component is exactly midnight, and combining the two violates the date-time invariant.
In release builds the MOZ_ASSERT is compiled out and the invalid endDateTime flows into GetEpochNanosecondsFor → GetPossibleEpochNanoseconds, which contains a deliberate runtime check (TimeZone.cpp:924, marked with a reference to tc39/proposal-temporal#3014) that throws RangeError. The user-observable release behavior is therefore spec-correct; this finding is a debug-only crash indicating that lines 2201 and 2226 should be fallible RangeError checks rather than assertions.
Build Info
- Branch: main
- Revision: fb4975f0b42423c8e47d3d72c7a1e5d2f17b5129
- Timestamp: 2026-04-01T12:45:07+00:00
Affected Code
File: js/src/builtin/temporal/Duration.cpp, line 2217-2226
// Step 9.
ISODate end;
if (!CalendarDateAdd(cx, calendar, isoDateTime.date, endDuration,
TemporalOverflow::Constrain, &end)) {
return false;
}
// Step 10.
auto endDateTime = ISODateTime{end, isoDateTime.time};
MOZ_ASSERT(ISODateTimeWithinLimits(endDateTime)); // <-- fires when end == ISODate::min() && isoDateTime.time == midnight
File: js/src/builtin/temporal/PlainDateTime.cpp, line 92-99
if (year < 0) {
if (isoDateTime.date != min) {
return isoDateTime.date > min;
}
// Needs to be past midnight.
return isoDateTime.time != Time{};
}
File: js/src/builtin/temporal/Duration.cpp, line 2151-2175
} else {
// Step 4.a.
MOZ_ASSERT(unit == TemporalUnit::Day);
// Step 4.b.
int64_t days = RoundNumberToIncrement(duration.date.days, increment,
TemporalRoundingMode::Trunc);
// Steps 4.c-d.
if (!additionalShift) {
r1 = days;
} else {
r1 = days + int64_t(increment.value()) * sign;
}
// Step 4.e.
r2 = r1 + int64_t(increment.value()) * sign; // <-- with sign=-1, increment=1: r2 = days - 1
// Step 4.f.
startDuration = {duration.date.years, duration.date.months,
duration.date.weeks, r1};
// Step 4.g.
endDuration = {duration.date.years, duration.date.months,
duration.date.weeks, r2}; // <-- one extra negative day
File: js/src/builtin/temporal/Duration.cpp, line 2786-2799
bool irregularLengthUnit = (smallestUnit < TemporalUnit::Day) ||
(timeZone && smallestUnit == TemporalUnit::Day);
// Step 4. (Not applicable in our implementation.)
// Steps 5-7.
DurationNudge nudge;
if (irregularLengthUnit) {
// Step 5.a.
if (!NudgeToCalendarUnit(cx, duration, originEpochNs, destEpochNs,
isoDateTime, timeZone, calendar, increment,
smallestUnit, roundingMode, &nudge)) {
return false;
}
File: js/src/builtin/temporal/ZonedDateTime.cpp, line 813-823
// Step 4.
ISODateTime dateTime;
if (!GetISODateTimeFor(cx, timeZone, ns1, &dateTime)) {
return false;
}
// Step 5.
return RoundRelativeDuration(
cx, difference, ns1, ns2, dateTime, timeZone, calendar,
settings.largestUnit, settings.roundingIncrement, settings.smallestUnit,
settings.roundingMode, result);
File: js/src/builtin/temporal/TimeZone.cpp, line 920-928
bool js::temporal::GetPossibleEpochNanoseconds(
JSContext* cx, Handle<TimeZoneValue> timeZone,
const ISODateTime& isoDateTime, PossibleEpochNanoseconds* result) {
// TODO: https://github.com/tc39/proposal-temporal/pull/3014
if (!ISODateTimeWithinLimits(isoDateTime)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_PLAIN_DATE_TIME_INVALID);
return false;
} // <-- deliberate downstream catch; saves release builds
ComputeNudgeWindow constructs endDateTime by combining the result of CalendarDateAdd (which only enforces ISODateWithinLimits — the date alone) with the source isoDateTime's time component, then asserts ISODateTimeWithinLimits on the combination. The date-time limit is stricter than the date limit at ISODate::min(): it requires the time to be strictly past midnight. When the rounding-window endpoint lands exactly on ISODate::min() and the source time happens to be midnight (which occurs when z1 is the maximum instant under UTC), the assertion fires. The vulnerable call originates in ZonedDateTime.cpp:819 (DifferenceZonedDateTimeWithRounding) which passes the dateTime with potentially-midnight time. This path is ZonedDateTime-specific because RoundRelativeDuration only routes Day-rounding through NudgeToCalendarUnit when timeZone is non-null (Duration.cpp:2787). Release builds are protected by a deliberate, spec-PR-tracked runtime check in GetPossibleEpochNanoseconds.
Exploit Chain
- Construct a ZonedDateTime at the maximum representable instant (8640000000000000000000n) under UTC, whose ISO date-time is +275760-09-13T00:00:00 — note the time component is exactly midnight.
- Construct a second ZonedDateTime at the minimum representable instant (-8640000000000000000000n) under UTC.
- Call z1.until(z2, {largestUnit: 'year', smallestUnit: 'day', roundingMode: 'ceil'}). The combination of largestUnit≥day and smallestUnit==day with a non-null timeZone routes through DifferenceZonedDateTimeWithRounding (ZonedDateTime.cpp:819) → RoundRelativeDuration → NudgeToCalendarUnit → ComputeNudgeWindow.
- DifferenceZonedDateTime computes the unrounded duration as approximately {-547581y, -4m, 0w, -23d}. In ComputeNudgeWindow with unit==Day, sign=-1, increment=1: r1=-23 and r2=-24, giving endDuration={-547581, -4, 0, -24}.
- CalendarDateAdd(+275760-09-13, {-547581y, -4m, 0w, -24d}) yields -271821-04-19 = ISODate::min(). CalendarDateAdd succeeds because ISODateWithinLimits accepts the bare minimum date.
- Line 2225 combines ISODate::min() with isoDateTime.time (midnight from z1). ISODateTimeWithinLimits(PlainDateTime.cpp:98) returns false because at the minimum date, time must be strictly past midnight. MOZ_ASSERT at line 2226 fires.
- In release builds the assert is compiled out; endDateTime flows to GetEpochNanosecondsFor → GetPossibleEpochNanoseconds (TimeZone.cpp:924), which has a deliberate runtime check that throws RangeError. No memory corruption or information disclosure occurs.
Steps to Reproduce
- Build SpiderMonkey with --enable-debug.
- Run the JS shell with: const z1 = new Temporal.ZonedDateTime(8640000000000000000000n, "UTC"); const z2 = new Temporal.ZonedDateTime(-8640000000000000000000n, "UTC"); z1.until(z2, {largestUnit: "year", smallestUnit: "day", roundingMode: "ceil"});
- Observe: Assertion failure: ISODateTimeWithinLimits(endDateTime), at js/src/builtin/temporal/Duration.cpp:2226
Security Impact
- Severity: Low
- Attacker capability: Crash a debug build of the JS engine. No memory corruption, no out-of-bounds access, no information disclosure. Release builds throw RangeError correctly via a deliberate downstream check (TimeZone.cpp:924, tracking tc39/proposal-temporal#3014). The fix is to replace the MOZ_ASSERT at Duration.cpp:2201 and 2226 with fallible RangeError checks.
- Preconditions: Debug build of SpiderMonkey. JavaScript execution capability. No special prefs, flags, or user interaction required — pure content-script Temporal API calls at the boundary of the representable date range.
ASAN Report
[40392] Assertion failure: ISODateTimeWithinLimits(endDateTime), at /firefox/js/src/builtin/temporal/Duration.cpp:2226
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==40392==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5585d545cd0e bp 0x7ffc8fe84120 sp 0x7ffc8fe83fb0 T40392)
==40392==The signal is caused by a WRITE memory access.
==40392==Hint: address points to the zero page.
#0 0x5585d545cd0e in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:235:3
#1 0x5585d545cd0e in ComputeNudgeWindow(JSContext*, js::temporal::InternalDuration const&, js::temporal::EpochNanoseconds const&, js::temporal::ISODateTime const&, JS::Handle<js::temporal::TimeZoneValue>, JS::Handle<js::temporal::CalendarValue>, js::temporal::Increment, js::temporal::TemporalUnit, bool, NudgeWindow*) /firefox/js/src/builtin/temporal/Duration.cpp:2226:3
#2 0x5585d54545e2 in NudgeToCalendarUnit(JSContext*, js::temporal::InternalDuration const&, js::temporal::EpochNanoseconds const&, js::temporal::EpochNanoseconds const&, js::temporal::ISODateTime const&, JS::Handle<js::temporal::TimeZoneValue>, JS::Handle<js::temporal::CalendarValue>, js::temporal::Increment, js::temporal::TemporalUnit, js::temporal::TemporalRoundingMode, DurationNudge*) /firefox/js/src/builtin/temporal/Duration.cpp:2279:8
#3 0x5585d5452549 in js::temporal::RoundRelativeDuration(JSContext*, js::temporal::InternalDuration const&, js::temporal::EpochNanoseconds const&, js::temporal::EpochNanoseconds const&, js::temporal::ISODateTime const&, JS::Handle<js::temporal::TimeZoneValue>, JS::Handle<js::temporal::CalendarValue>, js::temporal::TemporalUnit, js::temporal::Increment, js::temporal::TemporalUnit, js::temporal::TemporalRoundingMode, js::temporal::InternalDuration*) /firefox/js/src/builtin/temporal/Duration.cpp:2795:10
#4 0x5585d54b0774 in js::temporal::DifferenceZonedDateTimeWithRounding(JSContext*, JS::Handle<js::temporal::ZonedDateTime>, js::temporal::EpochNanoseconds const&, js::temporal::DifferenceSettings const&, js::temporal::InternalDuration*) /firefox/js/src/builtin/temporal/ZonedDateTime.cpp:819:10
#5 0x5585d54b70b7 in DifferenceTemporalZonedDateTime(JSContext*, js::temporal::TemporalDifference, JS::CallArgs const&) /firefox/js/src/builtin/temporal/ZonedDateTime.cpp:973:8
#6 0x5585d54b3715 in ZonedDateTime_until(JSContext*, JS::CallArgs const&) /firefox/js/src/builtin/temporal/ZonedDateTime.cpp:2411:10
#7 0x5585d54b3715 in bool JS::CallNonGenericMethod<&IsZonedDateTime(JS::Handle<JS::Value>), &ZonedDateTime_until(JSContext*, JS::CallArgs const&)>(JSContext*, JS::CallArgs const&) /firefox/obj-js-debug/dist/include/js/CallNonGenericMethod.h:103:12
#8 0x5585d54b3715 in ZonedDateTime_until(JSContext*, unsigned int, JS::Value*) /firefox/js/src/builtin/temporal/ZonedDateTime.cpp:2420:10
#9 0x5585d4bd7ef4 in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:490:13
#10 0x5585d4bb32a5 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:586:12
#11 0x5585d4bc304c in js::CallFromStack(JSContext*, JS::CallArgs const&, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:658:10
#12 0x5585d4bc304c in js::Interpret(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:3272:16
#13 0x5585d4bb291a in js::RunScript(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:460:13
#14 0x5585d4bb5780 in js::ExecuteKernel(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, js::AbstractFramePtr, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:850:10
#15 0x5585d4bb5bde in js::Execute(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:880:10
#16 0x5585d4e6f111 in ExecuteScript(JSContext*, JS::Handle<JSObject*>, JS::Handle<JSScript*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:548:10
#17 0x5585d4e6f2f3 in JS_ExecuteScript(JSContext*, JS::Handle<JSScript*>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:572:10
#18 0x5585d37b751d in RunFile(JSContext*, char const*, _IO_FILE*, CompileUtf8, bool, bool) /firefox/js/src/shell/js.cpp:1388:10
#19 0x5585d37b6a09 in Process(JSContext*, char const*, bool, FileKind) /firefox/js/src/shell/js.cpp
#20 0x5585d377252c in ProcessArgs(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12130:10
#21 0x5585d377252c in Shell(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12383:12
#22 0x5585d37693f0 in main /firefox/js/src/shell/js.cpp:12789:12
#23 0x7e04155cc1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#24 0x7e04155cc28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#25 0x5585d3730bc8 in _start (/firefox/obj-js-debug/dist/bin/js+0x2690bc8) (BuildId: 462b280863c023beb7967d5cc845269c)
SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:235:3 in MOZ_CrashSequence(void*, long)
==40392==ABORTING
| Reporter | ||
Comment 1•7 months ago
|
||
| Reporter | ||
Comment 2•7 months ago
|
||
| Reporter | ||
Comment 3•7 months ago
|
||
| Reporter | ||
Comment 4•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
| Assignee | ||
Comment 6•7 months ago
|
||
Similar to bug 2028729, this is just an overzealous assertion, so also not sec-sensitive.
| Assignee | ||
Comment 7•7 months ago
|
||
Relax the assertion for endDateTime similar to changes needed for bug 2028729.
The test case matches the one for bug 2028729, except using ZonedDateTime
instead of PlainDate.
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 9•7 months ago
|
||
Updated•7 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•2 months ago
|
Description
•