Reachable assertion failure [@ NudgeToCalendarUnit] (startEpochNs != endEpochNs) via Temporal.Duration.prototype.total across a 24h time-zone transition
Categories
(Core :: JavaScript: Standard Library, defect)
Tracking
()
People
(Reporter: bugmon, Unassigned)
Details
(4 keywords)
Attachments
(3 files)
|
148 bytes,
text/plain
|
Details | |
|
3.55 KB,
patch
|
Details | Diff | Splinter Review | |
|
987 bytes,
text/plain
|
Details |
SpiderMonkey's Temporal duration-rounding machinery asserts that the two endpoints of a calendar-unit "nudge window" map to distinct epoch instants: MOZ_ASSERT(startEpochNs != endEpochNs) in NudgeToCalendarUnit (js/src/builtin/temporal/Duration.cpp:2311). This invariant is assumed because two adjacent calendar-unit boundaries (e.g. the day before and after an anchor) should never be the same point in time. However, when the relevant time zone skips an entire wall-clock day via a >=24h forward UTC-offset transition, both endpoints disambiguate forward to the SAME instant, breaking the invariant.
The assertion is reachable from web-exposed JavaScript via Temporal.Duration.prototype.total with unit:'days' and a ZonedDateTime relativeTo whose time zone is Pacific/Apia (which skipped 2011-12-30 entirely during the 2011 international-dateline jump). ComputeNudgeWindow builds start = 2011-12-31T12:00 and end = 2011-12-30T12:00 around the 2012-01-01T12:00 anchor and converts each through GetEpochNanosecondsFor(..., TemporalDisambiguation::Compatible). Because 2011-12-30 has no valid local time in Apia, both resolve to the same epoch (1325282400000000000 ns), so startEpochNs == endEpochNs and the assertion fires (debug MOZ_ASSERT -> MOZ_CrashSequence, reported by UBSan as a SEGV/abort).
This bug is part of the same spec-defect assertion cluster as the recently-relaxed sibling assertions (bugs 2028729 / 2028880 / 2036259); the fix author relaxed several but left this monotonicity assertion intact. The assigned analysis file was builtin/temporal/PlainDate.cpp, which I verified is byte-identical to upstream and whose own paths are robustly guarded; the genuine reachable defect lives in the shared duration-rounding code in Duration.cpp.
Build Info
- Branch: main
- Revision: 1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1
- Timestamp: 2026-05-21T16:50:48+00:00
Affected Code
File: js/src/builtin/temporal/Duration.cpp, line 2306-2320
// Steps 7-12.
const auto& [r1, r2, startEpochNs, endEpochNs, startDuration, endDuration] =
nudgeWindow;
// Step 13.
MOZ_ASSERT(startEpochNs != endEpochNs); // <-- fires: both endpoints collapse
MOZ_ASSERT_IF(sign > 0,
startEpochNs <= destEpochNs && destEpochNs <= endEpochNs);
MOZ_ASSERT_IF(sign < 0,
endEpochNs <= destEpochNs && destEpochNs <= startEpochNs);
// Step 14.
auto numerator = (destEpochNs - startEpochNs).toNanoseconds();
auto denominator = (endEpochNs - startEpochNs).toNanoseconds();
MOZ_ASSERT(denominator != Int128{0}); // also violated: denominator == 0
File: js/src/builtin/temporal/Duration.cpp, line 2209-2243
} else {
// Step 8.d. (startDateTime = anchor +/- one calendar unit, here -1 day)
if (!GetEpochNanosecondsFor(cx, timeZone, startDateTime,
TemporalDisambiguation::Compatible,
&startEpochNs)) {
return false;
}
}
}
// ...
if (!timeZone) {
endEpochNs = GetUTCEpochNanoseconds(endDateTime);
} else {
// Step 12.a. (endDateTime = the other adjacent calendar unit)
if (!GetEpochNanosecondsFor(cx, timeZone, endDateTime,
TemporalDisambiguation::Compatible,
&endEpochNs)) {
return false;
}
}
// For Pacific/Apia both 2011-12-31T12:00 and 2011-12-30T12:00 disambiguate
// forward to the SAME instant -> startEpochNs == endEpochNs.
The invariant assertion fires when the nudge-window endpoints collapse to the same instant. The endpoints are produced by ComputeNudgeWindow, which converts two adjacent calendar-unit ISODateTimes to epoch nanoseconds with Compatible disambiguation; a >=24h-skip time zone makes both convert to the same epoch.
Exploit Chain
- Create a ZonedDateTime in a time zone that skips a whole wall-clock day via a >=24h forward offset transition: Temporal.ZonedDateTime.from("2012-01-01T12:00:00[Pacific/Apia]").
- Call Temporal.Duration.prototype.total with unit:'days' and that ZonedDateTime as relativeTo, on a small duration (e.g. {days:-1}).
- Duration_total -> DifferenceZonedDateTimeWithTotal (ZonedDateTime.cpp:860) -> TotalRelativeDuration (Duration.cpp:2861) -> NudgeToCalendarUnit (Duration.cpp).
- NudgeToCalendarUnit -> ComputeNudgeWindow builds the two adjacent day-endpoints 2011-12-31T12:00 and 2011-12-30T12:00 and converts each via GetEpochNanosecondsFor(..., Compatible).
- Because the local day 2011-12-30 was skipped by the dateline jump, both endpoints disambiguate forward to the identical epoch instant (1325282400000000000 ns).
- Back in NudgeToCalendarUnit, startEpochNs == endEpochNs, violating MOZ_ASSERT(startEpochNs != endEpochNs) at Duration.cpp:2311 (and the subsequent denominator != 0 assertion); in a debug build this aborts via MOZ_CrashSequence (UBSan SEGV).
Steps to Reproduce
- Build the SpiderMonkey debug shell (--enable-debug, assertions enabled): /firefox/obj-js-debug/dist/bin/js
- Run the testcase under --fuzzing-safe: js --fuzzing-safe testcase.js
- Observe: 'Assertion failure: startEpochNs != endEpochNs, at js/src/builtin/temporal/Duration.cpp:2311' followed by a UBSan SEGV/abort in MOZ_CrashSequence.
Security Impact
- Severity: Low
- Attacker capability: Web-exposed JavaScript (no special privileges) can deterministically trigger a reachable engine-invariant violation in the Temporal implementation. In assertion-enabled (debug/fuzzing) builds this aborts the process (MOZ_ASSERT -> MOZ_CrashSequence, observed by UBSan as a SEGV). In release builds MOZ_ASSERT is compiled out and the engine does not crash (the equal-endpoint/zero-denominator case is tolerated at runtime), so there is no observed memory corruption or information disclosure; the practical impact is a correctness/invariant violation and a crash limited to assertion-enabled builds.
- Preconditions: None beyond standard Temporal availability (enabled by default; reachable in --fuzzing-safe). Requires a time zone whose data includes a >=24h forward offset transition that skips a whole wall-clock day (e.g. Pacific/Apia, 2011-12-30).
ASAN Report
Assertion failure: startEpochNs != endEpochNs, at js/src/builtin/temporal/Duration.cpp:2311
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000
#0 MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3
#1 NudgeToCalendarUnit(...) js/src/builtin/temporal/Duration.cpp:2311:3
#2 js::temporal::TotalRelativeDuration(...) js/src/builtin/temporal/Duration.cpp:2861:8
#3 js::temporal::DifferenceZonedDateTimeWithTotal(...) js/src/builtin/temporal/ZonedDateTime.cpp:860:10
#4 Duration_total(JSContext*, JS::CallArgs const&) js/src/builtin/temporal/Duration.cpp:3933:10
#5 Duration_total(JSContext*, unsigned int, JS::Value*) js/src/builtin/temporal/Duration.cpp:4008:10
#6 CallJSNative(...) js/src/vm/Interpreter.cpp:498:13
SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3 in MOZ_CrashSequence(void*, long)
==ABORTING
| Reporter | ||
Comment 1•1 month ago
|
||
| Reporter | ||
Comment 2•1 month ago
|
||
| Reporter | ||
Comment 3•1 month ago
|
||
Updated•1 month ago
|
Comment 5•1 month ago
|
||
Duplicate of bug 2029455. (The test case is exactly the same.)
Updated•1 month ago
|
Description
•