Closed Bug 2028733 Opened 7 months ago Closed 7 months ago

# Assertion failure: ISODateTimeWithinLimits(endDateTime), at Duration.cpp:2226 via ZonedDateTime.prototype.until

Categories

(Core :: JavaScript Engine, defect, P1)

defect

Tracking

()

RESOLVED FIXED
151 Branch
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

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

  1. 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.
  2. Construct a second ZonedDateTime at the minimum representable instant (-8640000000000000000000n) under UTC.
  3. 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.
  4. 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}.
  5. CalendarDateAdd(+275760-09-13, {-547581y, -4m, 0w, -24d}) yields -271821-04-19 = ISODate::min(). CalendarDateAdd succeeds because ISODateWithinLimits accepts the bare minimum date.
  6. 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.
  7. 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

  1. Build SpiderMonkey with --enable-debug.
  2. 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"});
  3. 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
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Attached file analysis.json —
Group: core-security → javascript-core-security

Similar to bug 2028729, this is just an overzealous assertion, so also not sec-sensitive.

Attached file (secure) —

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.

Assignee: nobody → andrebargull
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Whiteboard: [prefs-checked]
Blocks: sm-security
Severity: -- → S3
Priority: -- → P1
Pushed by andre.bargull@gmail.com: https://github.com/mozilla-firefox/firefox/commit/d032fbca7f1a https://hg.mozilla.org/integration/autoland/rev/2bf39d903df2 Relax assertions for duration rounding with ZonedDateTime. r=spidermonkey-reviewers,jandem
Group: javascript-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 7 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch
QA Whiteboard: [sec] [qa-triage-done-c152/b151]
Whiteboard: [prefs-checked] → [prefs-checked][adv-main151-][adv-esr115.36-][adv-esr140.11-]
Whiteboard: [prefs-checked][adv-main151-][adv-esr115.36-][adv-esr140.11-] → [prefs-checked][adv-main151-]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: