From f26e57fa13a537f46391dd4b74337248ffa4d2e4 Mon Sep 17 00:00:00 2001 From: Trung Date: Mon, 3 Aug 2026 17:53:14 +0700 Subject: [PATCH] Fix Unix98 pty allocation under a sysroot glibc's posix_openpt() opens /dev/ptmx and then confirms devpts is mounted before handing the master back. statfs did not identify /dev/pts, so glibc closed a working master and every glibc pty consumer failed -- terminal emulators, script(1), expect, tmux, screen, sshd, anything using openpty() or forkpty(). Report devpts from statfs and fstatfs, following the per-filesystem pattern already used for proc and tmpfs. That exposed two more links in the same chain. grantpt(3) chmods the slave when it thinks the mode is wrong, and /dev/pts/N is virtualized for open and stat but not for chmod, so the call fell through to the host and failed with ENOENT. glibc then fell back to the pt_chown helper, which no modern distro ships. Intercept chmod and chown on a live slave instead, where the mode and owner come from the pty layer. The synthesized slave stat also reported the host file owner rather than the guest uid, which is why grantpt wanted to chown at all. Report the opening uid and the tty group, matching what devpts gives. tests/test-devpts.c covers the statfs magic, posix_openpt, fstatfs agreement, the grantpt sequence, a master/slave round trip and a negative control that an ordinary path is not devpts. The round trip runs in raw mode under an alarm so a stuck pty fails the test rather than hanging the run. Fix #263 --- src/runtime/procemu.c | 80 ++++++++--- src/runtime/procemu.h | 12 ++ src/syscall/fs-stat.c | 83 +++++++++++ src/syscall/fs.c | 36 +++++ tests/test-devpts.c | 314 ++++++++++++++++++++++++++++++++++++++++++ tests/test-matrix.sh | 2 + 6 files changed, 509 insertions(+), 18 deletions(-) create mode 100644 tests/test-devpts.c diff --git a/src/runtime/procemu.c b/src/runtime/procemu.c index ed83fcc7..f0a5ccc2 100644 --- a/src/runtime/procemu.c +++ b/src/runtime/procemu.c @@ -1589,6 +1589,45 @@ static void proc_task_collect_cb(thread_entry_t *t, void *arg) */ #define PTY_KEEPALIVE_MAX 256 #define PTY_KEEPALIVE_FREE (-1) +/* Group that owns pty slaves. Linux distributions mount devpts with gid=5 + * ("tty") and glibc's grantpt(3) looks that group up before deciding whether + * the slave needs chowning. + */ +#define PTY_SLAVE_TTY_GID 5u + +/* Parse the N out of "/dev/pts/N". Returns false for the directory itself, a + * missing or non-numeric tail, or trailing garbage. + * + * Deliberately stricter than strtoul, which would take leading whitespace, a + * "+" sign and leading zeros. devpts dentries are decimal and canonical, so + * Linux answers ENOENT for "/dev/pts/016" even while slave 16 is open, and + * accepting the alias here would let one live slave answer under many names -- + * for its stat, its statfs identity, and for whether chmod and chown are + * intercepted at all. + */ +static bool pty_slave_num_from_path(const char *path, uint32_t *out) +{ + if (!path || strncmp(path, "/dev/pts/", 9) != 0) + return false; + const char *digits = path + 9; + if (!*digits) + return false; + /* "0" is the only name that may start with a zero. */ + if (digits[0] == '0' && digits[1] != '\0') + return false; + + unsigned long n = 0; + for (const char *d = digits; *d; d++) { + if (*d < '0' || *d > '9') + return false; + if (n > (UINT32_MAX - (unsigned long) (*d - '0')) / 10) + return false; + n = n * 10 + (unsigned long) (*d - '0'); + } + if (out) + *out = (uint32_t) n; + return true; +} /* PTY_SLAVE_PATH_MAX lives in procemu.h so this table and the fork-IPC payload * (proc_pty_ipc_entry_t) cannot drift apart. */ @@ -1954,6 +1993,16 @@ static int pty_lookup_slave_path(uint32_t linux_pts_num, return 0; } +bool proc_pty_slave_stat(const char *path, struct stat *out) +{ + if (!path || strncmp(path, "/dev/pts/", 9) != 0 || !path[9]) + return false; + struct stat st; + if (proc_intercept_stat(path, out ? out : &st) != 0) + return false; + return true; +} + static int pty_open_slave(uint32_t linux_pts_num, int linux_flags) { int oflags = translate_open_flags(linux_flags) & @@ -2816,14 +2865,8 @@ int proc_intercept_open(const guest_t *g, * Linux devpts behavior for an unallocated slave number. */ if (!strncmp(path, "/dev/pts/", 9)) { - const char *digits = path + 9; - if (!*digits) { - errno = ENOENT; - return -1; - } - char *endp; - unsigned long n = strtoul(digits, &endp, 10); - if (endp == digits || *endp != '\0' || n > UINT32_MAX) { + uint32_t n; + if (!pty_slave_num_from_path(path, &n)) { errno = ENOENT; return -1; } @@ -3539,14 +3582,8 @@ int proc_intercept_stat(const char *path, struct stat *st) return 0; } if (!strncmp(path, "/dev/pts/", 9)) { - const char *digits = path + 9; - if (!*digits) { - errno = ENOENT; - return -1; - } - char *endp; - unsigned long n = strtoul(digits, &endp, 10); - if (endp == digits || *endp != '\0' || n > UINT32_MAX) { + uint32_t n; + if (!pty_slave_num_from_path(path, &n)) { errno = ENOENT; return -1; } @@ -3567,8 +3604,15 @@ int proc_intercept_stat(const char *path, struct stat *st) memset(st, 0, sizeof(*st)); st->st_mode = S_IFCHR | 0620; st->st_nlink = 1; - st->st_uid = host_st.st_uid; - st->st_gid = host_st.st_gid; + /* devpts gives the slave to whoever opened the master, group tty -- + * that is what grantpt(3) expects to find. Reporting the host owner + * instead makes glibc see a foreign uid, try to chown the slave, fail, + * and fall back to exec'ing the pt_chown helper, which does not exist + * on a modern distro: grantpt then fails with ENOENT and no pty can be + * allocated even though the master opened fine. + */ + st->st_uid = (uid_t) proc_get_uid(); + st->st_gid = (gid_t) PTY_SLAVE_TTY_GID; /* macOS dev_t = (major << 24) | minor; the fs-stat translation layer * (mac_to_linux_dev) re-encodes that into Linux's split major/minor * layout, so storing 136 in the macOS-major slot makes glibc's diff --git a/src/runtime/procemu.h b/src/runtime/procemu.h index 32d8767a..cd8e75c4 100644 --- a/src/runtime/procemu.h +++ b/src/runtime/procemu.h @@ -11,6 +11,7 @@ #pragma once +#include #include #include #include @@ -45,6 +46,17 @@ int proc_intercept_readlink(const char *path, char *buf, size_t bufsiz); */ int proc_intercept_stat(const char *path, struct stat *mac_st); +/* True when PATH names a live Unix98 pty slave (/dev/pts/N whose master is + * open), filling out with the same synthesized stat proc_intercept_stat would + * return. Such a path has no host backing, so metadata operations on it must be + * answered here rather than passed through to the host filesystem. + * + * Answering and stat'ing together keeps callers that need both from walking the + * locked pty table twice, which would leave a window where the slave goes away + * between the test and the stat. out may be NULL for a pure test. + */ +bool proc_pty_slave_stat(const char *path, struct stat *out); + /* Intercept writes to synthetic proc files that need stateful behavior. * Returns 1 if handled (with *written_out set), 0 if not intercepted, or -1 on * error with errno set. diff --git a/src/syscall/fs-stat.c b/src/syscall/fs-stat.c index 7b14d174..cd737f6b 100644 --- a/src/syscall/fs-stat.c +++ b/src/syscall/fs-stat.c @@ -369,6 +369,67 @@ static bool statfs_path_is_proc(const char *path) return !strncmp(path, "/proc", 5) && (path[5] == '\0' || path[5] == '/'); } +/* /dev/pts itself and the pty slaves under it. /dev/ptmx is the multiplexer + * that hands out those slaves; Linux reports devpts for a master fd too. + */ +/* How statfs should answer for a path under the virtual devpts mount. */ +typedef enum { + DEVPTS_UNRELATED = 0, /* not under /dev/pts; carry on */ + DEVPTS_MOUNT, /* the mount point or a live slave: synthesize */ + DEVPTS_ABSENT, /* under /dev/pts but no such slave: ENOENT */ +} devpts_class_t; + +static devpts_class_t statfs_devpts_class(const char *path) +{ + if (!path || strncmp(path, "/dev/pts", 8) != 0) + return DEVPTS_UNRELATED; + if (path[8] != '\0' && path[8] != '/') + return DEVPTS_UNRELATED; /* "/dev/ptsfoo" is an ordinary name */ + + const char *tail = path + 8; + while (*tail == '/') + tail++; + if (!*tail) + return DEVPTS_MOUNT; /* "/dev/pts", "/dev/pts/", "/dev/pts//" */ + + /* The mount point exists for as long as the pty layer does. A particular + * slave does not: Linux answers ENOENT for an unallocated or malformed + * /dev/pts/N. Report that rather than falling through to the host, so the + * answer cannot depend on whether the sysroot happens to carry a file of + * the same name -- the devpts mount shadows whatever is underneath it, and + * the stat and open intercepts already treat the directory that way. + * + * Only the canonical spelling of a slave resolves, matching those + * intercepts: "/dev/pts/0" is the dentry, "/dev/pts/00" and "/dev/pts/./0" + * are not. Non-canonical spellings land here as ENOENT rather than + * resolving, which diverges from a real kernel for the "." form and is + * consistent across every /dev/pts intercept. + * + * /dev/ptmx is deliberately not claimed. Which filesystem backs it depends + * on whether it resolves to /dev/pts/ptmx or to the devtmpfs node, the same + * ambiguity that keeps sys_fstatfs from answering for a master fd, and + * nothing asks: glibc's getpt statfs's /dev/pts and /dev, never /dev/ptmx. + */ + return proc_pty_slave_stat(path, NULL) ? DEVPTS_MOUNT : DEVPTS_ABSENT; +} + +/* devpts is virtualized rather than host-backed, so answer synthetically. + * Matches what Linux reports for a devpts mount: no blocks, no inodes. + */ +static void fill_devpts_statfs(linux_statfs_t *lin) +{ + memset(lin, 0, sizeof(*lin)); + lin->f_type = 0x1cd1; /* DEVPTS_SUPER_MAGIC */ + lin->f_bsize = 4096; + lin->f_blocks = 0; + lin->f_bfree = 0; + lin->f_bavail = 0; + lin->f_files = 0; + lin->f_ffree = 0; + lin->f_namelen = 255; + lin->f_frsize = 4096; +} + static void fill_proc_statfs(linux_statfs_t *lin) { memset(lin, 0, sizeof(*lin)); @@ -431,6 +492,23 @@ static int64_t sys_statfs_impl(guest_t *g, } } + /* glibc's posix_openpt() opens /dev/ptmx and then confirms devpts is + * mounted before handing the master back (sysdeps/unix/sysv/linux/getpt.c). + * /dev/pts has no host backing here, so a pass-through statfs fails and + * glibc closes a perfectly good master -- breaking Unix98 pty allocation + * for every glibc program. Answer from the virtual filesystem instead. + */ + devpts_class_t devpts = statfs_devpts_class(tx.intercept_path); + if (devpts == DEVPTS_ABSENT) + return -LINUX_ENOENT; + if (devpts == DEVPTS_MOUNT) { + linux_statfs_t lin_st; + fill_devpts_statfs(&lin_st); + if (guest_write_small(g, buf_gva, &lin_st, sizeof(lin_st)) < 0) + return -LINUX_EFAULT; + return 0; + } + /* Report /dev/shm and its leaves as tmpfs, from the backing dir. statfs() * on the leaf would follow a symlink onto the host and leak the host fs * identity, so answer synthetically; lstat is the nofollow existence probe. @@ -480,6 +558,11 @@ int64_t sys_statfs(guest_t *g, uint64_t path_gva, uint64_t buf_gva) int64_t sys_fstatfs(guest_t *g, int fd, uint64_t buf_gva) { + /* Deliberately no devpts case for a pty master fd: Linux answers from + * whatever filesystem provides /dev/ptmx, which is devpts only when it is + * the bind-mounted /dev/pts/ptmx and tmpfs or devtmpfs otherwise. There is + * no single correct value to report, and nothing needs one. + */ fd_entry_t snap; memset(&snap, 0, sizeof(snap)); if (fd_snapshot(fd, &snap) && statfs_path_is_proc(snap.proc_path)) { diff --git a/src/syscall/fs.c b/src/syscall/fs.c index a7ce56af..ab64d5c4 100644 --- a/src/syscall/fs.c +++ b/src/syscall/fs.c @@ -2709,6 +2709,20 @@ int64_t sys_fchmodat(guest_t *g, if (rc != INT64_MIN) return rc; + /* A pty slave has no host file to chmod -- its mode comes from the pty + * layer. Passing this through would hit the host and fail with ENOENT, + * which is what grantpt(3) does when it decides the slave's mode needs + * adjusting: it would then fall back to the pt_chown helper and fail. + * + * The requested mode is accepted but not retained, so a later stat still + * reports the 0620 the pty layer synthesizes. grantpt only ever asks for + * the owner-access bits it is about to hand out, so nothing observes the + * difference; keeping it would need per-slave state that also has to cross + * the fork-IPC boundary. + */ + if (proc_pty_slave_stat(tx.intercept_path, NULL)) + return 0; + host_fd_ref_t dir_ref; if (host_dirfd_ref_open(dirfd, &dir_ref) < 0) return -LINUX_EBADF; @@ -2860,6 +2874,28 @@ int64_t sys_fchownat(guest_t *g, if (rc != INT64_MIN) return rc; + /* A pty slave has no host file to chown -- its owner comes from the pty + * layer. Accept only a request that leaves the reported owner alone; + * anything else would have to be remembered to be observable, so refuse + * rather than report a success the next stat() contradicts. + * + * Not for grantpt(3): the synthesized stat already reports proc_get_uid(), + * so glibc's "chown only when st_uid != getuid()" test is false by + * construction and musl's grantpt is a no-op. This exists so that a request + * naming some other owner is refused instead of silently lost. The known + * divergence is a privileged guest -- login(1) or sshd handing a tty to a + * user -- which Linux would allow and which is refused here, because + * reporting success without retaining the owner would be the worse lie. + */ + struct stat pty_st; + if (proc_pty_slave_stat(tx.intercept_path, &pty_st)) { + bool keeps_owner = + owner == (uint32_t) -1 || owner == (uint32_t) pty_st.st_uid; + bool keeps_group = + group == (uint32_t) -1 || group == (uint32_t) pty_st.st_gid; + return (keeps_owner && keeps_group) ? 0 : -LINUX_EPERM; + } + host_fd_ref_t dir_ref; if (host_dirfd_ref_open(dirfd, &dir_ref) < 0) return -LINUX_EBADF; diff --git a/tests/test-devpts.c b/tests/test-devpts.c new file mode 100644 index 00000000..5694adeb --- /dev/null +++ b/tests/test-devpts.c @@ -0,0 +1,314 @@ +/* devpts identification and Unix98 pty allocation. + * + * glibc's posix_openpt() opens /dev/ptmx and then confirms devpts is mounted + * before handing the master back (sysdeps/unix/sysv/linux/getpt.c). If statfs + * does not identify /dev/pts as devpts, glibc closes the master it just opened + * and every glibc pty consumer fails -- terminal emulators, script(1), expect, + * tmux, screen, sshd, anything using openpty()/forkpty(). + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEVPTS_SUPER_MAGIC 0x1cd1 + +/* Fail loudly instead of hanging if the pty never delivers the payload. */ +static void on_alarm(int sig) +{ + (void) sig; + static const char msg[] = "\ntest-devpts: TIMEOUT waiting on pty read\n"; + ssize_t ignored = write(2, msg, sizeof(msg) - 1); + (void) ignored; + _exit(1); +} + +int main(void) +{ + int failures = 0; + + /* 1. statfs(/dev/pts) must report devpts -- the check glibc performs. */ + printf("test-devpts: 1. statfs(/dev/pts) reports devpts... "); + struct statfs pts_fs; + if (statfs("/dev/pts", &pts_fs) != 0) { + printf("FAIL (statfs: %m)\n"); + failures++; + } else if ((unsigned long) pts_fs.f_type != DEVPTS_SUPER_MAGIC) { + printf("FAIL (f_type=0x%lx, want 0x%x)\n", + (unsigned long) pts_fs.f_type, DEVPTS_SUPER_MAGIC); + failures++; + } else { + printf("PASS (f_type=0x%lx)\n", (unsigned long) pts_fs.f_type); + } + + /* 2. posix_openpt() must hand back a usable master. */ + printf("test-devpts: 2. posix_openpt()... "); + int master = posix_openpt(O_RDWR | O_NOCTTY); + if (master < 0) { + printf("FAIL (%m)\n"); + failures++; + /* Everything below needs the master; report what we have. */ + printf("test-devpts: %s\n", failures ? "FAILED" : "all tests passed"); + return failures != 0; + } + printf("PASS (fd=%d)\n", master); + + /* No assertion on fstatfs(master): Linux answers from whatever filesystem + * provides /dev/ptmx, which is devpts only when it is the bind-mounted + * /dev/pts/ptmx and tmpfs or devtmpfs otherwise. There is no portable + * value to expect. + * + * The slave fd is not ambiguous that way -- on Linux it really does live on + * devpts -- and elfuse still does not answer devpts for it. That is left + * alone knowingly: sys_fstatfs passes the macOS f_type through untranslated + * for every filesystem, so a guest sniffing fs types sees macOS mount-type + * indices whatever it opens, and fixing the slave alone would be one more + * spot patch on the mapping gap that is the other half of the issue this + * change addresses. grantpt(3) is unaffected: the fd it checks is the + * master. + */ + + /* 3. The rest of the Unix98 sequence must work on that master. */ + printf("test-devpts: 3. grantpt/unlockpt/ptsname... "); + char slave_name[256]; + int seq_ok = 1; + if (grantpt(master) != 0) { + printf("FAIL (grantpt: %m)\n"); + seq_ok = 0; + } else if (unlockpt(master) != 0) { + printf("FAIL (unlockpt: %m)\n"); + seq_ok = 0; + } else if (ptsname_r(master, slave_name, sizeof(slave_name)) != 0) { + printf("FAIL (ptsname_r: %m)\n"); + seq_ok = 0; + } else { + printf("PASS (%s)\n", slave_name); + } + if (!seq_ok) + failures++; + + /* 4. Data must survive a round trip through the pair. */ + printf("test-devpts: 4. master/slave round trip... "); + if (!seq_ok) { + printf("SKIP (no slave name)\n"); + } else { + int slave = open(slave_name, O_RDWR | O_NOCTTY); + if (slave < 0) { + printf("FAIL (open slave: %m)\n"); + failures++; + } else { + /* Raw mode: in canonical mode the slave read would block until a + * newline arrives, and ECHO would race the payload back at the + * master. Both would hang the run rather than fail it, so a + * termios failure has to stop here -- carrying on would spend the + * whole alarm window below only to report a round-trip failure + * that is really a setup failure. + */ + struct termios tio; + if (tcgetattr(slave, &tio) != 0) { + printf("FAIL (tcgetattr on slave: %m)\n"); + failures++; + close(slave); + goto round_trip_done; + } + cfmakeraw(&tio); + if (tcsetattr(slave, TCSANOW, &tio) != 0) { + printf("FAIL (tcsetattr raw mode: %m)\n"); + failures++; + close(slave); + goto round_trip_done; + } + /* Belt and braces: a stuck read must fail the test, not wedge CI. + */ + signal(SIGALRM, on_alarm); + alarm(10); + + static const char msg[] = "devpts"; + const size_t want = sizeof(msg) - 1; + char buf[sizeof(msg)]; + memset(buf, 0, sizeof(buf)); + + /* A pty may transfer fewer bytes per call than asked for, in + * either direction, so drive both to completion instead of + * assuming one call moves the whole payload -- a legal short read + * would otherwise fail a working pair. The alarm above bounds the + * exchange, so a stall still fails rather than looping forever. + */ + size_t sent = 0; + while (sent < want) { + ssize_t n = write(master, msg + sent, want - sent); + if (n <= 0) + break; + sent += (size_t) n; + } + size_t got = 0; + while (sent == want && got < want) { + ssize_t n = read(slave, buf + got, want - got); + if (n <= 0) + break; + got += (size_t) n; + } + alarm(0); + if (sent != want) { + printf("FAIL (wrote %zu of %zu: %m)\n", sent, want); + failures++; + } else if (got != want || memcmp(buf, msg, want) != 0) { + printf("FAIL (read %zu of %zu, buf='%s')\n", got, want, buf); + failures++; + } else { + printf("PASS\n"); + } + close(slave); + } + round_trip_done:; + } + + /* 5. An unallocated or malformed slave must not claim devpts: a prefix + * match on /dev/pts would make these succeed, where Linux reports ENOENT. + */ + printf("test-devpts: 5. bogus slave paths are ENOENT... "); + { + /* The last four are aliases of a live slave that strtoul would take: + * leading zeros, a sign, leading whitespace. Linux answers ENOENT for + * every one of them, and accepting any would let one slave answer + * under several names -- for stat, for devpts identity, and for + * whether chmod and chown are intercepted at all. + */ + char alias_zero[64], alias_plus[64], alias_space[64]; + snprintf(alias_zero, sizeof(alias_zero), "/dev/pts/0%s", + seq_ok ? slave_name + 9 : "0"); + snprintf(alias_plus, sizeof(alias_plus), "/dev/pts/+%s", + seq_ok ? slave_name + 9 : "0"); + snprintf(alias_space, sizeof(alias_space), "/dev/pts/ %s", + seq_ok ? slave_name + 9 : "0"); + const char *const bogus[] = {"/dev/pts/99999", "/dev/pts/bogus", + "/dev/pts/1x", alias_zero, + alias_plus, alias_space}; + int bad = 0; + for (size_t i = 0; i < sizeof(bogus) / sizeof(bogus[0]); i++) { + struct statfs b; + errno = 0; + if (statfs(bogus[i], &b) == 0) { + printf("FAIL (%s resolved, f_type=0x%lx) ", bogus[i], + (unsigned long) b.f_type); + bad++; + } else if (errno != ENOENT) { + /* Failing is not enough: EACCES or ENOTDIR would mean the + * lookup went wrong somewhere else rather than the slave + * simply not existing. + */ + printf("FAIL (%s errno=%d (%s), want ENOENT) ", bogus[i], errno, + strerror(errno)); + bad++; + } + } + if (bad) + failures++; + else + printf("PASS\n"); + } + + /* 6. chown must not claim to have given the slave away. Keeping the + * reported owner is what grantpt(3) asks for and must succeed; handing it + * to another uid is refused rather than silently discarded. + */ + printf("test-devpts: 6. chown semantics on the slave... "); + if (!seq_ok) { + printf("SKIP (no slave name)\n"); + } else { + struct stat st; + if (stat(slave_name, &st) != 0) { + printf("FAIL (stat slave: %m)\n"); + failures++; + } else if (chown(slave_name, st.st_uid, st.st_gid) != 0) { + printf("FAIL (no-op chown rejected: %m)\n"); + failures++; + } else if (chown(slave_name, (uid_t) -1, (gid_t) -1) != 0) { + printf("FAIL (unchanged chown rejected: %m)\n"); + failures++; + } else { + /* Giving the slave to another uid must not silently "succeed". */ + uid_t other = st.st_uid == 0 ? 12345 : 0; + int rc = chown(slave_name, other, (gid_t) -1); + struct stat after; + if (rc == 0 && stat(slave_name, &after) == 0 && + after.st_uid != other) { + printf("FAIL (chown reported success but owner unchanged)\n"); + failures++; + } else { + printf("PASS\n"); + } + } + } + + /* 7. chmod on the slave must succeed. This is the branch grantpt(3) needs + * on glibc: it derives the mode it wants from getgrnam("tty"), and on a + * macOS host that group is gid 4 while the synthesized slave reports gid 5 + * (PTY_SLAVE_TTY_GID, what Linux devpts uses), so the group never matches, + * the wanted mode comes out 0600 against the synthesized 0620, and grantpt + * chmods every time rather than accepting the mode as-is. Passing that + * through to the host would fail ENOENT and send grantpt to pt_chown. + * + * Succeeding is the whole portable contract, so the follow-up stat accepts + * two answers. A real kernel keeps the new mode and reports 0600. elfuse + * accepts the request without retaining it -- keeping it would need + * per-slave state that also has to cross the fork-IPC boundary -- and goes + * on reporting the synthesized 0620. Anything else means the chmod landed + * somewhere it should not have. + */ + printf("test-devpts: 7. chmod on the slave is accepted... "); + if (!seq_ok) { + printf("SKIP (no slave name)\n"); + } else if (chmod(slave_name, 0600) != 0) { + printf("FAIL (chmod rejected: %m)\n"); + failures++; + } else { + struct stat after; + unsigned mode = 0; + if (stat(slave_name, &after) != 0) { + printf("FAIL (stat after chmod: %m)\n"); + failures++; + } else if ((mode = after.st_mode & 07777) == 0600) { + printf("PASS (mode retained 0600)\n"); + } else if (mode == 0620) { + printf("PASS (accepted, stat still reports 0620)\n"); + } else { + printf("FAIL (mode=%04o, want 0600 or the synthesized 0620)\n", + mode); + failures++; + } + } + + /* 8. Negative control: an ordinary directory must not claim devpts, so a + * blanket f_type would not pass this file. + */ + printf("test-devpts: 8. ordinary path is not devpts... "); + struct statfs root_fs; + if (statfs("/", &root_fs) != 0) { + printf("FAIL (statfs /: %m)\n"); + failures++; + } else if ((unsigned long) root_fs.f_type == DEVPTS_SUPER_MAGIC) { + printf("FAIL (/ reports devpts)\n"); + failures++; + } else { + printf("PASS (f_type=0x%lx)\n", (unsigned long) root_fs.f_type); + } + + close(master); + + if (failures == 0) + printf("test-devpts: all tests passed -- PASS\n"); + else + printf("test-devpts: %d failed\n", failures); + return failures != 0; +} diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 8565766f..529158f5 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -629,6 +629,8 @@ run_unit_tests() printf "\nSyscall coverage\n" test_check "$runner" "test-file-ops" "0 failed" "$bindir/test-file-ops" + test_check "$runner" "test-devpts" "all tests passed -- PASS" \ + "$bindir/test-devpts" test_check "$runner" "test-sysinfo" "0 failed" "$bindir/test-sysinfo" test_check "$runner" "test-io-opt" "0 failed" "$bindir/test-io-opt" test_check "$runner" "test-poll" "0 failed" "$bindir/test-poll"