Closed Bug 2028729 Opened 7 months ago Closed 7 months ago

# Assertion failure: IsValidLocalNanoseconds(originEpochNs) in [@ js::temporal::RoundRelativeDuration] via PlainDate.until at minimum date boundary

Categories

(Core :: JavaScript: Standard Library, 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: IsValidLocalNanoseconds(originEpochNs) in [@ js::temporal::RoundRelativeDuration] via PlainDate.until at minimum date boundary

DifferenceTemporalPlainDate constructs an ISODateTime by pairing the receiver's ISODate with a default-initialized (midnight) Time component before passing it to RoundRelativeDuration. The minimum valid PlainDate is -271821-04-19 (inclusive bound per ISODateWithinLimits), but the minimum valid ISODateTime requires the time on that specific date to be strictly past midnight (per ISODateTimeWithinLimits). Constructing ISODateTime{ISODate::min(), Time{}} therefore produces a value that lies exactly on the exclusive lower bound checked by IsValidLocalNanoseconds, tripping the MOZ_ASSERT_IF at Duration.cpp:2779 (and would also trip the ISODateTimeWithinLimits assertion at line 2782).

This is a debug-only assertion failure with no observable security impact in release builds. The IsValidLocalNanoseconds predicate is defined inside #ifdef DEBUG, and differential testing against non-boundary dates confirms that the release-build computation produces correct rounding results across multiple roundingMode/roundingIncrement/largestUnit/smallestUnit configurations. The boundary epoch-nanoseconds value does not cause arithmetic overflow or incorrect output in NudgeToDayOrTime or NudgeToCalendarUnit. The fix is either to relax the assertion to use non-strict inequality at the lower bound, or to special-case the minimum date in DifferenceTemporalPlainDate.

Build Info

Affected Code

File: js/src/builtin/temporal/PlainDate.cpp, line 640-660

  // Step 8.
  if (settings.smallestUnit != TemporalUnit::Day ||
      settings.roundingIncrement != Increment{1}) {
    // Step 8.a.
    auto isoDateTime = ISODateTime{temporalDate.date(), {}};  // BUG: midnight + ISODate::min() is below ISODateTimeWithinLimits

    // Step 8.b.
    auto originEpochNs = GetUTCEpochNanoseconds(isoDateTime);

    // Step 8.c.
    auto isoDateTimeOther = ISODateTime{other.date(), {}};

    // Step 8.d.
    auto destEpochNs = GetUTCEpochNanoseconds(isoDateTimeOther);

    // Step 8.e.
    Rooted<TimeZoneValue> timeZone(cx, TimeZoneValue{});
    if (!RoundRelativeDuration(cx, duration, originEpochNs, destEpochNs,
                               isoDateTime, timeZone, temporalDate.calendar(),
                               settings.largestUnit, settings.roundingIncrement,
                               settings.smallestUnit, settings.roundingMode,
                               &duration)) {

File: js/src/builtin/temporal/Duration.cpp, line 2777-2782

  MOZ_ASSERT(IsValidDuration(duration));
  MOZ_ASSERT_IF(timeZone, IsValidEpochNanoseconds(originEpochNs));
  MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(originEpochNs));  // <-- fires
  MOZ_ASSERT_IF(timeZone, IsValidEpochNanoseconds(destEpochNs));
  MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(destEpochNs));
  MOZ_ASSERT(ISODateTimeWithinLimits(isoDateTime));  // would also fire

File: js/src/builtin/temporal/Duration.cpp, line 1972-1990

#ifdef DEBUG
/**
 * Return true if the input is within the valid epoch nanoseconds limits with a
 * time zone offset applied, i.e. it's smaller than ±(8.64 × 10^21 + nsPerDay).
 */
static bool IsValidLocalNanoseconds(const EpochNanoseconds& epochNanoseconds) {
  MOZ_ASSERT(0 <= epochNanoseconds.nanoseconds &&
             epochNanoseconds.nanoseconds <= 999'999'999);

  // Time zone offsets can't exceed 24 hours.
  constexpr auto oneDay = EpochDuration::fromDays(1);

  // Exclusive limits.
  constexpr auto min = EpochNanoseconds::min() - oneDay;
  constexpr auto max = EpochNanoseconds::max() + oneDay;

  return min < epochNanoseconds && epochNanoseconds < max;  // strict < at lower bound
}
#endif

File: js/src/builtin/temporal/PlainDateTime.cpp, line 78-101

bool js::temporal::ISODateTimeWithinLimits(const ISODateTime& isoDateTime) {
  MOZ_ASSERT(IsValidISODateTime(isoDateTime));

  constexpr auto min = ISODate::min();
  constexpr auto max = ISODate::max();

  const auto& year = isoDateTime.date.year;

  // Fast-path when the input is definitely in range.
  if (min.year < year && year < max.year) {
    return true;
  }

  // Check |isoDateTime| is within the valid limits.
  if (year < 0) {
    if (isoDateTime.date != min) {
      return isoDateTime.date > min;
    }

    // Needs to be past midnight.
    return isoDateTime.time != Time{};  // midnight on min date is INVALID
  }
  return isoDateTime.date <= max;
}

File: js/src/builtin/temporal/PlainDate.cpp, line 94-112

bool js::temporal::ISODateWithinLimits(const ISODate& isoDate) {
  MOZ_ASSERT(IsValidISODate(isoDate));

  constexpr auto min = ISODate::min();
  constexpr auto max = ISODate::max();

  const auto& year = isoDate.year;

  // Fast-path when the input is definitely in range.
  if (min.year < year && year < max.year) {
    return true;
  }

  // Check |isoDate| is within the valid limits.
  if (year < 0) {
    return isoDate >= min;  // INCLUSIVE: -271821-04-19 is a valid PlainDate
  }
  return isoDate <= max;
}

File: js/src/builtin/temporal/TemporalTypes.h, line 408-416

  /**
   * The minimum ISODate within the valid limits is -271821 April, 19.
   */
  static constexpr ISODate min() { return {-271821, 4, 19}; }

  /**
   * The maximum ISODate within the valid limits is 275760 September, 13.
   */
  static constexpr ISODate max() { return {275760, 9, 13}; }

ISODateWithinLimits accepts -271821-04-19 inclusively (line 109: return isoDate >= min), making it a constructible PlainDate. But ISODateTimeWithinLimits rejects midnight on that date (line 98: return isoDateTime.time != Time{}). DifferenceTemporalPlainDate at line 643 unconditionally pairs the receiver date with {} (midnight) without checking whether the resulting ISODateTime is within limits, then passes it to RoundRelativeDuration which asserts the precondition. The corresponding originEpochNs computed by GetUTCEpochNanoseconds equals exactly EpochNanoseconds::min() - oneDay, which is the exclusive lower bound in IsValidLocalNanoseconds.

Exploit Chain

  1. Construct a Temporal.PlainDate at the minimum representable date: new Temporal.PlainDate(-271821, 4, 19). This passes ISODateWithinLimits because the lower bound is inclusive.
  2. Call .until() or .since() against any other PlainDate with options that force the rounding path: roundingIncrement != 1 OR smallestUnit != 'day'. This enters the branch at PlainDate.cpp:640.
  3. DifferenceTemporalPlainDate constructs ISODateTime{(-271821,4,19), Time{}} at line 643. This value fails ISODateTimeWithinLimits because midnight on the minimum date is excluded.
  4. GetUTCEpochNanoseconds converts this to an EpochNanoseconds value lying exactly on the exclusive lower bound (EpochNanoseconds::min() - oneDay).
  5. RoundRelativeDuration receives this value and MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(originEpochNs)) at Duration.cpp:2779 fails because IsValidLocalNanoseconds uses strict inequality at the lower bound.
  6. In debug builds the process aborts. In release builds the assertion is compiled out and computation proceeds correctly — differential testing confirms outputs match equivalent operations on non-boundary dates.

Steps to Reproduce

  1. Build SpiderMonkey with --enable-debug (MOZ_ASSERT must be enabled).
  2. Run the JS shell with the following script:
  3. let minDate = new Temporal.PlainDate(-271821, 4, 19);
  4. let other = new Temporal.PlainDate(-271821, 4, 20);
  5. minDate.until(other, { roundingIncrement: 2 });
  6. Observe assertion failure: IsValidLocalNanoseconds(originEpochNs) at Duration.cpp:2779.

Security Impact

  • Severity: Low
  • Attacker capability: Denial of service in debug builds only, via deterministic null-pointer-write abort in MOZ_CrashSequence. Release builds are unaffected: IsValidLocalNanoseconds is gated by #ifdef DEBUG, and the underlying duration-rounding arithmetic handles the boundary epoch-nanoseconds value correctly without overflow or precision loss. Differential testing against equivalent non-boundary date pairs confirms identical results across roundingMode (trunc/halfExpand/ceil), roundingIncrement, largestUnit (days/months/years), and smallestUnit configurations.
  • Preconditions: Debug-enabled SpiderMonkey build. Attacker must be able to execute arbitrary JavaScript (standard web content capability). The Temporal proposal must be enabled. No user interaction or special configuration is required beyond running the script.

ASAN Report

