Commit 46f7e3ea9f

Andrew Kelley <andrew@ziglang.org>
2025-10-23 22:58:49
std.Io.Threaded: add ioBasic which disables networking
1 parent 701d4bc
Changed files (11)
lib/std/fs/Dir.zig
@@ -849,14 +849,14 @@ pub fn close(self: *Dir) void {
 /// Deprecated in favor of `Io.Dir.openFile`.
 pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     return .adaptFromNewApi(try Io.Dir.openFile(self.adaptToNewApi(), io, sub_path, flags));
 }
 
 /// Deprecated in favor of `Io.Dir.createFile`.
 pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     const new_file = try Io.Dir.createFile(self.adaptToNewApi(), io, sub_path, flags);
     return .adaptFromNewApi(new_file);
 }
@@ -867,7 +867,7 @@ pub const MakeError = Io.Dir.MakeError;
 /// Deprecated in favor of `Io.Dir.makeDir`.
 pub fn makeDir(self: Dir, sub_path: []const u8) MakeError!void {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     return Io.Dir.makeDir(.{ .handle = self.fd }, io, sub_path);
 }
 
@@ -894,14 +894,14 @@ pub const MakePathError = Io.Dir.MakePathError;
 /// Deprecated in favor of `Io.Dir.makePathStatus`.
 pub fn makePathStatus(self: Dir, sub_path: []const u8) MakePathError!MakePathStatus {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     return Io.Dir.makePathStatus(.{ .handle = self.fd }, io, sub_path);
 }
 
 /// Deprecated in favor of `Io.Dir.makeOpenPath`.
 pub fn makeOpenPath(dir: Dir, sub_path: []const u8, options: OpenOptions) Io.Dir.MakeOpenPathError!Dir {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     return .adaptFromNewApi(try Io.Dir.makeOpenPath(dir.adaptToNewApi(), io, sub_path, options));
 }
 
@@ -1070,7 +1070,7 @@ pub const OpenOptions = Io.Dir.OpenOptions;
 /// Deprecated in favor of `Io.Dir.openDir`.
 pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args));
 }
 
@@ -1384,7 +1384,7 @@ pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 {
 /// Deprecated in favor of `Io.Dir.readFile`.
 pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     return Io.Dir.readFile(.{ .handle = self.fd }, io, file_path, buffer);
 }
 
