Skip to content

node:fs: do not convert the argument of fs._toUnixTimestamp, so a Date returns seconds - #45274

Draft
robobun wants to merge 9 commits into
mainfrom
robobun/3179519b/fs-tounixtimestamp-date
Draft

robobun wants to merge 9 commits into
mainfrom
robobun/3179519b/fs-tounixtimestamp-date

Conversation

@robobun

@robobun robobun commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Draft: it waits for CI on ac5f7f6. Nothing else is owed.

Problem

  • fs._toUnixTimestamp(new Date(5000)) returns the Date object. Node v26.3.0 returns 5.
  • null, true and [] come back unchanged. Node throws ERR_INVALID_ARG_TYPE.
  • Cause: $isFinite(time) at src/js/node/fs.ts:1010 is the global isFinite, which converts its argument. Node's line is NumberIsFinite(time).

Fix

  • Check typeof time === "number" before $isFinite(time). The pair equals Number.isFinite, so a Date reaches the isDate branch.
  • Correct because the line is Node's again. For 41 values, this branch and Node v26.3.0 give the same value or error code (Notes).
  • Verified: test/js/node/fs/fs-to-unix-timestamp.test.ts (new: 2 of 4 tests fail on main, 4 pass under Node). Also test-fs-utimes.js, 30 runs.
  • Self-reviewed: 23 concerns raised, 22 addressed. Rejected: an edit of REVIEW.md:55 here, because that text is the maintainers'.

Background

  • fs._toUnixTimestamp is Node's internal toUnixTimestamp, exported for tests. No user reported this fault: the case is Node parity.
  • test-fs-utimes.js sends times through it. Two of its seven cases, the numeric ones, compared seconds with milliseconds and could not fail. This PR takes Node v26.3.0's copy of the file.
  • Considered a native read of the clock and the Date, so that replaced built-ins cannot change the result. It is out: it conflicts with bindgen: delete b.any and .variadic #46789, and nobody asked for it.

Downsides

Notes

Node source. toUnixTimestamp is lib/internal/fs/utils.js#L779-L796. The export is lib/fs.js#L3361-L3362.

Repro.

import fs from "node:fs";
for (const date of [new Date(5000), new Date(0), new Date(1500), new Date(-5000)]) {
  const got = fs._toUnixTimestamp(date);
  console.log(date.getTime() / 1000, got instanceof Date ? "a Date object" : got);
}
wanted main (449877f) this branch node v26.3.0
5 a Date object 5 5
0 a Date object 0 0
1.5 a Date object 1.5 1.5
-5 the current time -5 -5

