From 419c4b2682f1e5435be89fbc4b950c3c30411526 Mon Sep 17 00:00:00 2001 From: zbarsky-openai Date: Mon, 24 Aug 2026 18:45:59 -0400 Subject: [PATCH] vm: provide isolated pseudo-terminals for each action Tools that need an interactive terminal currently fail inside actiond: ```text OSError: out of pty devices ``` A real remotely executed Python probe confirms there is no `/dev/ptmx`, no `/dev/pts`, and no `devpts` mount in the action chroot. Enable `CONFIG_UNIX98_PTYS=y` for both ARM64 and x86_64 guest kernels. Prepare `/dev/pts` and point `/dev/ptmx` at its instance-local `ptmx` node. Mount a fresh `devpts` filesystem with `newinstance`, `ptmxmode=0666`, `mode=0620`, `nosuid`, and `noexec` inside each action's existing private mount namespace before dropping privileges. `nodev` is intentionally omitted because pseudo-terminal device nodes must remain usable. No runtime package or shared host device is added. The real Linux Kconfig parser verifies both architectures, and the existing chroot unit test checks the new directory and relative symlink. The ARM64 kernel and guest initramfs were rebuilt through actiond itself; the restarted executor passes a real `pty.openpty()` read/write probe and the complete 1,224-test packaging suite. --- src/action_executor.zig | 6 ++++++ src/action_runner.zig | 8 ++++++++ vm/linux.config | 1 + vm/linux_x86_64.config | 1 + 4 files changed, 16 insertions(+) diff --git a/src/action_executor.zig b/src/action_executor.zig index 29a208f..9cf93e1 100644 --- a/src/action_executor.zig +++ b/src/action_executor.zig @@ -2110,6 +2110,8 @@ fn createOutputParent(io: std.Io, work_root: std.Io.Dir, path: []const u8) !void fn prepareChrootBaseDirs(io: std.Io, chroot_root: std.Io.Dir) !void { try chroot_root.createDirPath(io, "dev"); + try chroot_root.createDirPath(io, "dev/pts"); + try chroot_root.symLink(io, "pts/ptmx", "dev/ptmx", .{}); try chroot_root.createDirPath(io, "proc"); try chroot_root.createDirPath(io, "tmp"); try chroot_root.createDirPath(io, "var/tmp"); @@ -3641,6 +3643,10 @@ test "prepareChrootBaseDirs creates temporary directories" { defer work_dir.close(std.testing.io); try prepareChrootBaseDirs(std.testing.io, work_dir); + try work_dir.access(std.testing.io, "dev/pts", .{}); + var pseudo_terminal_target: [64]u8 = undefined; + const pseudo_terminal_target_len = try work_dir.readLink(std.testing.io, "dev/ptmx", &pseudo_terminal_target); + try std.testing.expectEqualStrings("pts/ptmx", pseudo_terminal_target[0..pseudo_terminal_target_len]); try work_dir.access(std.testing.io, "tmp", .{}); try work_dir.access(std.testing.io, "var/tmp", .{}); } diff --git a/src/action_runner.zig b/src/action_runner.zig index 0ecbb14..ba74608 100644 --- a/src/action_runner.zig +++ b/src/action_runner.zig @@ -648,6 +648,14 @@ fn forkAction(action: ForkAction) !std.os.linux.pid_t { childSyscallName(linux.chroot(action.chroot_dir.ptr), "chroot"); childSyscallName(linux.chdir("/"), "chdir_root"); childSyscallName(linux.mount("proc", "/proc", "proc", linux.MS.NOSUID | linux.MS.NODEV | linux.MS.NOEXEC, 0), "mount_proc"); + const pseudo_terminal_mount_data: [:0]const u8 = "newinstance,ptmxmode=0666,mode=0620"; + childSyscallName(linux.mount( + "devpts", + "/dev/pts", + "devpts", + linux.MS.NOSUID | linux.MS.NOEXEC, + @intFromPtr(pseudo_terminal_mount_data.ptr), + ), "mount_devpts"); childDropPrivileges(action.sandbox_uid, action.sandbox_gid); childSyscallName(linux.chdir(action.cwd.ptr), "chdir"); childInstallSocketFilter(); diff --git a/vm/linux.config b/vm/linux.config index 69abcd9..44e105b 100644 --- a/vm/linux.config +++ b/vm/linux.config @@ -79,6 +79,7 @@ CONFIG_VIRTIO_PCI=y CONFIG_VIRTIO_VSOCKETS=y CONFIG_VSOCKETS=y CONFIG_TTY=y +CONFIG_UNIX98_PTYS=y # CONFIG_BT is not set # CONFIG_EFI_PARTITION is not set # CONFIG_MODULES is not set diff --git a/vm/linux_x86_64.config b/vm/linux_x86_64.config index 278a71f..49413e7 100644 --- a/vm/linux_x86_64.config +++ b/vm/linux_x86_64.config @@ -70,6 +70,7 @@ CONFIG_VIRTIO_PCI=y CONFIG_VIRTIO_VSOCKETS=y CONFIG_VSOCKETS=y CONFIG_TTY=y +CONFIG_UNIX98_PTYS=y # CONFIG_BT is not set # CONFIG_EFI_PARTITION is not set # CONFIG_MODULES is not set