Assertion failure: startEpochNs != endEpochNs in NudgeToCalendarUnit with 24h dateline gap (Pacific/Apia)
Categories
(Core :: JavaScript: Standard Library, defect, P3)
Tracking
()
People
(Reporter: bugmon, Unassigned)
References
(Blocks 1 open bug)
Details
(Keywords: ai-involved, assertion, testcase)
Attachments
(3 files)
|
1.84 KB,
patch
|
Details | Diff | Splinter Review | |
|
4.50 KB,
text/plain
|
Details | |
|
148 bytes,
text/plain
|
Details |
When Temporal duration rounding/totaling is performed relative to a ZonedDateTime in a zone that underwent a 24-hour forward dateline transition (e.g. Pacific/Apia, which skipped 2011-12-30 entirely when moving from UTC-11 to UTC+13), the nudge-window endpoints computed by NudgeToCalendarUnit can collapse to a single instant. DisambiguatePossibleEpochNanoseconds handles the gap by adding (offsetAfter - offsetBefore) = 86400000000000 ns and resolving the resulting later date-time, so the skipped day 2011-12-30T<t> and the real day 2011-12-31T<t> both resolve to the same epoch nanoseconds. NudgeToCalendarUnit then computes startEpochNs == endEpochNs and trips MOZ_ASSERT(startEpochNs != endEpochNs).
In release builds the assertion is compiled out; numerator and denominator both become 0, FractionToDouble short-circuits on numerator==0 and returns 0, and the API returns a silently incorrect Temporal result (e.g. Duration.from({days:-1}).total({unit:'days', relativeTo:zdt}) returns 0 instead of -1). No out-of-bounds access, division-by-zero trap, or other memory-safety issue occurs. This is a debug-only crash / release correctness bug in the Temporal implementation, reachable from content JS via Duration.prototype.total, Duration.prototype.round, and ZonedDateTime.prototype.until/since with calendar smallestUnit.
Build Info
- Branch: main
- Revision: 6164ea4bacaeaed1f617c11911df7fc32f2e6ec2
- Timestamp: 2026-04-02T19:36:24+00:00
Affected Code
File: js/src/builtin/temporal/TimeZone.cpp, line 1054-1116
// Step 14.
int64_t nanoseconds = offsetAfter - offsetBefore;
// Step 15.
MOZ_ASSERT(std::abs(nanoseconds) <= ToNanoseconds(TemporalUnit::Day));
...
// Step 17.
MOZ_ASSERT(disambiguation == TemporalDisambiguation::Compatible ||
disambiguation == TemporalDisambiguation::Later);
// Steps 18-19.
auto laterTime = ::AddTime(isoDateTime.time, nanoseconds);
MOZ_ASSERT(std::abs(laterTime.days) <= 1,
"adding nanoseconds is at most one day");
// Step 20.
auto laterDate =
BalanceISODate(isoDateTime.date, static_cast<int32_t>(laterTime.days));
// Step 21.
auto laterDateTime = ISODateTime{laterDate, laterTime.time};
// Step 22.
PossibleEpochNanoseconds laterEpochNs;
if (!GetPossibleEpochNanoseconds(cx, timeZone, laterDateTime,
&laterEpochNs)) {
return false;
}
// Steps 23-24.
MOZ_ASSERT(!laterEpochNs.empty());
// Step 25.
*result = laterEpochNs.back();
return true;
File: js/src/builtin/temporal/Duration.cpp, line 2303-2317
const auto& [r1, r2, startEpochNs, endEpochNs, startDuration, endDuration] =
nudgeWindow;
// Step 13.
MOZ_ASSERT(startEpochNs != endEpochNs);
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});
MOZ_ASSERT(numerator.abs() <= denominator.abs());
DisambiguatePossibleEpochNanoseconds resolves a wall-clock time that falls in a gap by shifting forward by (offsetAfter - offsetBefore). For the Pacific/Apia 2011-12-29/31 dateline change this shift is exactly 24 hours, so 2011-12-30T12:00 (gap) and 2011-12-31T12:00 (real) both resolve to the same epoch instant. ComputeNudgeWindow in NudgeToCalendarUnit picks r1 = -1 day (2011-12-31T12:00) and r2 = -2 days (2011-12-30T12:00) relative to 2012-01-01T12:00[Pacific/Apia]; both map to identical epoch nanoseconds, so startEpochNs == endEpochNs and the MOZ_ASSERT fires. In release builds denominator becomes 0 but numerator is also forced to 0 by the |numerator.abs() <= denominator.abs()| structural invariant, so FractionToDouble returns 0 without dividing by zero and the API returns a wrong-but-valid number.
Exploit Chain
- Attacker-controlled script constructs a ZonedDateTime shortly after a 24-hour forward dateline transition, e.g. Temporal.ZonedDateTime.from("2012-01-01T12:00:00[Pacific/Apia]").
- Script calls Temporal.Duration.from({days:-1}).total({unit:'days', relativeTo:zdt}) (or .round / ZonedDateTime.until with a calendar smallestUnit).
- Duration_total -> DifferenceZonedDateTimeWithTotal -> TotalRelativeDuration -> NudgeToCalendarUnit calls ComputeNudgeWindow, which adds r1=-1 and r2=-2 days to the relative date and resolves each via GetEpochNanosecondsFor(..., Compatible).
- For r2 the target 2011-12-30T12:00 falls in the 24h gap; DisambiguatePossibleEpochNanoseconds (TimeZone.cpp) shifts it forward 24h to 2011-12-31T12:00, the same instant as r1.
- startEpochNs == endEpochNs; MOZ_ASSERT(startEpochNs != endEpochNs) fires in debug builds (controlled crash).
- In release builds the assert is absent; numerator = denominator = 0, FractionToDouble returns 0, and the user receives an incorrect total/rounded duration. No memory corruption.
Steps to Reproduce
- Build SpiderMonkey debug shell (--enable-debug).
- Save testcase: let zdt = Temporal.ZonedDateTime.from("2012-01-01T12:00:00[Pacific/Apia]"); Temporal.Duration.from({days:-1}).total({unit:'days', relativeTo:zdt});
- Run: obj-js-debug/dist/bin/js --fuzzing-safe testcase.js
- Observe: Assertion failure: startEpochNs != endEpochNs, at js/src/builtin/temporal/Duration.cpp:2307.
Security Impact
- Severity: Low
- Attacker capability: Content JavaScript can deterministically crash a debug/assertion-enabled SpiderMonkey build (DoS for developers/fuzzers). In release builds the same input causes Temporal Duration.total/round and ZonedDateTime.until/since to return numerically incorrect results around 24-hour dateline transitions (e.g. total of -1 day reported as 0; 24h difference rounded to -P2D), but does not cause memory corruption, undefined behaviour, or information disclosure.
- Preconditions: None beyond the ability to run JavaScript that uses the Temporal API with an IANA time zone containing a 24-hour forward transition (Pacific/Apia 2011-12-30, Pacific/Kiritimati 1994-12-31, etc.). Crash requires a debug (MOZ_ASSERT-enabled) build; release builds only miscompute.
ASAN Report
[1085] Assertion failure: startEpochNs != endEpochNs, at /firefox/js/src/builtin/temporal/Duration.cpp:2307
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==1085==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x591124951f96 bp 0x7ffcea56e3a0 sp 0x7ffcea56e2b0 T1085)
==1085==The signal is caused by a WRITE memory access.
==1085==Hint: address points to the zero page.
#0 0x591124951f96 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:235:3
#1 0x591124951f96 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:2307:3
#2 0x591124952339 in js::temporal::TotalRelativeDuration(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, double*) /firefox/js/src/builtin/temporal/Duration.cpp:2853:8
#3 0x5911249ae3a3 in js::temporal::DifferenceZonedDateTimeWithTotal(JSContext*, JS::Handle<js::temporal::ZonedDateTime>, js::temporal::EpochNanoseconds const&, js::temporal::TemporalUnit, double*) /firefox/js/src/builtin/temporal/ZonedDateTime.cpp:862:10
#4 0x59112495f7c7 in Duration_total(JSContext*, JS::CallArgs const&) /firefox/js/src/builtin/temporal/Duration.cpp:3919:10
#5 0x59112495cd5c in bool JS::CallNonGenericMethod<&IsDuration(JS::Handle<JS::Value>), &Duration_total(JSContext*, JS::CallArgs const&)>(JSContext*, JS::CallArgs const&) /firefox/obj-js-debug/dist/include/js/CallNonGenericMethod.h:103:12
#6 0x59112495cd5c in Duration_total(JSContext*, unsigned int, JS::Value*) /firefox/js/src/builtin/temporal/Duration.cpp:3988:10
#7 0x5911240d5094 in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:490:13
#8 0x5911240b0445 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:586:12
#9 0x5911240c01ec in js::CallFromStack(JSContext*, JS::CallArgs const&, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:658:10
#10 0x5911240c01ec in js::Interpret(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:3272:16
#11 0x5911240afaba in js::RunScript(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:460:13
#12 0x5911240b2920 in js::ExecuteKernel(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, js::AbstractFramePtr, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:850:10
#13 0x5911240b2d7e in js::Execute(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:880:10
#14 0x59112436c0b1 in ExecuteScript(JSContext*, JS::Handle<JSObject*>, JS::Handle<JSScript*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:548:10
#15 0x59112436c293 in JS_ExecuteScript(JSContext*, JS::Handle<JSScript*>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:572:10
#16 0x591122cb48ad in RunFile(JSContext*, char const*, _IO_FILE*, CompileUtf8, bool, bool) /firefox/js/src/shell/js.cpp:1388:10
#17 0x591122cb3d99 in Process(JSContext*, char const*, bool, FileKind) /firefox/js/src/shell/js.cpp
#18 0x591122c6f8bc in ProcessArgs(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12130:10
#19 0x591122c6f8bc in Shell(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12383:12
#20 0x591122c66780 in main /firefox/js/src/shell/js.cpp:12789:12
#21 0x7e74112f91c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9)
#22 0x7e74112f928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a)
#23 0x591122c2df58 in _start (/firefox/obj-js-debug/dist/bin/js+0x2685f58)
SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:235:3 in MOZ_CrashSequence(void*, long)
==1085==ABORTING
Updated•1 month ago
|
| Reporter | ||
Comment 1•1 month ago
|
||
| Reporter | ||
Comment 2•1 month ago
|
||
| Reporter | ||
Comment 3•1 month ago
|
||
Updated•1 month ago
|
Updated•1 month ago
|
Comment 5•1 month ago
|
||
This is another bug in the Temporal spec.
Updated•21 days ago
|
Updated•21 days ago
|
Updated•21 days ago
|
Comment 6•19 days ago
|
||
Description
•