41 values. "now" is the current time in seconds. The message text is not compared (see #38657). On main, {valueOf(){return 7}} also runs the user's valueOf.

fs._toUnixTimestamp(x) main (449877f) this branch node v26.3.0
1000 1000 1000 1000
1.5 1.5 1.5 1.5
0 0 0 0
-0 -0 -0 -0
-1 now now now
-5.5 now now now
NaN throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
Infinity throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
-Infinity throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
'1000' 1000 1000 1000
'-1' -1 -1 -1
'' 0 0 0
' ' 0 0 0
'abc' throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
'Infinity' Infinity Infinity Infinity
'0x10' 16 16 16
'1e3' 1000 1000 1000
' 12 ' 12 12 12
new Date(5000) the argument itself (Date) 5 5
new Date(0) the argument itself (Date) 0 0
new Date(1500) the argument itself (Date) 1.5 1.5
new Date(-5000) now -5 -5
new Date(NaN) NaN NaN NaN
new MyDate(5000) (a subclass) the argument itself (Date) 5 5
vm.runInNewContext("new Date(5000)") the argument itself (Date) 5 5
Object.create(Date.prototype) throws TypeError throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
null null throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
undefined throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
true true throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
false false throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
[] the argument itself (Array) throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
[7] the argument itself (Array) throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
[1,2] throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
{} throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
new Number(12) the argument itself (Number) throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
new String('12') the argument itself (String) throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
12n throws TypeError (no code) throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
Symbol() throws TypeError (no code) throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
{valueOf(){return 7}} the argument itself (Object) throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
{valueOf(){throw}} throws the user's Error throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE
function(){} throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE throws ERR_INVALID_ARG_TYPE

test-fs-utimes.js. Lines 48 to 53 of the old copy compute fs._toUnixTimestamp(mtime) - fs._toUnixTimestamp(stats.mtime), and line 67 asserts that the result is less than 2.

  • For the five Date cases, both calls return the Date on main, so the check compares milliseconds. It is real there, and 1000 times stricter than Node's.
  • For the two numeric cases, main compares seconds with milliseconds. With a wanted time of 5000 and a real mtime of 1000 it computes -995000, and the assertion passes. This branch and node v26.3.0 compute 4000.
  • One numeric case has mtime = -1, which means "now". The old copy reads that "now" after the call, so with this fix the check becomes a race against a 2 s bound. Node v26.3.0's copy reads it before the call (getExpectedMtime). The PR takes that copy verbatim. It passes on main, on this branch (30 of 30 runs on the debug ASAN build) and under node.

What the fix does not cover. With the type check, every valid Date reaches time.getTime(), and a negative number reaches Date.now() as before. User code can replace both. Node calls the DatePrototypeGetTime and DateNow primordials, which it saves before user code runs. A grid of 27 states of the Date built-ins, each in 3 orders against the first load of node:fs, in 81 fresh processes with 38 cells each:

cells that differ from node v26.3.0, of 3078
main (449877f) 1500
this branch 311

The 311 are a Date subclass that overrides getTime, an own getTime property and an own getTime accessor (81 each, in every state), and the cells where Date.now or getTime is replaced or deleted at the time of the call. Against main: 1578 cells are as Node on both, 1189 moved to Node's answer, 311 are as Node on neither, and 0 are right on main and wrong on this branch. node:fs loads in all 81 processes.

How the PR got here. Two earlier heads tried to close that gap.

  1. Captures of Date.now and Date.prototype.getTime at the top of fs.ts, after a review nit. They read the global Date when node:fs loads. With Date deleted, undefined or spied (bun:test spyOn(globalThis, "Date")) before the first load, node:fs did not load. main never had that defect.
  2. One bindgen native, timeValueOrNow, read where no user code runs. It matched Node in all 3078 cells. A self-review then said to take it out: it adds a b.any union while bindgen: delete b.any and .variadic #46789 deletes b.any (a trial merge conflicts in node_fs_binding.rs), no issue or user asks for it, and REVIEW.md:66 and the PR process notes ask for the narrowest change. It is kept on the branch robobun/3179519b/fs-tounixtimestamp-native-read (c0126ec) with its tests and measurements. internal/primordials: Node.js parity, generated and engine-backed #35567 would supply $DateNow and $DatePrototypeGetTime for all of src/js; it has had no push since 2026-08-16.

Upstream. nodejs/node#66634 ("fs: preserve negative utimes timestamps", open, approved, semver-major) changes what a negative time means in Node. Bun follows v26, where a negative number means "now".

The forms that take a time. utimes, lutimes and futimes (sync, callback, promises and FileHandle.utimes) do not use this helper. They parse times in time_like::from_js (src/runtime/node/time_like.rs). With 14 values, main, this branch and node give the same mtime or error code in all 9 forms.

Not fixed here, and handed to separate work.

  • Six built-in files read Date at module scope on main (node/v8.ts:13, node/inspector.ts:12, node/_http_server.ts:123, internal/test_runner/mock_timers.ts:24, internal/repl/node-primordials.js:32, internal/util/inspect.js:62-64). With no global Date at their first load, node:v8, node:inspector, node:http, node:https, node:test and node:repl do not load. Node loads them.
  • The same kind of substitution as $isFinite for NumberIsFinite: $isJSArray stands where Node has ArrayIsArray in child_process.ts, so a Proxy of an array is rejected as args or stdio where Node accepts it.
  • $isFinite itself has no other use in src/js.

Other open PRs. This branch merges without a conflict with #46789 and #45338 (git merge-tree). Against #38657 and #35541 it adds no conflict that they do not already have with main.

Measurements.

  • embedded JS (bundle-modules.ts --debug=OFF): +28 B against main (InternalModuleRegistryConstants.bin 2,714,997 to 2,715,025, node/fs.js 30,980 to 31,008). The 28 B are the text typeof time === "number" && .
  • native code: none changes. JS2Native entries stay at 374 (debug).
  • node:fs top level (BUN_JSC_dumpGeneratedBytecodes): 987 instructions and 5070 bytecode bytes, the same as main.
  • per call: one typeof check, only for an argument that is not a string.

Tests.

$ node --test test/js/node/fs/fs-to-unix-timestamp.test.ts          (v26.3.0)
tests 4, pass 4, fail 0

$ bun bd test test/js/node/fs/fs-to-unix-timestamp.test.ts           (this branch, debug ASAN build)
4 pass, 0 fail

$ bun test test/js/node/fs/fs-to-unix-timestamp.test.ts              (release build of main 449877f3e0)
(pass) fs._toUnixTimestamp() > returns a number and a numeric string as seconds
(pass) fs._toUnixTimestamp() > returns the current time for a negative number
(fail) fs._toUnixTimestamp() > returns the seconds of a Date
(fail) fs._toUnixTimestamp() > throws ERR_INVALID_ARG_TYPE for a value that is not a number, a numeric string or a Date
2 pass, 2 fail

The test file uses node:test and node:assert, as fs-read-buffer-before-offset.test.ts and fs-write-offset-bound.test.ts do, so the same file runs under Node.

Other runs on this branch (debug ASAN build):

  • test/js/node/test/parallel/test-fs-utimes.js (the new copy), test-fs-timestamp-parsing-error.js, test-fs-utimes-y2K38.js, test-fs-promises.js: exit 0.
  • test/js/node/fs/fs.test.ts -t utimes: 9 pass, 1 skip.
  • tsc --noEmit -p src/js/tsconfig.json: exit 0. bun lint and bun fmt --check on the changed files: no problems.

[human-review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/fs/fs-to-unix-timestamp.test.ts
bun test v1.4.4 (367d939d9)

test/js/node/fs/fs-to-unix-timestamp.test.ts:
(pass) fs._toUnixTimestamp() > returns a number and a numeric string as seconds [174.94ms]
(pass) fs._toUnixTimestamp() > returns the current time for a negative number [17.82ms]
48 |       Date,
49 |       "now",
50 |       () => 0,
51 |       () => toUnixTimestamp(-5),
52 |     );
53 |     assert.ok(Math.abs(seconds - Date.now() / 1000) < 60, `${seconds} is not the current time`);
                ^
AssertionError: 0 is not the current time
 generatedMessage: false,
     actual: false,
   expected: true,
   operator: "==",
       diff: "simple",
       code: "ERR_ASSERTION"

      at /workspace/bun/test/js/node/fs/fs-to-unix-timestamp.test.ts:53:12
      at run (node:async_hooks:226:29)
      at node:test:1781:26
      at executeTestNode (node:test:1785:45)
      at node:test:1956:20
(fail) fs._toUnixTimestamp() > returns the current time for a negative number when Date.now is replaced [87.30ms]
53 |     assert.ok(Mat
... (truncated)

release without fix: 4 FAILED
bun test v1.4.3-canary.1 (367d939d9)

test/js/node/fs/fs-to-unix-timestamp.test.ts:
(pass) fs._toUnixTimestamp() > returns a number and a numeric string as seconds [10.86ms]
(pass) fs._toUnixTimestamp() > returns the current time for a negative number [2.26ms]
48 |       Date,
49 |       "now",
50 |       () => 0,
51 |       () => toUnixTimestamp(-5),
52 |     );
53 |     assert.ok(Math.abs(seconds - Date.now() / 1000) < 60, `${seconds} is not the current time`);
                ^
AssertionError: 0 is not the current time
 generatedMessage: false,
     actual: false,
   expected: true,
   operator: "==",
       diff: "simple",
       code: "ERR_ASSERTION"

      at /workspace/bun/test/js/node/fs/fs-to-unix-timestamp.test.ts:53:12
      at run (node:async_hooks:144:29)
      at node:test:1445:26
      at executeTestNode (node:test:1448:45)
      at node:test:1591:20
(fail) fs._toUnixTimestamp() > returns the current time for a negative number when Date.now is replaced [7.17ms]
53 |     assert.ok(Math.abs(seconds - Date.now() / 1000) < 60, `${seconds} is not the current time`);
54 |   });
55 | 
56 |   test("returns the seconds of a Date", () => {
57 |     const dates 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/fs/fs-to-unix-timestamp.test.ts
bun test v1.4.4 (367d939d9)

test/js/node/fs/fs-to-unix-timestamp.test.ts:
(pass) fs._toUnixTimestamp() > returns a number and a numeric string as seconds [207.53ms]
(pass) fs._toUnixTimestamp() > returns the current time for a negative number [15.75ms]
(pass) fs._toUnixTimestamp() > returns the current time for a negative number when Date.now is replaced [33.95ms]
(pass) fs._toUnixTimestamp() > returns the seconds of a Date [30.47ms]
(pass) fs._toUnixTimestamp() > returns the seconds of a Date when getTime is overridden or replaced [27.90ms]
(pass) fs._toUnixTimestamp() > throws ERR_INVALID_ARG_TYPE for a value that is not a number, a numeric string or a Date [30.94ms]

 6 pass
 0 fail
Ran 6 tests across 1 file. [4.80s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     dacbecf144
  features     lto, baseline

23 deps, 146 codegen, 1176 objects in 1958ms

ninja: Entering directory `/workspace/bun/build/release'
[1/4] fetch rust-argon2
[rust-argon2] up to date
[2/4] fetch lolhtml
[lolhtml] up to date
[2/4] cargo plan → /workspace/bun/build/release/rust-target/plan.json
264 units: 192 lib, 16 proc-macro (host), 19 custom-build (host), 15 run custom-build, 17 lib (host), 4 run custom-build (host), 1 rlib
[3/4] reconfigure
[1/1529] mkdir stamps
[2/1529] mkdir codegen
[3/1529] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[4/1529] install /workspace/bun
bun install v1.4.3-canary.1 (367d939d9)

Checked 22 installs across 44 packages (no changes) [465.00ms]
[5/1529] install /workspace/bun/packages/bun-error
bun install v1.4.3-canary.1 (367d939d9)

Checked 1 install across 2 packages (no changes) [48.00ms]
[6/1529] install /workspace/bun/src/node-fallbacks
bun install v1.4.3-canary.1 (367d939d9)

Checked 111 installs acro
... (truncated)
diff hotspot
src/js/node/fs.ts                            |  11 ++-
 test/js/node/fs/fs-to-unix-timestamp.test.ts | 115 +++++++++++++++++++++++++++
 2 files changed, 123 insertions(+), 3 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                          reads  edits  tests
src/js/node/fs.ts                                 4      7     27
test/js/node/fs/fs-to-unix-timestamp.test.ts      2      3     25

…e returns seconds

fs._toUnixTimestamp tested its argument with the global isFinite, which
converts the argument to a number first. A Date passed that test and came
back unchanged, or as the current time when it was before 1970. null,
booleans, arrays and boxed primitives also came back unchanged.

Check typeof time === "number" first, which is what Node's
NumberIsFinite(time) does. A Date now reaches the isDate branch and
returns getTime() / 1000. Every other value that is not a number or a
numeric string throws ERR_INVALID_ARG_TYPE.
@robobun

robobun commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:19 PM PT - Oct 26th, 2026

✅ @robobun, your commit ac5f7f6563799c556c79dd93efe7d052f191cf34 passed in Build #133208! 🎉


🧪   To try this PR locally:

bunx bun-pr 45274

That installs a local version of the PR into your bun-45274 executable, so you can run:

bun-45274 --bun

@robobun

robobun commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: reproduced, fix pushed in this PR (#45274).

Reproduced with fs._toUnixTimestamp(new Date(5000)). A release build of main (449877f) returns the Date object. Node v26.3.0 returns 5. The new tests in test/js/node/fs/fs-to-unix-timestamp.test.ts fail on main (2 of 4) and pass on this branch and under node --test.

@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 36c3b00f-c5b7-48e7-af2f-f29a07730b56
📥 Commits

Reviewing files that changed from the base of the PR and between e5bad31 and dacbecf.

📒 Files selected for processing: 1
  • src/js/node/fs.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Walkthrough

_toUnixTimestamp now checks numeric timestamps for finiteness without coercion. It uses captured Date methods for negative-time fallback and date conversion. The new tests cover numbers, dates, replaced methods, and invalid inputs.

Changes

Unix Timestamp Validation

Layer / File(s) Summary
Timestamp validation and tests
src/js/node/fs.ts, test/js/node/fs/fs-to-unix-timestamp.test.ts
_toUnixTimestamp accepts finite numbers without relying on coercive global isFinite. It uses captured Date methods for negative-time fallback and date conversion. Tests cover numeric and date conversions, invalid-input errors, and an object whose valueOf method must not be called.

Suggested reviewers: jarred-sumner

Priority: Low

Merge Risk: Minimal · up to dacbe

The timestamp conversion change has no concrete merge-blocking issue established by the supplied evidence.

Pre-merge checks: Passed 4
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title names the main change: fs._toUnixTimestamp no longer converts its argument, so a Date returns seconds. It matches the diff, which replaces the global isFinite check with a typeof number chec…
Description check Passed The description follows the template. It has a 'Problem' and 'Fix' section that covers the 'What does this PR do?' content, and it has a 'Verified' section with test commands and results that covers '…
Full details: Title check

Explanation

The title names the main change: fs._toUnixTimestamp no longer converts its argument, so a Date returns seconds. It matches the diff, which replaces the global isFinite check with a typeof number check before $isFinite. The title is 86 characters, which is longer than ideal, but it is specific and accurate.

Full details: Description check

Explanation

The description follows the template. It has a 'Problem' and 'Fix' section that covers the 'What does this PR do?' content, and it has a 'Verified' section with test commands and results that covers 'How did you verify your code works?'. The template headings 'What does this PR do?' and 'How did you verify your code works?' are not used verbatim. The description is long and contains a draft note, extensive notes, and internal process remarks (such as self-review counts and references to other PRs) that do not belong in a PR description. These are not missing required information, so the check passes.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 src/js/node/fs.ts — Any script that patches Date.prototype.getTime now changes what fs._toUnixTimestamp returns for every valid Date; on the base branch that method was never reached for a valid Date. src/js/node/fs.ts:1012 now sends every Date past the number branch, so src/js/node/fs.ts:1020 calls the user-overridable time.getTime() for all of them. Node uses the DatePrototypeGetTime primordial here, and src/js code must not route through user-replaceable methods. Fix: capture Date.prototype.getTime at module load (as src/js/internal/test_runner/mock_timers.ts:24 does) and invoke it with $call, so a tampered or mocked prototype cannot alter the result.

    Why this was flagged

    Trigger: any Date argument to fs._toUnixTimestamp, arriving from user code or from vendored Node tests such as test/js/node/test/parallel/test-fs-utimes.js:49 and :51, in a process where Date.prototype.getTime was replaced (test mocks, polyfills, instrumentation). On the base branch, $isFinite(date) coerced the Date and returned it unchanged, so src/js/node/fs.ts:1020 ran only for an invalid Date; after the diff, src/js/node/fs.ts:1012 excludes all non-numbers and every valid Date reaches time.getTime() at :1020. The call is a normal property lookup, so a replaced getTime returns whatever user code decides, and the dismissal's claim that no runtime path depends on it ignores that the function is a public export exercised by Node's own test suite. The sibling test file asserts valueOf is never invoked (test/js/node/fs/fs-to-unix-timestamp.test.ts:69) yet the Date path still invokes user-overridable code. Remedy: use a module-load-captured DatePrototypeGetTime with $call.

    Verification: nit — triggered when any code in the process replaces or shadows Date.prototype.getTime and then a valid Date reaches fs._toUnixTimestamp. src/js/node/fs.ts:1012 now excludes every Date, so each valid Date reaches src/js/node/fs.ts:1020 return time.getTime() / 1000;, a user-replaceable method. On the base, line 1020 ran only for an invalid Date.

Node's toUnixTimestamp calls the DateNow and DatePrototypeGetTime
primordials. Capture both when node:fs loads and call those. A Date
subclass that overrides getTime, a replaced Date.prototype.getTime and a
replaced Date.now (for example node:test mock timers) no longer change
the result.
Comment thread src/js/node/fs.ts Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small, self-contained port of Node's toUnixTimestamp with a test that runs under both runtimes.

What was reviewed:

  • Branch order and semantics in src/js/node/fs.ts against Node's lib/internal/fs/utils.js: typeof === "number" && $isFinite is equivalent to NumberIsFinite ($isFinite maps to the coercing global in src/codegen/replacements.ts), and the isDate / DatePrototypeGetTime.$call / DateNow capture matches the primordials pattern used elsewhere in src/js.
  • Callers: _toUnixTimestamp is only exported from node:fs (the utimes family parses times natively), so the behavior change is confined to the helper; the vendored test-fs-timestamp-parsing-error.js and test-fs-utimes.js usages remain valid since Stats.mtime is a real DateInstance.
  • The new test's tamper cases (replaced Date.now, own/overridden/replaced getTime) each fail without the corresponding captured primordial, and no existing test asserts the old pass-through behavior.
Extended reasoning...

The diff changes one exported helper in src/js/node/fs.ts (about eight lines) to use a typeof guard before $isFinite and to call Date.now and Date.prototype.getTime captured at module load, plus a new 115-line node:test file. It touches no security-sensitive surface; the helper is only exported and not used by utimes/futimes/lutimes, which parse times natively. The implementation matches Node's source line for line, the test file follows an existing precedent in test/js/node/fs for node:test-based compat tests, and the changed paths are not covered by CODEOWNERS. The earlier COMMENTED review was followed by a commit adding the primordial captures, and nothing in the timeline indicates an outstanding CHANGES_REQUESTED objection.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the one-line comment from the latest push settles the last open nit, and the fix now matches Node's toUnixTimestamp exactly.

What was reviewed:

  • The typeof time === "number" && $isFinite(time) guard is equivalent to Node's NumberIsFinite and makes the isDate branch reachable; DateNow/DatePrototypeGetTime.$call are captured at load, so a replaced Date.now/getTime cannot change the result.
  • Confirmed _toUnixTimestamp has no internal callers in src/js/ (only the export at src/js/node/fs.ts:1365); utimes/futimes/lutimes parse times natively, so no sibling site shares the pattern.
  • Checked that $ERR_INVALID_ARG_TYPE formats bigint and symbol arguments (the newly reachable inputs) in ErrorCode.cpp without throwing a bare TypeError.
  • The new test restores Date.now/Date.prototype.getTime in finally and asserts exact code/class on every rejected input, so it cannot pass vacuously.
Extended reasoning...

The diff changes ~10 lines in src/js/node/fs.ts (a typeof guard in _toUnixTimestamp plus two load-time captures of Date.now and Date.prototype.getTime) and adds a 115-line node:test-based test file following the pattern of sibling files in test/js/node/fs/. It touches no security-sensitive surface; _toUnixTimestamp is an internal helper exported only for Node's vendored tests and has no callers inside src/js/. The change is small, self-contained, mirrors Node's reference implementation line for line, and the bug hunt ran dry; the paths are not covered by CODEOWNERS and the only prior reviews were COMMENTED states whose last open item (comment length at line 1014) was addressed by commit 667d598.

@robobun robobun changed the title node:fs: do not convert the argument of fs._toUnixTimestamp, so a Date returns seconds node:fs: do not convert the argument or call replaceable Date methods in fs._toUnixTimestamp Oct 15, 2026
…xTimestamp

The captures of Date.now and Date.prototype.getTime ran when node:fs
loads. A program that deleted or replaced the global Date before that
made node:fs fail to load, and a replaced built-in stayed captured after
it was restored.

fs._toUnixTimestamp now calls one native, made at its first call, that
gives the clock's time or the time value of a Date. node:fs reads
nothing from Date when it loads. The isDate capture goes too: the native
does that check.
@robobun
robobun marked this pull request as draft October 26, 2026 11:25
@robobun

robobun commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Update: the native fix that the last paragraph describes is not in this PR. See #45274 (comment) for the final shape.

I converted this PR to a draft. Do not merge the head dacbecf.

A later test found a defect in it. The two module-scope captures (const DateNow = Date.now and const DatePrototypeGetTime = Date.prototype.getTime in src/js/node/fs.ts) read the global Date when node:fs loads. If a program deleted or replaced Date before that, node:fs does not load, and it stays so after Date is put back:

const D = Date;
delete globalThis.Date;
await import("node:fs"); // this head: ReferenceError: Date is not defined. main and node v26.3.0: loads

bun:test spyOn(globalThis, "Date") before the first load has the same effect. A built-in that was replaced before the load and restored later also stays captured.

A fix is built and verified locally. fs._toUnixTimestamp reads the clock and the time value of a Date through one native function, so node:fs reads nothing from Date when it loads. On that build, node:fs loads in every state that I tried, and 0 of 3078 cells of a 27-state grid differ from node v26.3.0 (this head: 786). I will push it after a self-review.

Take the native read of the clock and of a Date's time value out again.
fs._toUnixTimestamp calls Date.now() and time.getTime() as on main, and
only tests the type before $isFinite.

The tests keep the cases that need no replaced built-in. Rows for an
invalid Date, a Date of another realm, an object with Date.prototype and
a Proxy of a Date are new.

test-fs-utimes.js is the copy of Node v26.3.0. It takes the wanted time
of a negative number before the call, so the check that this fix makes
real does not depend on how long the call takes.
@robobun robobun changed the title node:fs: do not convert the argument or call replaceable Date methods in fs._toUnixTimestamp node:fs: do not convert the argument of fs._toUnixTimestamp, so a Date returns seconds Oct 26, 2026
@robobun

robobun commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

This PR is back to its first shape: the one-line type check in fs._toUnixTimestamp, with tests (head ac5f7f6).

What changed since my draft note above:

  • The native read (timeValueOrNow) is out of this PR. A self-review found three things. It adds a b.any union while bindgen: delete b.any and .variadic #46789 deletes b.any, and the two conflict in node_fs_binding.rs. No issue or user asks for it. REVIEW.md:66 and the PR process notes ask for the narrowest change. The native stays on the branch robobun/3179519b/fs-tounixtimestamp-native-read (c0126ec) with its tests and measurements.
  • The module-scope captures are gone too. node:fs reads nothing from Date when it loads, as on main.
  • test/js/node/test/parallel/test-fs-utimes.js is now Node v26.3.0's copy. The fix makes its check for mtime = -1 real, and the older copy read the wanted time after the call.

What the PR does not do: a replaced or overridden getTime or Date.now still changes the result, where Node reads both before user code runs. The PR text has the numbers.

The PR stays a draft until CI on this head is done.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants