Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ deno_core::extension!(deno_node,
ops::fs::op_node_statfs_sync,
ops::fs::op_node_statfs,
ops::fs::op_node_file_from_fd,
ops::fs::op_node_get_fd,
ops::fs::op_node_dup_fd,
ops::winerror::op_node_sys_to_uv_error,
ops::v8::op_v8_cached_data_version_tag,
ops::v8::op_v8_get_heap_statistics,
Expand Down Expand Up @@ -445,6 +447,7 @@ deno_core::extension!(deno_node,
"internal/fs/streams.mjs",
"internal/fs/utils.mjs",
"internal/fs/handle.ts",
"internal/fs/fd_map.ts",
"internal/hide_stack_frames.ts",
"internal/http.ts",
"internal/http2/util.ts",
Expand Down
92 changes: 92 additions & 0 deletions ext/node/ops/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,3 +574,95 @@ pub fn op_node_file_from_fd(
"op_node_file_from_fd is not supported on this platform",
)))
}

/// Create a file resource from a raw file descriptor by dup'ing it first.
/// This is safe for cross-worker use because the dup'd fd is independently
/// owned and can be closed without affecting the original.
#[cfg(unix)]
#[op2(fast)]
#[smi]
pub fn op_node_dup_fd(
Copy link
Member

Choose a reason for hiding this comment

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

This is a dangerous op. It allows you to create a Resource for any open fd that Deno has (e.g sqlite DB)

Copy link
Contributor Author

@fraidev fraidev Feb 4, 2026

Choose a reason for hiding this comment

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

Indeed, what do you think about separating the internal FDs? A separate set for (SQLite, etc.)

Copy link
Member

@littledivy littledivy Feb 5, 2026

Choose a reason for hiding this comment

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

I think the FD -> resource map should be in Rust so we can verify the fd exists in the map before calling dup() on it

Copy link
Member

Choose a reason for hiding this comment

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

FDs are assigned by the kernel - we can't have a different set for internal tasks.

state: &mut OpState,
#[smi] fd: i32,
) -> Result<ResourceId, FsError> {
use std::fs::File as StdFile;
use std::os::unix::io::FromRawFd;

if fd < 0 {
return Err(FsError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid file descriptor",
)));
}

// SAFETY: dup() creates a new fd pointing to the same open file description.
let new_fd = unsafe { libc::dup(fd) };
if new_fd < 0 {
return Err(FsError::Io(std::io::Error::last_os_error()));
}

// Clear O_NONBLOCK flag - the PTY fd might be in non-blocking mode,
// but StdFileResourceInner uses spawn_blocking for reads which expects
// blocking I/O. We need blocking reads so the read will wait for data.
// SAFETY: new_fd is valid, fcntl with F_GETFL/F_SETFL is safe
unsafe {
let flags = libc::fcntl(new_fd, libc::F_GETFL);
if flags >= 0 && (flags & libc::O_NONBLOCK) != 0 {
libc::fcntl(new_fd, libc::F_SETFL, flags & !libc::O_NONBLOCK);
}
}

// SAFETY: new_fd is a valid fd we just created via dup().
let std_file = unsafe { StdFile::from_raw_fd(new_fd) };
let file: Rc<dyn deno_io::fs::File> =
Rc::new(deno_io::StdFileResourceInner::file(std_file, None));
let rid = state
.resource_table
.add(FileResource::new(file, "fsFile".to_string()));
Ok(rid)
}

#[cfg(not(unix))]
#[op2(fast)]
#[smi]
pub fn op_node_dup_fd(
_state: &mut OpState,
#[smi] _fd: i32,
) -> Result<ResourceId, FsError> {
Err(FsError::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"op_node_dup_fd is not supported on this platform",
)))
}

/// Retrieves the OS file descriptor for a given resource ID.
#[cfg(unix)]
#[op2(fast)]
pub fn op_node_get_fd(
state: &mut OpState,
#[smi] rid: ResourceId,
) -> Result<i32, FsError> {
use deno_core::ResourceHandle;
let handle = state.resource_table.get_handle(rid).map_err(|_| {
FsError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Bad resource ID",
))
})?;
match handle {
ResourceHandle::Fd(fd) => Ok(fd),
_ => Err(FsError::Io(std::io::Error::other(
"Resource is not a file descriptor",
))),
}
}

/// On non-Unix platforms, return the RID as-is for now.
#[cfg(not(unix))]
#[op2(fast)]
pub fn op_node_get_fd(
_state: &mut OpState,
#[smi] rid: ResourceId,
) -> Result<i32, FsError> {
Ok(rid as i32)
}
13 changes: 7 additions & 6 deletions ext/node/polyfills/_fs/_fs_close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
makeCallback,
} from "ext:deno_node/_fs/_fs_common.ts";
import { getValidatedFd } from "ext:deno_node/internal/fs/utils.mjs";
import { getRid, unregisterFd } from "ext:deno_node/internal/fs/fd_map.ts";
import { core, primordials } from "ext:core/mod.js";

const {
Expand All @@ -29,9 +30,9 @@ export function close(
setTimeout(() => {
let error = null;
try {
// TODO(@littledivy): Treat `fd` as real file descriptor. `rid` is an
// implementation detail and may change.
core.close(fd);
const rid = getRid(fd);
core.close(rid);
unregisterFd(fd);
} catch (err) {
error = ObjectPrototypeIsPrototypeOf(ErrorPrototype, err)
? err as Error
Expand All @@ -43,7 +44,7 @@ export function close(

export function closeSync(fd: number) {
fd = getValidatedFd(fd);
// TODO(@littledivy): Treat `fd` as real file descriptor. `rid` is an
// implementation detail and may change.
core.close(fd);
const rid = getRid(fd);
core.close(rid);
unregisterFd(fd);
}
5 changes: 3 additions & 2 deletions ext/node/polyfills/_fs/_fs_fchmod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { op_fs_fchmod_async, op_fs_fchmod_sync } from "ext:core/ops";
import { primordials } from "ext:core/mod.js";
import { promisify } from "ext:deno_node/internal/util.mjs";
import { getRid } from "ext:deno_node/internal/fs/fd_map.ts";

const { PromisePrototypeThen } = primordials;

Expand All @@ -24,7 +25,7 @@ export function fchmod(
callback = makeCallback(callback);

PromisePrototypeThen(
op_fs_fchmod_async(fd, mode),
op_fs_fchmod_async(getRid(fd), mode),
() => callback(null),
callback,
);
Expand All @@ -33,7 +34,7 @@ export function fchmod(
export function fchmodSync(fd: number, mode: string | number) {
validateInteger(fd, "fd", 0, 2147483647);

op_fs_fchmod_sync(fd, parseFileMode(mode, "mode"));
op_fs_fchmod_sync(getRid(fd), parseFileMode(mode, "mode"));
}

export const fchmodPromise = promisify(fchmod) as (
Expand Down
5 changes: 3 additions & 2 deletions ext/node/polyfills/_fs/_fs_fchown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { promisify } from "ext:deno_node/internal/util.mjs";
import { primordials } from "ext:core/mod.js";

const { PromisePrototypeThen } = primordials;
import { getRid } from "ext:deno_node/internal/fs/fd_map.ts";

/**
* Changes the owner and group of a file.
Expand All @@ -27,7 +28,7 @@ export function fchown(
callback = makeCallback(callback);

PromisePrototypeThen(
op_fs_fchown_async(fd, uid, gid),
op_fs_fchown_async(getRid(fd), uid, gid),
() => callback(null),
callback,
);
Expand All @@ -45,7 +46,7 @@ export function fchownSync(
validateInteger(uid, "uid", -1, kMaxUserId);
validateInteger(gid, "gid", -1, kMaxUserId);

op_fs_fchown_sync(fd, uid, gid);
op_fs_fchown_sync(getRid(fd), uid, gid);
}

export const fchownPromise = promisify(fchown) as (
Expand Down
5 changes: 3 additions & 2 deletions ext/node/polyfills/_fs/_fs_fdatasync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FsFile } from "ext:deno_fs/30_fs.js";
import { promisify } from "ext:deno_node/internal/util.mjs";
import { validateInt32 } from "ext:deno_node/internal/validators.mjs";
import { primordials } from "ext:core/mod.js";
import { getRid } from "ext:deno_node/internal/fs/fd_map.ts";

const { PromisePrototypeThen, SymbolFor } = primordials;

Expand All @@ -14,15 +15,15 @@ export function fdatasync(
) {
validateInt32(fd, "fd", 0);
PromisePrototypeThen(
new FsFile(fd, SymbolFor("Deno.internal.FsFile")).syncData(),
new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).syncData(),
() => callback(null),
callback,
);
}

export function fdatasyncSync(fd: number) {
validateInt32(fd, "fd", 0);
new FsFile(fd, SymbolFor("Deno.internal.FsFile")).syncDataSync();
new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).syncDataSync();
}

export const fdatasyncPromise = promisify(fdatasync) as (
Expand Down
5 changes: 3 additions & 2 deletions ext/node/polyfills/_fs/_fs_fstat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "ext:deno_node/_fs/_fs_stat.ts";
import { FsFile } from "ext:deno_fs/30_fs.js";
import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts";
import { getRid } from "ext:deno_node/internal/fs/fd_map.ts";

export function fstat(fd: number, callback: statCallback): void;
export function fstat(
Expand Down Expand Up @@ -42,7 +43,7 @@ export function fstat(

if (!callback) throw new Error("No callback function supplied");

new FsFile(fd, Symbol.for("Deno.internal.FsFile")).stat().then(
new FsFile(getRid(fd), Symbol.for("Deno.internal.FsFile")).stat().then(
(stat) => callback(null, CFISBIS(stat, options.bigint)),
(err) => callback(denoErrorToNodeError(err, { syscall: "fstat" })),
);
Expand All @@ -62,7 +63,7 @@ export function fstatSync(
options?: statOptions,
): Stats | BigIntStats {
try {
const origin = new FsFile(fd, Symbol.for("Deno.internal.FsFile"))
const origin = new FsFile(getRid(fd), Symbol.for("Deno.internal.FsFile"))
.statSync();
return CFISBIS(origin, options?.bigint || false);
} catch (err) {
Expand Down
5 changes: 3 additions & 2 deletions ext/node/polyfills/_fs/_fs_fsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FsFile } from "ext:deno_fs/30_fs.js";
import { promisify } from "ext:deno_node/internal/util.mjs";
import { validateInt32 } from "ext:deno_node/internal/validators.mjs";
import { primordials } from "ext:core/mod.js";
import { getRid } from "ext:deno_node/internal/fs/fd_map.ts";

const { PromisePrototypeThen, SymbolFor } = primordials;

Expand All @@ -14,15 +15,15 @@ export function fsync(
) {
validateInt32(fd, "fd", 0);
PromisePrototypeThen(
new FsFile(fd, SymbolFor("Deno.internal.FsFile")).sync(),
new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).sync(),
() => callback(null),
callback,
);
}

export function fsyncSync(fd: number) {
validateInt32(fd, "fd", 0);
new FsFile(fd, SymbolFor("Deno.internal.FsFile")).syncSync();
new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).syncSync();
}

export const fsyncPromise = promisify(fsync) as (fd: number) => Promise<void>;
5 changes: 3 additions & 2 deletions ext/node/polyfills/_fs/_fs_ftruncate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { primordials } from "ext:core/mod.js";
import type { CallbackWithError } from "ext:deno_node/_fs/_fs_common.ts";
import { FsFile } from "ext:deno_fs/30_fs.js";
import { promisify } from "ext:deno_node/internal/util.mjs";
import { getRid } from "ext:deno_node/internal/fs/fd_map.ts";

const {
Error,
Expand All @@ -26,14 +27,14 @@ export function ftruncate(
if (!callback) throw new Error("No callback function supplied");

PromisePrototypeThen(
new FsFile(fd, SymbolFor("Deno.internal.FsFile")).truncate(len),
new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).truncate(len),
() => callback(null),
callback,
);
}

export function ftruncateSync(fd: number, len?: number) {
new FsFile(fd, SymbolFor("Deno.internal.FsFile")).truncateSync(len);
new FsFile(getRid(fd), SymbolFor("Deno.internal.FsFile")).truncateSync(len);
}

export const ftruncatePromise = promisify(ftruncate) as (
Expand Down
17 changes: 10 additions & 7 deletions ext/node/polyfills/_fs/_fs_futimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { validateInteger } from "ext:deno_node/internal/validators.mjs";
import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts";
import { toUnixTimestamp } from "ext:deno_node/internal/fs/utils.mjs";
import { promisify } from "ext:deno_node/internal/util.mjs";
import { getRid } from "ext:deno_node/internal/fs/fd_map.ts";

function getValidTime(
time: number | string | Date,
Expand Down Expand Up @@ -48,11 +49,11 @@ export function futimes(
atime = getValidTime(atime, "atime");
mtime = getValidTime(mtime, "mtime");

// TODO(@littledivy): Treat `fd` as real file descriptor.
new FsFile(fd, Symbol.for("Deno.internal.FsFile")).utime(atime, mtime).then(
() => callback(null),
callback,
);
new FsFile(getRid(fd), Symbol.for("Deno.internal.FsFile")).utime(atime, mtime)
.then(
() => callback(null),
callback,
);
}

export function futimesSync(
Expand All @@ -69,8 +70,10 @@ export function futimesSync(
atime = getValidTime(atime, "atime");
mtime = getValidTime(mtime, "mtime");

// TODO(@littledivy): Treat `fd` as real file descriptor.
new FsFile(fd, Symbol.for("Deno.internal.FsFile")).utimeSync(atime, mtime);
new FsFile(getRid(fd), Symbol.for("Deno.internal.FsFile")).utimeSync(
atime,
mtime,
);
}

export const futimesPromise = promisify(futimes) as (
Expand Down
14 changes: 11 additions & 3 deletions ext/node/polyfills/_fs/_fs_open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
import { FileHandle } from "ext:deno_node/internal/fs/handle.ts";
import type { Buffer } from "node:buffer";
import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts";
import { op_node_open, op_node_open_sync } from "ext:core/ops";
import { op_node_get_fd, op_node_open, op_node_open_sync } from "ext:core/ops";
import { registerFd } from "ext:deno_node/internal/fs/fd_map.ts";

const { Promise, PromisePrototypeThen } = primordials;

Expand Down Expand Up @@ -69,7 +70,11 @@ export function open(

PromisePrototypeThen(
op_node_open(path, flags, mode),
(rid: number) => callback(null, rid),
(rid: number) => {
const fd = op_node_get_fd(rid);
registerFd(fd, rid);
callback(null, fd);
},
(err: Error) =>
callback(denoErrorToNodeError(err, { syscall: "open", path })),
);
Expand Down Expand Up @@ -109,7 +114,10 @@ export function openSync(
const mode = parseFileMode(maybeMode, "mode", 0o666);

try {
return op_node_open_sync(path, flags, mode);
const rid = op_node_open_sync(path, flags, mode);
const fd = op_node_get_fd(rid);
registerFd(fd, rid);
return fd;
} catch (err) {
throw denoErrorToNodeError(err as Error, { syscall: "open", path });
}
Expand Down
Loading
Loading