[21737] Assertion failure: IsValidLocalNanoseconds(originEpochNs), at /firefox/js/src/builtin/temporal/Duration.cpp:2779
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==21737==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5c18e99e0948 bp 0x7ffdfae2d350 sp 0x7ffdfae2d160 T21737)
==21737==The signal is caused by a WRITE memory access.
==21737==Hint: address points to the zero page.
    #0 0x5c18e99e0948 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:235:3
    #1 0x5c18e99e0948 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:2779:3
    #2 0x5c18e99fce4e in DifferenceTemporalPlainDate(JSContext*, js::temporal::TemporalDifference, JS::CallArgs const&) /firefox/js/src/builtin/temporal/PlainDate.cpp:656:10
    #3 0x5c18e99fa345 in PlainDate_until(JSContext*, JS::CallArgs const&) /firefox/js/src/builtin/temporal/PlainDate.cpp:1459:10
    #4 0x5c18e99fa345 in bool JS::CallNonGenericMethod<&IsPlainDate(JS::Handle<JS::Value>), &PlainDate_until(JSContext*, JS::CallArgs const&)>(JSContext*, JS::CallArgs const&) /firefox/obj-js-debug/dist/include/js/CallNonGenericMethod.h:103:12
    #5 0x5c18e99fa345 in PlainDate_until(JSContext*, unsigned int, JS::Value*) /firefox/js/src/builtin/temporal/PlainDate.cpp:1468:10
    #6 0x5c18e9164ef4 in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:490:13
    #7 0x5c18e91402a5 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:586:12
    #8 0x5c18e915004c in js::CallFromStack(JSContext*, JS::CallArgs const&, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:658:10
    #9 0x5c18e915004c in js::Interpret(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:3272:16
    #10 0x5c18e913f91a in js::RunScript(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:460:13
    #11 0x5c18e9142780 in js::ExecuteKernel(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, js::AbstractFramePtr, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:850:10
    #12 0x5c18e9142bde in js::Execute(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:880:10
    #13 0x5c18e93fc111 in ExecuteScript(JSContext*, JS::Handle<JSObject*>, JS::Handle<JSScript*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:548:10
    #14 0x5c18e93fc2f3 in JS_ExecuteScript(JSContext*, JS::Handle<JSScript*>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:572:10
    #15 0x5c18e7d4451d in RunFile(JSContext*, char const*, _IO_FILE*, CompileUtf8, bool, bool) /firefox/js/src/shell/js.cpp:1388:10
    #16 0x5c18e7d43a09 in Process(JSContext*, char const*, bool, FileKind) /firefox/js/src/shell/js.cpp
    #17 0x5c18e7cff52c in ProcessArgs(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12130:10
    #18 0x5c18e7cff52c in Shell(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12383:12
    #19 0x5c18e7cf63f0 in main /firefox/js/src/shell/js.cpp:12789:12
    #20 0x7e99bd7601c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #21 0x7e99bd76028a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #22 0x5c18e7cbdbc8 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)
==21737==ABORTING
Attached file poc.js —
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Attached file analysis.json —
Group: core-security → javascript-core-security

André, do you have time to take a look at this? For context, we've recently added testing of JS debug shell builds.

Flags: needinfo?(andrebargull)

Reading comment 0 this isn't a security bug at all.

Yes, this is just an overzealous assertion, so not sec-sensitive.

Flags: needinfo?(andrebargull)
Attached file (secure) —

Changes:

  • IsValidLocalNanoseconds can be called with the minimum PlainDate -271821 April, 19,
    so the correct lower limit is min <= epochNanoseconds. Adjust the limit and change
    the function name to reflect the update limits.
  • RoundRelativeDuration can be called with the minimum PlainDate -271821 April, 19
    at midnight, which is outside the valid limits for ISODateTimeWithinLimits. Replace
    the assertion IsValidISODateTime to test for valid date-times and ISODateWithinLimits
    to ensure the date part is within the allowed limits.
  • In ComputeNudgeWindow, step 8.b, the computed startDateTime is not guaranteed to
    pass ISODateTimeWithinLimits for the same reason. Apply the same changes as above and
    test only IsValidISODateTime and ISODateWithinLimits.
  • endDateTime at ComputeNudgeWindow, step 10, needs a similar change, but that change
    is addressed in bug 2028733.

The test case covers all assertions modified in this patch. The suggested patch in
bug 2028729 wants to change more assertions, but I don't see how the other assertions
can be reached with invalid inputs causing assertion failures. Exhaustive testing over
all rounding options and dates around the minimum plain date limits also didn't show
more assertion failures.

Assignee: nobody → andrebargull
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Whiteboard: [prefs-checked]

No non-default prefs required — javascript.options.experimental.temporal is true by default on all channels. Tagging [prefs-checked].

sec-other is correct (debug-only assertion, release unaffected). Not producing an additional patch since :anba already has D291719 up.

This is an automated analysis result. If this result is incorrect please add a needinfo and feel free to correct the error.

Blocks: sm-security
Severity: -- → S3
Priority: -- → P1
Pushed by andre.bargull@gmail.com: https://github.com/mozilla-firefox/firefox/commit/7df403aebcc1 https://hg.mozilla.org/integration/autoland/rev/1ffe5bd3a966 Relax assertions for duration rounding with PlainDate. 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: