lib/sys/src/pty.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const builtin = @import("builtin");
  3 
  4 const capabilities = @import("capabilities.zig");
  5 const fd = @import("fd.zig");
  6 const linux = std.os.linux;
  7 const native_os = builtin.os.tag;
  8 const posix = std.posix;
  9 
 10 extern fn posix_openpt(flags: c_int) c_int;
 11 
 12 pub const required_capabilities = switch (native_os) {
 13     .linux => capabilities.noLibc(&.{ .descriptors, .terminal }),
 14     else => capabilities.host(&.{ .descriptors, .terminal }),
 15 };
 16 
 17 pub const Pair = struct {
 18     master: fd.Descriptor,
 19     slave: fd.Descriptor,
 20 
 21     pub fn close(self: Pair) void {
 22         fd.close(self.slave);
 23         fd.close(self.master);
 24     }
 25 };
 26 
 27 pub fn open() !Pair {
 28     return switch (native_os) {
 29         .linux => openLinux(),
 30         .freebsd => openFreeBsd(),
 31         .ios, .macos, .tvos, .visionos, .watchos => openDarwin(),
 32         else => error.UnsupportedPlatform,
 33     };
 34 }
 35 
 36 pub fn isSupported() bool {
 37     return native_os == .linux or native_os == .freebsd or isDarwin();
 38 }
 39 
 40 fn openLinux() !Pair {
 41     if (comptime native_os != .linux) return error.UnsupportedPlatform;
 42 
 43     const master = try openLinuxMaster();
 44     errdefer fd.close(master);
 45 
 46     var unlocked: c_int = 0;
 47     try controlLinux(master, linux.T.IOCSPTLCK, @intFromPtr(&unlocked));
 48 
 49     const flags: u32 = @bitCast(linux.O{
 50         .ACCMODE = .RDWR,
 51         .CLOEXEC = true,
 52         .NOCTTY = true,
 53     });
 54     const slave = while (true) {
 55         const result = linux.ioctl(master, linux.T.IOCGPTPEER, flags);
 56         switch (linux.errno(result)) {
 57             .SUCCESS => break @as(fd.Descriptor, @intCast(result)),
 58             .INTR => continue,
 59             .MFILE => return error.ProcessFdQuotaExceeded,
 60             .NFILE => return error.SystemFdQuotaExceeded,
 61             .NOMEM => return error.SystemResources,
 62             .ACCES, .PERM => return error.AccessDenied,
 63             .INVAL, .IO, .NOTTY => return error.InvalidPseudoTerminal,
 64             else => return error.OpenSlaveFailed,
 65         }
 66     };
 67 
 68     return .{ .master = master, .slave = slave };
 69 }
 70 
 71 fn openLinuxMaster() !fd.Descriptor {
 72     if (comptime native_os != .linux) return error.UnsupportedPlatform;
 73 
 74     while (true) {
 75         const result = linux.open("/dev/ptmx", .{
 76             .ACCMODE = .RDWR,
 77             .CLOEXEC = true,
 78             .NOCTTY = true,
 79         }, 0);
 80         switch (linux.errno(result)) {
 81             .SUCCESS => return @intCast(result),
 82             .INTR => continue,
 83             .ACCES, .PERM => return error.AccessDenied,
 84             .MFILE => return error.ProcessFdQuotaExceeded,
 85             .NFILE => return error.SystemFdQuotaExceeded,
 86             .NOMEM => return error.SystemResources,
 87             .NOENT => return error.FileNotFound,
 88             else => return error.OpenMasterFailed,
 89         }
 90     }
 91 }
 92 
 93 fn controlLinux(descriptor: fd.Descriptor, request: u32, argument: usize) !void {
 94     if (comptime native_os != .linux) return error.UnsupportedPlatform;
 95 
 96     while (true) {
 97         const result = linux.ioctl(descriptor, request, argument);
 98         switch (linux.errno(result)) {
 99             .SUCCESS => return,
100             .INTR => continue,
101             .ACCES, .PERM => return error.AccessDenied,
102             .INVAL, .NOTTY => return error.InvalidPseudoTerminal,
103             else => return error.ControlFailed,
104         }
105     }
106 }
107 
108 fn openDarwin() !Pair {
109     if (comptime !isDarwin()) return error.UnsupportedPlatform;
110 
111     const master = try openPosixMaster();
112     errdefer fd.close(master);
113 
114     try controlPosix(master, @bitCast(@as(u32, 0x20007454)), 0);
115     try controlPosix(master, @bitCast(@as(u32, 0x20007452)), 0);
116 
117     var slave_path: [128]u8 = @splat(0);
118     try controlPosix(master, @bitCast(@as(u32, 0x40807453)), @intFromPtr(&slave_path));
119     const path_len = std.mem.indexOfScalar(u8, &slave_path, 0) orelse return error.InvalidSlaveName;
120     const slave = try openPosixPath(slave_path[0..path_len :0]);
121 
122     return .{ .master = master, .slave = slave };
123 }
124 
125 fn openFreeBsd() !Pair {
126     if (comptime native_os != .freebsd) return error.UnsupportedPlatform;
127 
128     const flags: u32 = @bitCast(posix.O{
129         .ACCMODE = .RDWR,
130         .CLOEXEC = true,
131         .NOCTTY = true,
132     });
133     const master = while (true) {
134         const result = posix_openpt(@bitCast(flags));
135         switch (posix.errno(result)) {
136             .SUCCESS => break @as(fd.Descriptor, @intCast(result)),
137             .INTR => continue,
138             .ACCES, .PERM => return error.AccessDenied,
139             .MFILE => return error.ProcessFdQuotaExceeded,
140             .NFILE => return error.SystemFdQuotaExceeded,
141             .NOMEM => return error.SystemResources,
142             else => return error.OpenMasterFailed,
143         }
144     };
145     errdefer fd.close(master);
146 
147     var number: c_int = undefined;
148     try controlPosix(master, @bitCast(@as(u32, 0x4004740f)), @intFromPtr(&number));
149 
150     var path_buffer: [64]u8 = undefined;
151     const slave_path = std.fmt.bufPrintSentinel(&path_buffer, "/dev/pts/{d}", .{number}, 0) catch return error.InvalidSlaveName;
152     const slave = try openPosixPath(slave_path);
153 
154     return .{ .master = master, .slave = slave };
155 }
156 
157 fn openPosixMaster() !fd.Descriptor {
158     return openPosixPath("/dev/ptmx");
159 }
160 
161 fn openPosixPath(path: [:0]const u8) !fd.Descriptor {
162     return posix.openat(posix.AT.FDCWD, path, .{
163         .ACCMODE = .RDWR,
164         .CLOEXEC = true,
165         .NOCTTY = true,
166     }, 0) catch |err| switch (err) {
167         error.PermissionDenied => error.AccessDenied,
168         else => err,
169     };
170 }
171 
172 fn controlPosix(descriptor: fd.Descriptor, request: c_int, argument: usize) !void {
173     while (true) {
174         const result = posix.system.ioctl(descriptor, request, argument);
175         switch (posix.errno(result)) {
176             .SUCCESS => return,
177             .INTR => continue,
178             .ACCES, .PERM => return error.AccessDenied,
179             .INVAL, .NOTTY => return error.InvalidPseudoTerminal,
180             else => return error.ControlFailed,
181         }
182     }
183 }
184 
185 fn isDarwin() bool {
186     return switch (native_os) {
187         .ios, .macos, .tvos, .visionos, .watchos => true,
188         else => false,
189     };
190 }
191 
192 test "pseudo-terminal pair opens" {
193     if (comptime !isSupported()) return error.SkipZigTest;
194 
195     const pair = open() catch |err| switch (err) {
196         error.FileNotFound, error.AccessDenied => return error.SkipZigTest,
197         else => return err,
198     };
199     defer pair.close();
200 
201     try std.testing.expect(pair.master >= 0);
202     try std.testing.expect(pair.slave >= 0);
203 
204     if (comptime native_os == .linux) {
205         try expectCloseOnExec(pair.master);
206         try expectCloseOnExec(pair.slave);
207     }
208 }
209 
210 test "pseudo-terminal capability follows platform ABI" {
211     const expected: capabilities.CapabilityTier = if (native_os == .linux) .no_libc else .host;
212     try std.testing.expectEqual(expected, required_capabilities.tier);
213 }
214 
215 fn expectCloseOnExec(descriptor: fd.Descriptor) !void {
216     if (comptime native_os != .linux) return error.UnsupportedPlatform;
217 
218     const result = linux.fcntl(descriptor, linux.F.GETFD, 0);
219     switch (linux.errno(result)) {
220         .SUCCESS => try std.testing.expect(result & linux.FD_CLOEXEC != 0),
221         else => return error.FlagQueryFailed,
222     }
223 }