@@ -1437,7 +1437,7 @@ pub fn readFileAllocOptions(
     comptime sentinel: ?u8,
 ) ReadFileAllocError!(if (sentinel) |s| [:s]align(alignment.toByteUnits()) u8 else []align(alignment.toByteUnits()) u8) {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
 
     var file = try dir.openFile(sub_path, .{});
     defer file.close();
@@ -1892,7 +1892,7 @@ pub const AccessError = Io.Dir.AccessError;
 /// Deprecated in favor of `Io.Dir.access`.
 pub fn access(self: Dir, sub_path: []const u8, options: Io.Dir.AccessOptions) AccessError!void {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     return Io.Dir.access(self.adaptToNewApi(), io, sub_path, options);
 }
 
@@ -1928,7 +1928,7 @@ pub fn copyFile(
     options: CopyFileOptions,
 ) CopyFileError!void {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
 
     const file = try source_dir.openFile(source_path, .{});
     var file_reader: File.Reader = .init(.{ .handle = file.handle }, io, &.{});
@@ -1996,7 +1996,7 @@ pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError
 /// Deprecated in favor of `Io.Dir.statPath`.
 pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     return Io.Dir.statPath(.{ .handle = self.fd }, io, sub_path, .{});
 }
 
lib/std/fs/File.zig
@@ -313,7 +313,7 @@ pub const StatError = posix.FStatError;
 /// Returns `Stat` containing basic information about the `File`.
 pub fn stat(self: File) StatError!Stat {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     return Io.File.stat(.{ .handle = self.handle }, io);
 }
 
lib/std/Io/Threaded.zig
@@ -168,80 +168,28 @@ pub fn io(t: *Threaded) Io {
             .conditionWaitUncancelable = conditionWaitUncancelable,
             .conditionWake = conditionWake,
 
-            .dirMake = switch (native_os) {
-                .windows => dirMakeWindows,
-                .wasi => dirMakeWasi,
-                else => dirMakePosix,
-            },
-            .dirMakePath = switch (native_os) {
-                .windows => dirMakePathWindows,
-                else => dirMakePathPosix,
-            },
-            .dirMakeOpenPath = switch (native_os) {
-                .windows => dirMakeOpenPathWindows,
-                .wasi => dirMakeOpenPathWasi,
-                else => dirMakeOpenPathPosix,
-            },
+            .dirMake = dirMake,
+            .dirMakePath = dirMakePath,
+            .dirMakeOpenPath = dirMakeOpenPath,
             .dirStat = dirStat,
-            .dirStatPath = switch (native_os) {
-                .linux => dirStatPathLinux,
-                .windows => dirStatPathWindows,
-                .wasi => dirStatPathWasi,
-                else => dirStatPathPosix,
-            },
-            .fileStat = switch (native_os) {
-                .linux => fileStatLinux,
-                .windows => fileStatWindows,
-                .wasi => fileStatWasi,
-                else => fileStatPosix,
-            },
-            .dirAccess = switch (native_os) {
-                .windows => dirAccessWindows,
-                .wasi => dirAccessWasi,
-                else => dirAccessPosix,
-            },
-            .dirCreateFile = switch (native_os) {
-                .windows => dirCreateFileWindows,
-                .wasi => dirCreateFileWasi,
-                else => dirCreateFilePosix,
-            },
-            .dirOpenFile = switch (native_os) {
-                .windows => dirOpenFileWindows,
-                .wasi => dirOpenFileWasi,
-                else => dirOpenFilePosix,
-            },
-            .dirOpenDir = switch (native_os) {
-                .wasi => dirOpenDirWasi,
-                .haiku => dirOpenDirHaiku,
-                else => dirOpenDirPosix,
-            },
+            .dirStatPath = dirStatPath,
+            .fileStat = fileStat,
+            .dirAccess = dirAccess,
+            .dirCreateFile = dirCreateFile,
+            .dirOpenFile = dirOpenFile,
+            .dirOpenDir = dirOpenDir,
             .dirClose = dirClose,
             .fileClose = fileClose,
             .fileWriteStreaming = fileWriteStreaming,
             .fileWritePositional = fileWritePositional,
-            .fileReadStreaming = switch (native_os) {
-                .windows => fileReadStreamingWindows,
-                else => fileReadStreamingPosix,
-            },
-            .fileReadPositional = switch (native_os) {
-                .windows => fileReadPositionalWindows,
-                else => fileReadPositionalPosix,
-            },
+            .fileReadStreaming = fileReadStreaming,
+            .fileReadPositional = fileReadPositional,
             .fileSeekBy = fileSeekBy,
             .fileSeekTo = fileSeekTo,
             .openSelfExe = openSelfExe,
 
-            .now = switch (native_os) {
-                .windows => nowWindows,
-                .wasi => nowWasi,
-                else => nowPosix,
-            },
-            .sleep = switch (native_os) {
-                .windows => sleepWindows,
-                .wasi => sleepWasi,
-                .linux => sleepLinux,
-                else => sleepPosix,
-            },
+            .now = now,
+            .sleep = sleep,
 
             .netListenIp = switch (native_os) {
                 .windows => netListenIpWindows,
@@ -291,6 +239,73 @@ pub fn io(t: *Threaded) Io {
     };
 }
 
+/// Same as `io` but disables all networking functionality, which has
+/// an additional dependency on Windows (ws2_32).
+pub fn ioBasic(t: *Threaded) Io {
+    return .{
+        .userdata = t,
+        .vtable = &.{
+            .async = async,
+            .concurrent = concurrent,
+            .await = await,
+            .cancel = cancel,
+            .cancelRequested = cancelRequested,
+            .select = select,
+
+            .groupAsync = groupAsync,
+            .groupWait = groupWait,
+            .groupWaitUncancelable = groupWaitUncancelable,
+            .groupCancel = groupCancel,
+
+            .mutexLock = mutexLock,
+            .mutexLockUncancelable = mutexLockUncancelable,
+            .mutexUnlock = mutexUnlock,
+
+            .conditionWait = conditionWait,
+            .conditionWaitUncancelable = conditionWaitUncancelable,
+            .conditionWake = conditionWake,
+
+            .dirMake = dirMake,
+            .dirMakePath = dirMakePath,
+            .dirMakeOpenPath = dirMakeOpenPath,
+            .dirStat = dirStat,
+            .dirStatPath = dirStatPath,
+            .fileStat = fileStat,
+            .dirAccess = dirAccess,
+            .dirCreateFile = dirCreateFile,
+            .dirOpenFile = dirOpenFile,
+            .dirOpenDir = dirOpenDir,
+            .dirClose = dirClose,
+            .fileClose = fileClose,
+            .fileWriteStreaming = fileWriteStreaming,
+            .fileWritePositional = fileWritePositional,
+            .fileReadStreaming = fileReadStreaming,
+            .fileReadPositional = fileReadPositional,
+            .fileSeekBy = fileSeekBy,
+            .fileSeekTo = fileSeekTo,
+            .openSelfExe = openSelfExe,
+
+            .now = now,
+            .sleep = sleep,
+
+            .netListenIp = netListenIpUnavailable,
+            .netListenUnix = netListenUnixUnavailable,
+            .netAccept = netAcceptUnavailable,
+            .netBindIp = netBindIpUnavailable,
+            .netConnectIp = netConnectIpUnavailable,
+            .netConnectUnix = netConnectUnixUnavailable,
+            .netClose = netCloseUnavailable,
+            .netRead = netReadUnavailable,
+            .netWrite = netWriteUnavailable,
+            .netSend = netSendUnavailable,
+            .netReceive = netReceiveUnavailable,
+            .netInterfaceNameResolve = netInterfaceNameResolveUnavailable,
+            .netInterfaceName = netInterfaceNameUnavailable,
+            .netLookup = netLookupUnavailable,
+        },
+    };
+}
+
 pub const socket_flags_unsupported = native_os.isDarwin() or native_os == .haiku; // 💩💩
 const have_accept4 = !socket_flags_unsupported;
 const have_flock_open_flags = @hasField(posix.O, "EXLOCK");
@@ -804,7 +819,7 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut
 fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void {
     if (builtin.single_threaded) unreachable; // Deadlock.
     const t: *Threaded = @ptrCast(@alignCast(userdata));
-    const t_io = t.io();
+    const t_io = ioBasic(t);
     comptime assert(@TypeOf(cond.state) == u64);
     const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
     const cond_state = &ints[0];
@@ -835,6 +850,7 @@ fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex:
 fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void {
     if (builtin.single_threaded) unreachable; // Deadlock.
     const t: *Threaded = @ptrCast(@alignCast(userdata));
+    const t_io = ioBasic(t);
     comptime assert(@TypeOf(cond.state) == u64);
     const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
     const cond_state = &ints[0];
@@ -858,8 +874,8 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I
     assert(state & waiter_mask != waiter_mask);
     state += one_waiter;
 
-    mutex.unlock(t.io());
-    defer mutex.lockUncancelable(t.io());
+    mutex.unlock(t_io);
+    defer mutex.lockUncancelable(t_io);
 
     while (true) {
         try futexWait(t, cond_epoch, epoch);
@@ -939,6 +955,12 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.
     }
 }
 
+const dirMake = switch (native_os) {
+    .windows => dirMakeWindows,
+    .wasi => dirMakeWasi,
+    else => dirMakePosix,
+};
+
 fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
 
@@ -1027,6 +1049,11 @@ fn dirMakeWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode
     windows.CloseHandle(sub_dir_handle);
 }
 
+const dirMakePath = switch (native_os) {
+    .windows => dirMakePathWindows,
+    else => dirMakePathPosix,
+};
+
 fn dirMakePathPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
     _ = t;
@@ -1045,6 +1072,12 @@ fn dirMakePathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8,
     @panic("TODO implement dirMakePathWindows");
 }
 
+const dirMakeOpenPath = switch (native_os) {
+    .windows => dirMakeOpenPathWindows,
+    .wasi => dirMakeOpenPathWasi,
+    else => dirMakeOpenPathPosix,
+};
+
 fn dirMakeOpenPathPosix(
     userdata: ?*anyopaque,
     dir: Io.Dir,
@@ -1052,11 +1085,11 @@ fn dirMakeOpenPathPosix(
     options: Io.Dir.OpenOptions,
 ) Io.Dir.MakeOpenPathError!Io.Dir {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
-    const t_io = t.io();
-    return dir.openDir(t_io, sub_path, options) catch |err| switch (err) {
+    const t_io = ioBasic(t);
+    return dirOpenDirPosix(t, dir, sub_path, options) catch |err| switch (err) {
         error.FileNotFound => {
             try dir.makePath(t_io, sub_path);
-            return dir.openDir(t_io, sub_path, options);
+            return dirOpenDirPosix(t, dir, sub_path, options);
         },
         else => |e| return e,
     };
@@ -1135,7 +1168,7 @@ fn dirMakeOpenPathWindows(
                 // could cause an infinite loop
                 check_dir: {
                     // workaround for windows, see https://github.com/ziglang/zig/issues/16738
-                    const fstat = dir.statPath(t.io(), component.path, .{
+                    const fstat = dirStatPathWindows(t, dir, component.path, .{
                         .follow_symlinks = options.follow_symlinks,
                     }) catch |stat_err| switch (stat_err) {
                         error.IsDir => break :check_dir,
@@ -1187,6 +1220,13 @@ fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat {
     @panic("TODO implement dirStat");
 }
 
+const dirStatPath = switch (native_os) {
+    .linux => dirStatPathLinux,
+    .windows => dirStatPathWindows,
+    .wasi => dirStatPathWasi,
+    else => dirStatPathPosix,
+};
+
 fn dirStatPathLinux(
     userdata: ?*anyopaque,
     dir: Io.Dir,
@@ -1275,12 +1315,11 @@ fn dirStatPathWindows(
     options: Io.Dir.StatPathOptions,
 ) Io.Dir.StatPathError!Io.File.Stat {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
-    const t_io = t.io();
-    var file = try dir.openFile(t_io, sub_path, .{
+    const file = try dirOpenFileWindows(t, dir, sub_path, .{
         .follow_symlinks = options.follow_symlinks,
     });
-    defer file.close(t_io);
-    return file.stat(t_io);
+    defer windows.CloseHandle(file.handle);
+    return fileStatWindows(t, file);
 }
 
 fn dirStatPathWasi(
@@ -1318,6 +1357,13 @@ fn dirStatPathWasi(
     }
 }
 
+const fileStat = switch (native_os) {
+    .linux => fileStatLinux,
+    .windows => fileStatWindows,
+    .wasi => fileStatWasi,
+    else => fileStatPosix,
+};
+
 fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
 
@@ -1440,6 +1486,12 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.
     }
 }
 
+const dirAccess = switch (native_os) {
+    .windows => dirAccessWindows,
+    .wasi => dirAccessWasi,
+    else => dirAccessPosix,
+};
+
 fn dirAccessPosix(
     userdata: ?*anyopaque,
     dir: Io.Dir,
@@ -1589,6 +1641,12 @@ fn dirAccessWindows(
     }
 }
 
+const dirCreateFile = switch (native_os) {
+    .windows => dirCreateFileWindows,
+    .wasi => dirCreateFileWasi,
+    else => dirCreateFilePosix,
+};
+
 fn dirCreateFilePosix(
     userdata: ?*anyopaque,
     dir: Io.Dir,
@@ -1827,6 +1885,12 @@ fn dirCreateFileWasi(
     }
 }
 
+const dirOpenFile = switch (native_os) {
+    .windows => dirOpenFileWindows,
+    .wasi => dirOpenFileWasi,
+    else => dirOpenFilePosix,
+};
+
 fn dirOpenFilePosix(
     userdata: ?*anyopaque,
     dir: Io.Dir,
@@ -2077,6 +2141,12 @@ fn dirOpenFileWasi(
     }
 }
 
+const dirOpenDir = switch (native_os) {
+    .wasi => dirOpenDirWasi,
+    .haiku => dirOpenDirHaiku,
+    else => dirOpenDirPosix,
+};
+
 fn dirOpenDirPosix(
     userdata: ?*anyopaque,
     dir: Io.Dir,
@@ -2320,6 +2390,11 @@ fn fileClose(userdata: ?*anyopaque, file: Io.File) void {
     posix.close(file.handle);
 }
 
+const fileReadStreaming = switch (native_os) {
+    .windows => fileReadStreamingWindows,
+    else => fileReadStreamingPosix,
+};
+
 fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
 
@@ -2482,6 +2557,11 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8, o
     }
 }
 
+const fileReadPositional = switch (native_os) {
+    .windows => fileReadPositionalWindows,
+    else => fileReadPositionalPosix,
+};
+
 fn fileReadPositionalWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
     try t.checkCancel();
@@ -2652,6 +2732,12 @@ fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp
     }
 }
 
+const now = switch (native_os) {
+    .windows => nowWindows,
+    .wasi => nowWasi,
+    else => nowPosix,
+};
+
 fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
     _ = t;
@@ -2680,6 +2766,13 @@ fn nowWasi(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
     return .fromNanoseconds(ns);
 }
 
+const sleep = switch (native_os) {
+    .windows => sleepWindows,
+    .wasi => sleepWasi,
+    .linux => sleepLinux,
+    else => sleepPosix,
+};
+
 fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
     const clock_id: posix.clockid_t = clockToPosix(switch (timeout) {
@@ -2710,9 +2803,10 @@ fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
 
 fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
+    const t_io = ioBasic(t);
     try t.checkCancel();
     const ms = ms: {
-        const d = (try timeout.toDurationFromNow(t.io())) orelse
+        const d = (try timeout.toDurationFromNow(t_io)) orelse
             break :ms std.math.maxInt(windows.DWORD);
         break :ms std.math.lossyCast(windows.DWORD, d.raw.toMilliseconds());
     };
@@ -2721,11 +2815,12 @@ fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
 
 fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
+    const t_io = ioBasic(t);
     try t.checkCancel();
 
     const w = std.os.wasi;
 
-    const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(t.io())) |d| .{
+    const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(t_io)) |d| .{
         .id = clockToWasi(d.clock),
         .timeout = std.math.lossyCast(u64, d.raw.nanoseconds),
         .precision = 0,
@@ -2750,11 +2845,12 @@ fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
 
 fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
+    const t_io = ioBasic(t);
     const sec_type = @typeInfo(posix.timespec).@"struct".fields[0].type;
     const nsec_type = @typeInfo(posix.timespec).@"struct".fields[1].type;
 
     var timespec: posix.timespec = t: {
-        const d = (try timeout.toDurationFromNow(t.io())) orelse break :t .{
+        const d = (try timeout.toDurationFromNow(t_io)) orelse break :t .{
             .sec = std.math.maxInt(sec_type),
             .nsec = std.math.maxInt(nsec_type),
         };
@@ -2919,6 +3015,17 @@ fn netListenIpWindows(
     };
 }
 
+fn netListenIpUnavailable(
+    userdata: ?*anyopaque,
+    address: IpAddress,
+    options: IpAddress.ListenOptions,
+) IpAddress.ListenError!net.Server {
+    _ = userdata;
+    _ = address;
+    _ = options;
+    return error.NetworkDown;
+}
+
 fn netListenUnixPosix(
     userdata: ?*anyopaque,
     address: *const net.UnixAddress,
@@ -2965,6 +3072,17 @@ fn netListenUnixWindows(
     @panic("TODO implement netListenUnixWindows");
 }
 
+fn netListenUnixUnavailable(
+    userdata: ?*anyopaque,
+    address: *const net.UnixAddress,
+    options: net.UnixAddress.ListenOptions,
+) net.UnixAddress.ListenError!net.Socket.Handle {
+    _ = userdata;
+    _ = address;
+    _ = options;
+    return error.AddressFamilyUnsupported;
+}
+
 fn posixBindUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void {
     while (true) {
         try t.checkCancel();
@@ -3235,6 +3353,17 @@ fn netConnectIpWindows(
     } };
 }
 
+fn netConnectIpUnavailable(
+    userdata: ?*anyopaque,
+    address: *const IpAddress,
+    options: IpAddress.ConnectOptions,
+) IpAddress.ConnectError!net.Stream {
+    _ = userdata;
+    _ = address;
+    _ = options;
+    return error.NetworkDown;
+}
+
 fn netConnectUnixPosix(
     userdata: ?*anyopaque,
     address: *const net.UnixAddress,
@@ -3263,6 +3392,15 @@ fn netConnectUnixWindows(
     @panic("TODO implement netConnectUnixWindows");
 }
 
+fn netConnectUnixUnavailable(
+    userdata: ?*anyopaque,
+    address: *const net.UnixAddress,
+) net.UnixAddress.ConnectError!net.Socket.Handle {
+    _ = userdata;
+    _ = address;
+    return error.AddressFamilyUnsupported;
+}
+
 fn netBindIpPosix(
     userdata: ?*anyopaque,
     address: *const IpAddress,
@@ -3330,6 +3468,17 @@ fn netBindIpWindows(
     };
 }
 
+fn netBindIpUnavailable(
+    userdata: ?*anyopaque,
+    address: *const IpAddress,
+    options: IpAddress.BindOptions,
+) IpAddress.BindError!net.Socket {
+    _ = userdata;
+    _ = address;
+    _ = options;
+    return error.NetworkDown;
+}
+
 fn openSocketPosix(
     t: *Threaded,
     family: posix.sa_family_t,
@@ -3498,7 +3647,14 @@ fn netAcceptWindows(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net
     }
 }
 
+fn netAcceptUnavailable(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net.Server.AcceptError!net.Stream {
+    _ = userdata;
+    _ = listen_handle;
+    return error.NetworkDown;
+}
+
 fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize {
+    if (!have_networking) return error.NetworkDown;
     const t: *Threaded = @ptrCast(@alignCast(userdata));
 
     var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined;
@@ -3560,7 +3716,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.
 }
 
 fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize {
-    if (!have_networking) return .{ error.NetworkDown, 0 };
+    if (!have_networking) return error.NetworkDown;
     const t: *Threaded = @ptrCast(@alignCast(userdata));
     _ = t;
     _ = handle;
@@ -3568,6 +3724,13 @@ fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8
     @panic("TODO implement netReadWindows");
 }
 
+fn netReadUnavailable(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize {
+    _ = userdata;
+    _ = fd;
+    _ = data;
+    return error.NetworkDown;
+}
+
 fn netSendPosix(
     userdata: ?*anyopaque,
     handle: net.Socket.Handle,
@@ -3612,6 +3775,19 @@ fn netSendWindows(
     @panic("TODO netSendWindows");
 }
 
+fn netSendUnavailable(
+    userdata: ?*anyopaque,
+    handle: net.Socket.Handle,
+    messages: []net.OutgoingMessage,
+    flags: net.SendFlags,
+) struct { ?net.Socket.SendError, usize } {
+    _ = userdata;
+    _ = handle;
+    _ = messages;
+    _ = flags;
+    return .{ error.NetworkDown, 0 };
+}
+
 fn netSendOne(
     t: *Threaded,
     handle: net.Socket.Handle,
@@ -3777,6 +3953,7 @@ fn netReceivePosix(
 ) struct { ?net.Socket.ReceiveTimeoutError, usize } {
     if (!have_networking) return .{ error.NetworkDown, 0 };
     const t: *Threaded = @ptrCast(@alignCast(userdata));
+    const t_io = io(t);
 
     // recvmmsg is useless, here's why:
     // * [timeout bug](https://bugzilla.kernel.org/show_bug.cgi?id=75371)
@@ -3803,7 +3980,7 @@ fn netReceivePosix(
     var message_i: usize = 0;
     var data_i: usize = 0;
 
-    const deadline = timeout.toDeadline(t.io()) catch |err| return .{ err, message_i };
+    const deadline = timeout.toDeadline(t_io) catch |err| return .{ err, message_i };
 
     recv: while (true) {
         t.checkCancel() catch |err| return .{ err, message_i };
@@ -3849,7 +4026,7 @@ fn netReceivePosix(
 
                 const max_poll_ms = std.math.maxInt(u31);
                 const timeout_ms: u31 = if (deadline) |d| t: {
-                    const duration = d.durationFromNow(t.io()) catch |err| return .{ err, message_i };
+                    const duration = d.durationFromNow(t_io) catch |err| return .{ err, message_i };
                     if (duration.raw.nanoseconds <= 0) return .{ error.Timeout, message_i };
                     break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds()));
                 } else max_poll_ms;
@@ -3915,6 +4092,23 @@ fn netReceiveWindows(
     @panic("TODO implement netReceiveWindows");
 }
 
+fn netReceiveUnavailable(
+    userdata: ?*anyopaque,
+    handle: net.Socket.Handle,
+    message_buffer: []net.IncomingMessage,
+    data_buffer: []u8,
+    flags: net.ReceiveFlags,
+    timeout: Io.Timeout,
+) struct { ?net.Socket.ReceiveTimeoutError, usize } {
+    _ = userdata;
+    _ = handle;
+    _ = message_buffer;
+    _ = data_buffer;
+    _ = flags;
+    _ = timeout;
+    return .{ error.NetworkDown, 0 };
+}
+
 fn netWritePosix(
     userdata: ?*anyopaque,
     fd: net.Socket.Handle,
@@ -4013,6 +4207,21 @@ fn netWriteWindows(
     @panic("TODO implement netWriteWindows");
 }
 
+fn netWriteUnavailable(
+    userdata: ?*anyopaque,
+    handle: net.Socket.Handle,
+    header: []const u8,
+    data: []const []const u8,
+    splat: usize,
+) net.Stream.Writer.Error!usize {
+    _ = userdata;
+    _ = handle;
+    _ = header;
+    _ = data;
+    _ = splat;
+    return error.NetworkDown;
+}
+
 fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void {
     // OS checks ptr addr before length so zero length vectors must be omitted.
     if (bytes.len == 0) return;
@@ -4030,6 +4239,12 @@ fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void {
     }
 }
 
+fn netCloseUnavailable(userdata: ?*anyopaque, handle: net.Socket.Handle) void {
+    _ = userdata;
+    _ = handle;
+    unreachable; // How you gonna close something that was impossible to open?
+}
+
 fn netInterfaceNameResolve(
     userdata: ?*anyopaque,
     name: *const net.Interface.Name,
@@ -4089,6 +4304,15 @@ fn netInterfaceNameResolve(
     @panic("unimplemented");
 }
 
+fn netInterfaceNameResolveUnavailable(
+    userdata: ?*anyopaque,
+    name: *const net.Interface.Name,
+) net.Interface.Name.ResolveError!net.Interface {
+    _ = userdata;
+    _ = name;
+    return error.InterfaceNotFound;
+}
+
 fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
     try t.checkCancel();
@@ -4109,6 +4333,12 @@ fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interfa
     @panic("unimplemented");
 }
 
+fn netInterfaceNameUnavailable(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name {
+    _ = userdata;
+    _ = interface;
+    return error.Unexpected;
+}
+
 fn netLookup(
     userdata: ?*anyopaque,
     host_name: HostName,
@@ -4116,17 +4346,31 @@ fn netLookup(
     options: HostName.LookupOptions,
 ) void {
     const t: *Threaded = @ptrCast(@alignCast(userdata));
-    const t_io = t.io();
+    const t_io = io(t);
     resolved.putOneUncancelable(t_io, .{ .end = netLookupFallible(t, host_name, resolved, options) });
 }
 
+fn netLookupUnavailable(
+    userdata: ?*anyopaque,
+    host_name: HostName,
+    resolved: *Io.Queue(HostName.LookupResult),
+    options: HostName.LookupOptions,
+) void {
+    _ = host_name;
+    _ = options;
+    const t: *Threaded = @ptrCast(@alignCast(userdata));
+    const t_io = ioBasic(t);
+    resolved.putOneUncancelable(t_io, .{ .end = error.NetworkDown });
+}
+
 fn netLookupFallible(
     t: *Threaded,
     host_name: HostName,
     resolved: *Io.Queue(HostName.LookupResult),
     options: HostName.LookupOptions,
 ) !void {
-    const t_io = t.io();
+    if (!have_networking) return error.NetworkDown;
+    const t_io = io(t);
     const name = host_name.bytes;
     assert(name.len <= HostName.max_len);
 
@@ -4637,7 +4881,7 @@ fn lookupDnsSearch(
     resolved: *Io.Queue(HostName.LookupResult),
     options: HostName.LookupOptions,
 ) HostName.LookupError!void {
-    const t_io = t.io();
+    const t_io = io(t);
     const rc = HostName.ResolvConf.init(t_io) catch return error.ResolvConfParseFailed;
 
     // Count dots, suppress search when >=ndots or name ends in
@@ -4681,7 +4925,7 @@ fn lookupDns(
     resolved: *Io.Queue(HostName.LookupResult),
     options: HostName.LookupOptions,
 ) HostName.LookupError!void {
-    const t_io = t.io();
+    const t_io = io(t);
     const family_records: [2]struct { af: IpAddress.Family, rr: HostName.DnsRecord } = .{
         .{ .af = .ip6, .rr = .A },
         .{ .af = .ip4, .rr = .AAAA },
@@ -4868,7 +5112,7 @@ fn lookupHosts(
     resolved: *Io.Queue(HostName.LookupResult),
     options: HostName.LookupOptions,
 ) !void {
-    const t_io = t.io();
+    const t_io = io(t);
     const file = Io.File.openAbsolute(t_io, "/etc/hosts", .{}) catch |err| switch (err) {
         error.FileNotFound,
         error.NotDir,
@@ -4906,7 +5150,7 @@ fn lookupHostsReader(
     options: HostName.LookupOptions,
     reader: *Io.Reader,
 ) error{ ReadFailed, Canceled, UnknownHostName }!void {
-    const t_io = t.io();
+    const t_io = io(t);
     var addresses_len: usize = 0;
     var canonical_name: ?HostName = null;
     while (true) {
@@ -5374,7 +5618,7 @@ const Wsa = struct {
 };
 
 fn initializeWsa(t: *Threaded) error{NetworkDown}!void {
-    const t_io = t.io();
+    const t_io = io(t);
     const wsa = &t.wsa;
     wsa.mutex.lockUncancelable(t_io);
     defer wsa.mutex.unlock(t_io);
lib/std/process/Child.zig
@@ -1089,7 +1089,7 @@ fn windowsCreateProcessPathExt(
     // opening function knowing which implementation we are in. Here, we imitate
     // that scenario.
     var threaded: std.Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
 
     var dir = dir: {
         // needs to be null-terminated
lib/std/debug.zig
@@ -649,7 +649,7 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf:
 /// See `captureCurrentStackTrace` to capture the trace addresses into a buffer instead of printing.
 pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_config: tty.Config) Writer.Error!void {
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
 
     if (!std.options.allow_stack_tracing) {
         tty_config.setColor(writer, .dim) catch {};
@@ -780,7 +780,7 @@ pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.C
     // We use an independent Io implementation here in case there was a problem
     // with the application's Io implementation itself.
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
 
     // Fetch `st.index` straight away. Aside from avoiding redundant loads, this prevents issues if
     // `st` is `@errorReturnTrace()` and errors are encountered while writing the stack trace.
@@ -1602,7 +1602,7 @@ test "manage resources correctly" {
     };
     const gpa = std.testing.allocator;
     var threaded: Io.Threaded = .init_single_threaded;
-    const io = threaded.io();
+    const io = threaded.ioBasic();
     var discarding: Io.Writer.Discarding = .init(&.{});
     var di: SelfInfo = .init;
     defer di.deinit(gpa);
lib/std/fs.zig
@@ -340,7 +340,7 @@ pub const OpenSelfExeError = Io.File.OpenSelfExeError;
 pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File {
     if (native_os == .linux or native_os == .serenity or native_os == .windows) {
         var threaded: Io.Threaded = .init_single_threaded;
-        const io = threaded.io();
+        const io = threaded.ioBasic();
         return .adaptFromNewApi(try Io.File.openSelfExe(io, flags));
     }
     // Use of max_path_bytes here is valid as the resulting path is immediately
lib/std/Thread.zig
@@ -320,7 +320,7 @@ pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]co
             const path = try std.fmt.bufPrint(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()});
 
             var threaded: std.Io.Threaded = .init_single_threaded;
-            const io = threaded.io();
+            const io = threaded.ioBasic();
 
             const file = try std.fs.cwd().openFile(path, .{});
             defer file.close();
test/standalone/child_process/child.zig
@@ -1,4 +1,5 @@
 const std = @import("std");
+const Io = std.Io;
 
 // 42 is expected by parent; other values result in test failure
 var exit_code: u8 = 42;
@@ -6,12 +7,17 @@ var exit_code: u8 = 42;
 pub fn main() !void {
     var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
     const arena = arena_state.allocator();
-    try run(arena);
+
+    var threaded: std.Io.Threaded = .init(arena);
+    defer threaded.deinit();
+    const io = threaded.io();
+
+    try run(arena, io);
     arena_state.deinit();
     std.process.exit(exit_code);
 }
 
-fn run(allocator: std.mem.Allocator) !void {
+fn run(allocator: std.mem.Allocator, io: Io) !void {
     var args = try std.process.argsWithAllocator(allocator);
     defer args.deinit();
     _ = args.next() orelse unreachable; // skip binary name
@@ -33,7 +39,8 @@ fn run(allocator: std.mem.Allocator) !void {
     const hello_stdin = "hello from stdin";
     var buf: [hello_stdin.len]u8 = undefined;
     const stdin: std.fs.File = .stdin();
-    const n = try stdin.readAll(&buf);
+    var reader = stdin.reader(io, &.{});
+    const n = try reader.interface.readSliceShort(&buf);
     if (!std.mem.eql(u8, buf[0..n], hello_stdin)) {
         testError("stdin: '{s}'; want '{s}'", .{ buf[0..n], hello_stdin });
     }
test/standalone/simple/cat/main.zig
@@ -9,6 +9,10 @@ pub fn main() !void {
     defer arena_instance.deinit();
     const arena = arena_instance.allocator();
 
+    var threaded: std.Io.Threaded = .init(arena);
+    defer threaded.deinit();
+    const io = threaded.io();
+
     const args = try std.process.argsAlloc(arena);
 
     const exe = args[0];
@@ -16,7 +20,7 @@ pub fn main() !void {
     var stdout_buffer: [4096]u8 = undefined;
     var stdout_writer = fs.File.stdout().writerStreaming(&stdout_buffer);
     const stdout = &stdout_writer.interface;
-    var stdin_reader = fs.File.stdin().readerStreaming(&.{});
+    var stdin_reader = fs.File.stdin().readerStreaming(io, &.{});
 
     const cwd = fs.cwd();
 
@@ -32,7 +36,7 @@ pub fn main() !void {
             defer file.close();
 
             catted_anything = true;
-            var file_reader = file.reader(&.{});
+            var file_reader = file.reader(io, &.{});
             _ = try stdout.sendFileAll(&file_reader, .unlimited);
             try stdout.flush();
         }
tools/fetch_them_macos_headers.zig
@@ -1,4 +1,5 @@
 const std = @import("std");
+const Io = std.Io;
 const fs = std.fs;
 const mem = std.mem;
 const process = std.process;
@@ -85,7 +86,7 @@ pub fn main() anyerror!void {
         } else try argv.append(arg);
     }
 
-    var threaded: std.Io.Threaded = .init(gpa);
+    var threaded: Io.Threaded = .init(gpa);
     defer threaded.deinit();
     const io = threaded.io();
 
@@ -118,12 +119,13 @@ pub fn main() anyerror!void {
             .arch = arch,
             .os_ver = os_ver,
         };
-        try fetchTarget(allocator, argv.items, sysroot_path, target, version, tmp);
+        try fetchTarget(allocator, io, argv.items, sysroot_path, target, version, tmp);
     }
 }
 
 fn fetchTarget(
     arena: Allocator,
+    io: Io,
     args: []const []const u8,
     sysroot: []const u8,
     target: Target,
@@ -194,7 +196,7 @@ fn fetchTarget(
     var dirs = std.StringHashMap(fs.Dir).init(arena);
     try dirs.putNoClobber(".", dest_dir);
 
-    var headers_list_file_reader = headers_list_file.reader(&.{});
+    var headers_list_file_reader = headers_list_file.reader(io, &.{});
     const headers_list_str = try headers_list_file_reader.interface.allocRemaining(arena, .unlimited);
     const prefix = "/usr/include";
 
@@ -267,8 +269,8 @@ const Version = struct {
 
     pub fn format(
         v: Version,
-        writer: *std.Io.Writer,
-    ) std.Io.Writer.Error!void {
+        writer: *Io.Writer,
+    ) Io.Writer.Error!void {
         try writer.print("{d}.{d}.{d}", .{ v.major, v.minor, v.patch });
     }
 };
tools/gen_macos_headers_c.zig
@@ -73,7 +73,7 @@ fn findHeaders(
         switch (entry.kind) {
             .directory => {
                 const path = try std.fs.path.join(arena, &.{ prefix, entry.name });
-                var subdir = try dir.openDir(entry.name, .{ .no_follow = true });
+                var subdir = try dir.openDir(entry.name, .{ .follow_symlinks = false });
                 defer subdir.close();
                 try findHeaders(arena, subdir, path, paths);
             },