diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index bc869fbd776a..bdb5c8c328f1 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -3905,6 +3905,13 @@ pub mod args { let buffer_value = arguments.next_eat().ok_or_else(|| // theoretically impossible, argument has been passed already ctx.throw_invalid_arguments(format_args!("buffer is required")))?; + if Buffer::from_js(ctx, buffer_value).is_none() { + return Err(ctx.throw_invalid_argument_type_value( + b"buffer", + b"TypedArray", + buffer_value, + )); + } let offset_value = arguments.next_eat().unwrap_or(JSValue::NULL); // if (offset == null) { @@ -3931,9 +3938,11 @@ pub mod args { } else { 0.0 }; - let buffer = Buffer::from_js(ctx, buffer_value).ok_or_else(|| { - ctx.throw_invalid_argument_type_value(b"buffer", b"TypedArray", buffer_value) - })?; + // `length.toNumber()` can re-enter JS and detach `buffer_value`; re-snapshot + // the backing store so the subsequent bounds checks and the read itself see + // the post-coercion length/pointer. + let buffer = Buffer::from_js(ctx, buffer_value) + .expect("buffer JSCell type is immutable; detached views return Some(len=0)"); // if (length === 0) { // return process.nextTick(function tick() { diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 4e0fe760e70c..d1527410ce7d 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -1262,6 +1262,47 @@ describe("readSync", () => { } }); + it("throws ERR_INVALID_ARG_TYPE for a non-buffer before validating offset", () => { + // Node's validateBuffer runs before validateInteger(offset, ...). When both + // arguments are invalid the buffer-type error must win. + let err: any; + try { + readSync(0, "not a buffer" as any, -1, 5); + } catch (e) { + err = e; + } + expect({ code: err?.code, message: err?.message }).toEqual({ + code: "ERR_INVALID_ARG_TYPE", + message: expect.stringContaining('"buffer"'), + }); + // Same ordering when offset is a non-numeric type (validateInteger would also + // throw ERR_INVALID_ARG_TYPE, so assert the message names "buffer"). + err = undefined; + try { + readSync(0, 123 as any, "bad" as any, 5); + } catch (e) { + err = e; + } + expect({ code: err?.code, message: err?.message }).toEqual({ + code: "ERR_INVALID_ARG_TYPE", + message: expect.stringContaining('"buffer"'), + }); + }); + + it("throws ERR_INVALID_ARG_TYPE for a non-buffer before validating offset (async)", () => { + // Node throws validation errors synchronously from fs.read(). + let err: any; + try { + fs.read(0, "not a buffer" as any, -1, 5, 0, () => {}); + } catch (e) { + err = e; + } + expect({ code: err?.code, message: err?.message }).toEqual({ + code: "ERR_INVALID_ARG_TYPE", + message: expect.stringContaining('"buffer"'), + }); + }); + const firstFourBytes = new Uint32Array(new TextEncoder().encode("File").buffer)[0]; it("works on large files", () => {