Closed Bug 2042758 Opened 1 month ago Closed 1 month ago

Reachable assertion failure [@ NudgeToCalendarUnit] (startEpochNs != endEpochNs) via Temporal.Duration.prototype.total across a 24h time-zone transition

Categories

(Core :: JavaScript: Standard Library, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2029455

People

(Reporter: bugmon, Unassigned)

Details

(4 keywords)

Attachments

(3 files)

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

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

  1. 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]").
  2. Call Temporal.Duration.prototype.total with unit:'days' and that ZonedDateTime as relativeTo, on a small duration (e.g. {days:-1}).
  3. Duration_total -> DifferenceZonedDateTimeWithTotal (ZonedDateTime.cpp:860) -> TotalRelativeDuration (Duration.cpp:2861) -> NudgeToCalendarUnit (Duration.cpp).
  4. NudgeToCalendarUnit -> ComputeNudgeWindow builds the two adjacent day-endpoints 2011-12-31T12:00 and 2011-12-30T12:00 and converts each via GetEpochNanosecondsFor(..., Compatible).
  5. Because the local day 2011-12-30 was skipped by the dateline jump, both endpoints disambiguate forward to the identical epoch instant (1325282400000000000 ns).
  6. 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

  1. Build the SpiderMonkey debug shell (--enable-debug, assertions enabled): /firefox/obj-js-debug/dist/bin/js
  2. Run the testcase under --fuzzing-safe: js --fuzzing-safe testcase.js
  3. 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
Attached file testcase.js
Attached patch fix.patchSplinter Review
Attached file crash_stack.txt
Group: core-security → javascript-core-security

Duplicate of bug 2029455. (The test case is exactly the same.)

Status: UNCONFIRMED → RESOLVED
Closed: 1 month ago
Duplicate of bug: 2029455
Resolution: --- → DUPLICATE
Group: javascript-core-security
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: