mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: Grigory <grigory.orlov.set@gmail.com> Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Co-authored-by: Meghan Denny <hello@nektro.net> Co-authored-by: Kenta Iwasaki <63115601+lithdew@users.noreply.github.com> Co-authored-by: John-David Dalton <john.david.dalton@gmail.com> Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Co-authored-by: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Co-authored-by: paperdave <paperdave@users.noreply.github.com> Co-authored-by: Georgijs Vilums <georgijs.vilums@gmail.com> Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
404 lines
16 KiB
Zig
404 lines
16 KiB
Zig
const std = @import("std");
|
|
const posix = std.posix;
|
|
|
|
const bun = @import("root").bun;
|
|
const env = bun.Environment;
|
|
const JSC = bun.JSC;
|
|
const JSValue = JSC.JSValue;
|
|
const libuv = bun.windows.libuv;
|
|
|
|
const allow_assert = env.allow_assert;
|
|
|
|
const log = bun.sys.syslog;
|
|
fn handleToNumber(handle: FDImpl.System) FDImpl.SystemAsInt {
|
|
if (env.os == .windows) {
|
|
// intCast fails if 'fd > 2^62'
|
|
// possible with handleToNumber(GetCurrentProcess());
|
|
return @intCast(@intFromPtr(handle));
|
|
} else {
|
|
return handle;
|
|
}
|
|
}
|
|
|
|
fn numberToHandle(handle: FDImpl.SystemAsInt) FDImpl.System {
|
|
if (env.os == .windows) {
|
|
if (!@inComptime()) {
|
|
bun.assert(handle != FDImpl.invalid_value);
|
|
}
|
|
return @ptrFromInt(handle);
|
|
} else {
|
|
return handle;
|
|
}
|
|
}
|
|
|
|
pub fn uv_get_osfhandle(in: c_int) libuv.uv_os_fd_t {
|
|
const out = libuv.uv_get_osfhandle(in);
|
|
return out;
|
|
}
|
|
|
|
pub fn uv_open_osfhandle(in: libuv.uv_os_fd_t) error{SystemFdQuotaExceeded}!c_int {
|
|
const out = libuv.uv_open_osfhandle(in);
|
|
bun.assert(out >= -1);
|
|
if (out == -1) return error.SystemFdQuotaExceeded;
|
|
return out;
|
|
}
|
|
|
|
/// Abstraction over file descriptors. This struct does nothing on non-windows operating systems.
|
|
///
|
|
/// bun.FileDescriptor is the bitcast of this struct, which is essentially a tagged pointer.
|
|
///
|
|
/// You can acquire one with FDImpl.decode(fd), and convert back to it with FDImpl.encode(fd).
|
|
///
|
|
/// On Windows builds we have two kinds of file descriptors:
|
|
/// - system: A "std.os.windows.HANDLE" that windows APIs can interact with.
|
|
/// In this case it is actually just an "*anyopaque" that points to some windows internals.
|
|
/// - uv: A libuv file descriptor that looks like a linux file descriptor.
|
|
/// (technically a c runtime file descriptor, libuv might do extra stuff though)
|
|
///
|
|
/// When converting UVFDs into Windows FDs, they are still said to be owned by libuv,
|
|
/// and they say to NOT close the handle.
|
|
pub const FDImpl = packed struct {
|
|
value: Value,
|
|
kind: Kind,
|
|
|
|
const invalid_value = std.math.maxInt(SystemAsInt);
|
|
pub const invalid = FDImpl{
|
|
.kind = .system,
|
|
.value = .{ .as_system = invalid_value },
|
|
};
|
|
|
|
pub const System = posix.fd_t;
|
|
|
|
pub const SystemAsInt = switch (env.os) {
|
|
.windows => u63,
|
|
else => System,
|
|
};
|
|
|
|
pub const UV = switch (env.os) {
|
|
.windows => bun.windows.libuv.uv_file,
|
|
else => System,
|
|
};
|
|
|
|
pub const Value = if (env.os == .windows)
|
|
packed union { as_system: SystemAsInt, as_uv: UV }
|
|
else
|
|
packed union { as_system: SystemAsInt };
|
|
|
|
pub const Kind = if (env.os == .windows)
|
|
enum(u1) { system = 0, uv = 1 }
|
|
else
|
|
enum(u0) { system };
|
|
|
|
comptime {
|
|
bun.assert(@sizeOf(FDImpl) == @sizeOf(System));
|
|
|
|
if (env.os == .windows) {
|
|
// we want the conversion from FD to fd_t to be a integer truncate
|
|
bun.assert(@as(FDImpl, @bitCast(@as(u64, 512))).value.as_system == 512);
|
|
}
|
|
}
|
|
|
|
pub fn fromSystemWithoutAssertion(system_fd: System) FDImpl {
|
|
return FDImpl{
|
|
.kind = .system,
|
|
.value = .{ .as_system = handleToNumber(system_fd) },
|
|
};
|
|
}
|
|
|
|
pub fn fromSystem(system_fd: System) FDImpl {
|
|
if (env.os == .windows) {
|
|
// the current process fd is max usize
|
|
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentprocess
|
|
bun.assert(@intFromPtr(system_fd) <= std.math.maxInt(SystemAsInt));
|
|
}
|
|
|
|
return fromSystemWithoutAssertion(system_fd);
|
|
}
|
|
|
|
pub fn fromUV(uv_fd: UV) FDImpl {
|
|
return switch (env.os) {
|
|
else => FDImpl{
|
|
.kind = .system,
|
|
.value = .{ .as_system = uv_fd },
|
|
},
|
|
.windows => FDImpl{
|
|
.kind = .uv,
|
|
.value = .{ .as_uv = uv_fd },
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn isValid(this: FDImpl) bool {
|
|
return switch (env.os) {
|
|
// the 'zero' value on posix is debatable. it can be standard in.
|
|
// TODO(@paperdave): steamroll away every use of bun.FileDescriptor.zero
|
|
else => this.value.as_system != invalid_value,
|
|
.windows => switch (this.kind) {
|
|
// zero is not allowed in addition to the invalid value (zero would be a null ptr)
|
|
.system => this.value.as_system != invalid_value and this.value.as_system != 0,
|
|
// the libuv tag is always fine
|
|
.uv => true,
|
|
},
|
|
};
|
|
}
|
|
|
|
/// When calling this function, you may not be able to close the returned fd.
|
|
/// To close the fd, you have to call `.close()` on the FD.
|
|
pub fn system(this: FDImpl) System {
|
|
return switch (env.os == .windows) {
|
|
false => numberToHandle(this.value.as_system),
|
|
true => switch (this.kind) {
|
|
.system => numberToHandle(this.value.as_system),
|
|
.uv => uv_get_osfhandle(this.value.as_uv),
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Convert to bun.FileDescriptor
|
|
pub fn encode(this: FDImpl) bun.FileDescriptor {
|
|
// https://github.com/ziglang/zig/issues/18462
|
|
return @enumFromInt(@as(bun.FileDescriptorInt, @bitCast(this)));
|
|
}
|
|
|
|
pub fn decode(fd: bun.FileDescriptor) FDImpl {
|
|
return @bitCast(@intFromEnum(fd));
|
|
}
|
|
|
|
/// When calling this function, you should consider the FD struct to now be invalid.
|
|
/// Calling `.close()` on the FD at that point may not work.
|
|
pub fn uv(this: FDImpl) UV {
|
|
return switch (env.os) {
|
|
else => numberToHandle(this.value.as_system),
|
|
.windows => switch (this.kind) {
|
|
.system => std.debug.panic(
|
|
\\Cast {} -> FDImpl.UV makes closing impossible!
|
|
\\
|
|
\\The supplier of this FileDescriptor should call 'bun.toLibUVOwnedFD'
|
|
\\or 'FDImpl.makeLibUVOwned', probably where open() was called.
|
|
,
|
|
.{this},
|
|
),
|
|
.uv => this.value.as_uv,
|
|
},
|
|
};
|
|
}
|
|
|
|
/// This function will prevent stdout and stderr from being closed.
|
|
pub fn close(this: FDImpl) ?bun.sys.Error {
|
|
if (env.os != .windows or this.kind == .uv) {
|
|
// This branch executes always on linux (uv() is no-op),
|
|
// or on Windows when given a UV file descriptor.
|
|
const fd = this.uv();
|
|
if (fd == 1 or fd == 2) {
|
|
log("close({}) SKIPPED", .{fd});
|
|
return null;
|
|
}
|
|
}
|
|
return this.closeAllowingStdoutAndStderr();
|
|
}
|
|
|
|
/// Assumes given a valid file descriptor
|
|
/// If error, the handle has not been closed
|
|
pub fn makeLibUVOwned(this: FDImpl) !FDImpl {
|
|
this.assertValid();
|
|
return switch (env.os) {
|
|
else => this,
|
|
.windows => switch (this.kind) {
|
|
.system => fd: {
|
|
break :fd FDImpl.fromUV(try uv_open_osfhandle(numberToHandle(this.value.as_system)));
|
|
},
|
|
.uv => this,
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn closeAllowingStdoutAndStderr(this: FDImpl) ?bun.sys.Error {
|
|
if (allow_assert) {
|
|
bun.assert(this.value.as_system != invalid_value); // probably a UAF
|
|
}
|
|
|
|
// Format the file descriptor for logging BEFORE closing it.
|
|
// Otherwise the file descriptor is always invalid after closing it.
|
|
var buf: if (env.isDebug) [1050]u8 else void = undefined;
|
|
const this_fmt = if (env.isDebug) std.fmt.bufPrint(&buf, "{}", .{this}) catch unreachable;
|
|
|
|
const result: ?bun.sys.Error = switch (env.os) {
|
|
.linux => result: {
|
|
const fd = this.encode();
|
|
bun.assert(fd != bun.invalid_fd);
|
|
bun.assert(fd.cast() >= 0);
|
|
break :result switch (bun.C.getErrno(bun.sys.system.close(fd.cast()))) {
|
|
.BADF => bun.sys.Error{ .errno = @intFromEnum(posix.E.BADF), .syscall = .close, .fd = fd },
|
|
else => null,
|
|
};
|
|
},
|
|
.mac => result: {
|
|
const fd = this.encode();
|
|
bun.assert(fd != bun.invalid_fd);
|
|
bun.assert(fd.cast() >= 0);
|
|
break :result switch (bun.C.getErrno(bun.sys.system.@"close$NOCANCEL"(fd.cast()))) {
|
|
.BADF => bun.sys.Error{ .errno = @intFromEnum(posix.E.BADF), .syscall = .close, .fd = fd },
|
|
else => null,
|
|
};
|
|
},
|
|
.windows => result: {
|
|
switch (this.kind) {
|
|
.uv => {
|
|
var req: libuv.fs_t = libuv.fs_t.uninitialized;
|
|
defer req.deinit();
|
|
const rc = libuv.uv_fs_close(libuv.Loop.get(), &req, this.value.as_uv, null);
|
|
break :result if (rc.errno()) |errno|
|
|
.{ .errno = errno, .syscall = .close, .fd = this.encode() }
|
|
else
|
|
null;
|
|
},
|
|
.system => {
|
|
bun.assert(this.value.as_system != 0);
|
|
const handle: System = @ptrFromInt(@as(u64, this.value.as_system));
|
|
break :result switch (bun.windows.NtClose(handle)) {
|
|
.SUCCESS => null,
|
|
else => |rc| bun.sys.Error{
|
|
.errno = if (bun.windows.Win32Error.fromNTStatus(rc).toSystemErrno()) |errno| @intFromEnum(errno) else 1,
|
|
.syscall = .CloseHandle,
|
|
.fd = this.encode(),
|
|
},
|
|
};
|
|
},
|
|
}
|
|
},
|
|
else => @compileError("FD.close() not implemented for this platform"),
|
|
};
|
|
|
|
if (env.isDebug) {
|
|
if (result) |err| {
|
|
if (err.errno == @intFromEnum(posix.E.BADF)) {
|
|
// TODO(@paperdave): Zig Compiler Bug, if you remove `this` from the log. An error is correctly printed, but with the wrong reference trace
|
|
bun.Output.debugWarn("close({s}) = EBADF. This is an indication of a file descriptor UAF", .{this_fmt});
|
|
} else {
|
|
log("close({s}) = {}", .{ this_fmt, err });
|
|
}
|
|
} else {
|
|
log("close({s})", .{this_fmt});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// This "fails" if not given an int32, returning null in that case
|
|
pub fn fromJS(value: JSValue) ?FDImpl {
|
|
if (!value.isInt32()) return null;
|
|
const fd = value.asInt32();
|
|
if (comptime env.isWindows) {
|
|
return switch (bun.FDTag.get(fd)) {
|
|
.stdin => FDImpl.decode(bun.STDIN_FD),
|
|
.stdout => FDImpl.decode(bun.STDOUT_FD),
|
|
.stderr => FDImpl.decode(bun.STDERR_FD),
|
|
else => FDImpl.fromUV(fd),
|
|
};
|
|
}
|
|
return FDImpl.fromUV(fd);
|
|
}
|
|
|
|
// If a non-number is given, returns null.
|
|
// If the given number is not an fd (negative), an error is thrown and error.JSException is returned.
|
|
pub fn fromJSValidated(value: JSValue, global: *JSC.JSGlobalObject, exception_ref: JSC.C.ExceptionRef) !?FDImpl {
|
|
if (!value.isInt32()) return null;
|
|
const fd = value.asInt32();
|
|
if (!JSC.Node.Valid.fileDescriptor(fd, global, exception_ref)) {
|
|
return error.JSException;
|
|
}
|
|
|
|
if (comptime env.isWindows) {
|
|
return switch (bun.FDTag.get(fd)) {
|
|
.stdin => FDImpl.decode(bun.STDIN_FD),
|
|
.stdout => FDImpl.decode(bun.STDOUT_FD),
|
|
.stderr => FDImpl.decode(bun.STDERR_FD),
|
|
else => FDImpl.fromUV(fd),
|
|
};
|
|
}
|
|
|
|
return FDImpl.fromUV(fd);
|
|
}
|
|
|
|
/// After calling, the input file descriptor is no longer valid and must not be used.
|
|
/// If an error is thrown, the file descriptor is cleaned up for you.
|
|
pub fn toJS(value: FDImpl, global: *JSC.JSGlobalObject) JSValue {
|
|
const fd = value.makeLibUVOwned() catch {
|
|
_ = value.close();
|
|
global.throwValue((JSC.SystemError{
|
|
.message = bun.String.static("EMFILE, too many open files"),
|
|
.code = bun.String.static("EMFILE"),
|
|
}).toErrorInstance(global));
|
|
return .zero;
|
|
};
|
|
return JSValue.jsNumberFromInt32(fd.uv());
|
|
}
|
|
|
|
pub fn format(this: FDImpl, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
|
if (!this.isValid()) {
|
|
try writer.writeAll("[invalid_fd]");
|
|
return;
|
|
}
|
|
|
|
if (fmt.len != 0) {
|
|
// The reason for this error is because formatting FD as an integer on windows is
|
|
// ambiguous and almost certainly a mistake. You probably meant to format fd.cast().
|
|
//
|
|
// Remember this formatter will
|
|
// - on posix, print the numebr
|
|
// - on windows, print if it is a handle or a libuv file descriptor
|
|
// - in debug on all platforms, print the path of the file descriptor
|
|
//
|
|
// Not having this error caused a linux+debug only crash in bun.sys.getFdPath because
|
|
// we forgot to change the thing being printed to "fd.cast()" when FDImpl was introduced.
|
|
@compileError("invalid format string for FDImpl.format. must be empty like '{}'");
|
|
}
|
|
|
|
switch (env.os) {
|
|
else => {
|
|
const fd = this.system();
|
|
try writer.print("{d}", .{fd});
|
|
if (env.isDebug and fd >= 3) print_with_path: {
|
|
var path_buf: bun.PathBuffer = undefined;
|
|
const path = std.os.getFdPath(fd, &path_buf) catch break :print_with_path;
|
|
try writer.print("[{s}]", .{path});
|
|
}
|
|
},
|
|
.windows => {
|
|
switch (this.kind) {
|
|
.system => {
|
|
if (env.isDebug) {
|
|
const peb = std.os.windows.peb();
|
|
const handle = this.system();
|
|
if (handle == peb.ProcessParameters.hStdInput) {
|
|
return try writer.print("{d}[stdin handle]", .{this.value.as_system});
|
|
} else if (handle == peb.ProcessParameters.hStdOutput) {
|
|
return try writer.print("{d}[stdout handle]", .{this.value.as_system});
|
|
} else if (handle == peb.ProcessParameters.hStdError) {
|
|
return try writer.print("{d}[stderr handle]", .{this.value.as_system});
|
|
} else if (handle == peb.ProcessParameters.CurrentDirectory.Handle) {
|
|
return try writer.print("{d}[cwd handle]", .{this.value.as_system});
|
|
} else print_with_path: {
|
|
var fd_path: bun.WPathBuffer = undefined;
|
|
const path = std.os.windows.GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &fd_path) catch break :print_with_path;
|
|
return try writer.print("{d}[{}]", .{
|
|
this.value.as_system,
|
|
bun.fmt.utf16(path),
|
|
});
|
|
}
|
|
}
|
|
|
|
try writer.print("{d}[handle]", .{this.value.as_system});
|
|
},
|
|
.uv => try writer.print("{d}[libuv]", .{this.value.as_uv}),
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn assertValid(this: FDImpl) void {
|
|
bun.assert(this.isValid());
|
|
}
|
|
};
|