diff --git a/misc-utils/lsfd-cdev.c b/misc-utils/lsfd-cdev.c index 70751bab970..63f95ed516b 100644 --- a/misc-utils/lsfd-cdev.c +++ b/misc-utils/lsfd-cdev.c @@ -22,6 +22,7 @@ #include "lsfd.h" static struct list_head miscdevs; +static struct list_head ttydrvs; struct miscdev { struct list_head miscdevs; @@ -29,6 +30,15 @@ struct miscdev { char *name; }; +struct ttydrv { + struct list_head ttydrvs; + unsigned long major; + unsigned long minor_start, minor_end; + char *name; + unsigned int is_ptmx: 1; + unsigned int is_pts: 1; +}; + struct cdev { struct file file; const char *devdrv; @@ -38,7 +48,7 @@ struct cdev { struct cdev_ops { const struct cdev_ops *parent; - bool (*probe)(const struct cdev *); + bool (*probe)(struct cdev *); char * (*get_name)(struct cdev *); bool (*fill_column)(struct proc *, struct cdev *, @@ -48,7 +58,9 @@ struct cdev_ops { char **); void (*init)(const struct cdev *); void (*free)(const struct cdev *); + void (*attach_xinfo)(struct cdev *); int (*handle_fdinfo)(struct cdev *, const char *, const char *); + const struct ipc_class * (*get_ipc_class)(struct cdev *); }; static bool cdev_fill_column(struct proc *proc __attribute__((__unused__)), @@ -139,22 +151,108 @@ static void read_misc(struct list_head *miscdevs_list, FILE *misc_fp) } } +#define TTY_DRIVERS_LINE_LEN0 1023 +#define TTY_DRIVERS_LINE_LEN (TTY_DRIVERS_LINE_LEN0 + 1) +static struct ttydrv *new_ttydrv(unsigned int major, + unsigned int minor_start, unsigned int minor_end, + const char *name) +{ + struct ttydrv *ttydrv = xmalloc(sizeof(*ttydrv)); + + INIT_LIST_HEAD(&ttydrv->ttydrvs); + + ttydrv->major = major; + ttydrv->minor_start = minor_start; + ttydrv->minor_end = minor_end; + ttydrv->name = xstrdup(name); + ttydrv->is_ptmx = 0; + if (strcmp(name, "ptmx") == 0) + ttydrv->is_ptmx = 1; + ttydrv->is_pts = 0; + if (strcmp(name, "pts") == 0) + ttydrv->is_pts = 1; + + return ttydrv; +} + +static void free_ttydrv(struct ttydrv *ttydrv) +{ + free(ttydrv->name); + free(ttydrv); +} + +static bool is_pty(const struct ttydrv *ttydrv) +{ + return ttydrv->is_ptmx || ttydrv->is_pts; +} + +static struct ttydrv *read_ttydrv(const char *line) +{ + const char *p; + char name[TTY_DRIVERS_LINE_LEN]; + unsigned long major; + unsigned long minor_range[2]; + + p = strchr(line, ' '); + if (p == NULL) + return NULL; + + p = strstr(p, "/dev/"); + if (p == NULL) + return NULL; + p += (sizeof("/dev/") - 1); /* Ignore the last null byte. */ + + if (sscanf(p, "%" stringify_value(TTY_DRIVERS_LINE_LEN0) "[^ ]", name) != 1) + return NULL; + + p += strlen (name); + if (sscanf(p, " %lu %lu-%lu ", &major, + minor_range, minor_range + 1) != 3) { + if (sscanf(p, " %lu %lu ", &major, minor_range) == 2) + minor_range[1] = minor_range[0]; + else + return NULL; + } + + return new_ttydrv(major, minor_range[0], minor_range[1], name); +} + +static void read_tty_drivers(struct list_head *ttydrvs_list, FILE *ttydrvs_fp) +{ + char line[TTY_DRIVERS_LINE_LEN]; + + while (fgets(line, sizeof(line), ttydrvs_fp)) { + struct ttydrv *ttydrv = read_ttydrv(line); + if (ttydrv) + list_add_tail(&ttydrv->ttydrvs, ttydrvs_list); + } +} + static void cdev_class_initialize(void) { FILE *misc_fp; + FILE *ttydrvs_fp; INIT_LIST_HEAD(&miscdevs); + INIT_LIST_HEAD(&ttydrvs); misc_fp = fopen("/proc/misc", "r"); if (misc_fp) { read_misc(&miscdevs, misc_fp); fclose(misc_fp); } + + ttydrvs_fp = fopen("/proc/tty/drivers", "r"); + if (ttydrvs_fp) { + read_tty_drivers(&ttydrvs, ttydrvs_fp); + fclose(ttydrvs_fp); + } } static void cdev_class_finalize(void) { list_free(&miscdevs, struct miscdev, miscdevs, free_miscdev); + list_free(&ttydrvs, struct ttydrv, ttydrvs, free_ttydrv); } const char *get_miscdev(unsigned long minor) @@ -168,10 +266,27 @@ const char *get_miscdev(unsigned long minor) return NULL; } +static const struct ttydrv *get_ttydrv(unsigned long major, + unsigned long minor) +{ + struct list_head *c; + + list_for_each(c, &ttydrvs) { + struct ttydrv *ttydrv = list_entry(c, struct ttydrv, ttydrvs); + if (ttydrv->major == major + && ttydrv->minor_start <= minor + && minor <= ttydrv->minor_end) + return ttydrv; + } + + return NULL; +} + + /* * generic (fallback implementation) */ -static bool cdev_generic_probe(const struct cdev *cdev __attribute__((__unused__))) { +static bool cdev_generic_probe(struct cdev *cdev __attribute__((__unused__))) { return true; } @@ -210,7 +325,7 @@ static struct cdev_ops cdev_generic_ops = { /* * misc device driver */ -static bool cdev_misc_probe(const struct cdev *cdev) { +static bool cdev_misc_probe(struct cdev *cdev) { return cdev->devdrv && strcmp(cdev->devdrv, "misc") == 0; } @@ -254,7 +369,7 @@ static struct cdev_ops cdev_misc_ops = { /* * tun devcie driver */ -static bool cdev_tun_probe(const struct cdev *cdev) +static bool cdev_tun_probe(struct cdev *cdev) { const char *miscdev; @@ -325,13 +440,223 @@ static struct cdev_ops cdev_tun_ops = { .handle_fdinfo = cdev_tun_handle_fdinfo, }; +/* + * tty devices + */ +struct ttydata { + struct cdev *cdev; + const struct ttydrv *drv; +#define NO_TTY_INDEX -1 + int tty_index; /* used only in ptmx devices */ + struct ipc_endpoint endpoint; +}; + +static bool cdev_tty_probe(struct cdev *cdev) { + const struct ttydrv *ttydrv = get_ttydrv(major(cdev->file.stat.st_rdev), + minor(cdev->file.stat.st_rdev)); + struct ttydata *data; + + if (!ttydrv) + return false; + + data = xmalloc(sizeof(struct ttydata)); + data->cdev = cdev; + data->drv = ttydrv; + data->tty_index = NO_TTY_INDEX; + cdev->cdev_data = data; + + return true; +} + +static void cdev_tty_free(const struct cdev *cdev) +{ + if (cdev->cdev_data) + free(cdev->cdev_data); +} + +static char * cdev_tty_get_name(struct cdev *cdev) +{ + struct ttydata *data = cdev->cdev_data; + char *str = NULL; + + if (!data->drv->is_ptmx) + return NULL; + + if (data->tty_index == NO_TTY_INDEX) + str = xstrdup("tty-index="); + else + xasprintf(&str, "tty-index=%d", data->tty_index); + return str; +} + +static inline char *cdev_tty_xstrendpoint(struct file *file) +{ + char *str = NULL; + xasprintf(&str, "%d,%s,%d%c%c", + file->proc->pid, file->proc->command, file->association, + (file->mode & S_IRUSR)? 'r': '-', + (file->mode & S_IWUSR)? 'w': '-'); + return str; +} + +static bool cdev_tty_fill_column(struct proc *proc __attribute__((__unused__)), + struct cdev *cdev, + struct libscols_line *ln __attribute__((__unused__)), + int column_id, + size_t column_index __attribute__((__unused__)), + char **str) +{ + struct file *file = &cdev->file; + struct ttydata *data = cdev->cdev_data; + + switch(column_id) { + case COL_SOURCE: + if (data->drv->minor_start == data->drv->minor_end) + *str = xstrdup(data->drv->name); + else + xasprintf(str, "%s:%u", data->drv->name, + minor(file->stat.st_rdev)); + return true; + case COL_PTMX_TTY_INDEX: + if (data->drv->is_ptmx) { + xasprintf(str, "%d", data->tty_index); + return true; + } + return false; + case COL_ENDPOINTS: + if (is_pty(data->drv)) { + struct ttydata *this = data; + struct list_head *e; + foreach_endpoint(e, data->endpoint) { + char *estr; + struct ttydata *other = list_entry(e, struct ttydata, endpoint.endpoints); + if (this == other) + continue; + + if ((this->drv->is_ptmx && !other->drv->is_pts) + || (this->drv->is_pts && !other->drv->is_ptmx)) + continue; + + if (*str) + xstrputc(str, '\n'); + estr = cdev_tty_xstrendpoint(&other->cdev->file); + xstrappend(str, estr); + free(estr); + } + if (*str) + return true; + } + return false; + default: + return false; + } +} + +static int cdev_tty_handle_fdinfo(struct cdev *cdev, const char *key, const char *val) +{ + struct ttydata *data = cdev->cdev_data; + + if (!data->drv->is_ptmx) + return 0; + + if (strcmp(key, "tty-index") == 0) { + errno = 0; + data->tty_index = (int)strtol(val, NULL, 10); + if (errno) { + data->tty_index = NO_TTY_INDEX; + return 0; + } + return 1; + } + + return 0; +} + +struct cdev_pty_ipc { + struct ipc ipc; + int tty_index; +}; + +static unsigned int cdev_pty_get_hash(struct file *file) +{ + struct cdev *cdev = (struct cdev *)file; + struct ttydata *data = cdev->cdev_data; + + return data->drv->is_ptmx? + (unsigned int)data->tty_index: + (unsigned int)minor(file->stat.st_rdev); +} + +static bool cdev_pty_is_suitable_ipc(struct ipc *ipc, struct file *file) +{ + struct cdev *cdev = (struct cdev *)file; + struct ttydata *data = cdev->cdev_data; + struct cdev_pty_ipc *cdev_pty_ipc = (struct cdev_pty_ipc *)ipc; + + return (data->drv->is_ptmx)? + cdev_pty_ipc->tty_index == (int)data->tty_index: + cdev_pty_ipc->tty_index == (int)minor(file->stat.st_rdev); +} + +static const struct ipc_class *cdev_tty_get_ipc_class(struct cdev *cdev) +{ + static const struct ipc_class cdev_pty_ipc_class = { + .size = sizeof(struct cdev_pty_ipc), + .get_hash = cdev_pty_get_hash, + .is_suitable_ipc = cdev_pty_is_suitable_ipc, + }; + + struct ttydata *data = cdev->cdev_data; + + if (is_pty(data->drv)) + return &cdev_pty_ipc_class; + + return NULL; +} + +static void cdev_tty_attach_xinfo(struct cdev *cdev) +{ + struct ttydata *data = cdev->cdev_data; + struct ipc *ipc; + unsigned int hash; + + + if (! is_pty(data->drv)) + return; + + init_endpoint(&data->endpoint); + ipc = get_ipc(&cdev->file); + if (ipc) + goto link; + + ipc = new_ipc(cdev_tty_get_ipc_class(cdev)); + hash = cdev_pty_get_hash(&cdev->file); + ((struct cdev_pty_ipc *)ipc)->tty_index = (int)hash; + + add_ipc(ipc, hash); + link: + add_endpoint(&data->endpoint, ipc); +} + +static struct cdev_ops cdev_tty_ops = { + .parent = &cdev_generic_ops, + .probe = cdev_tty_probe, + .free = cdev_tty_free, + .get_name = cdev_tty_get_name, + .fill_column = cdev_tty_fill_column, + .attach_xinfo = cdev_tty_attach_xinfo, + .handle_fdinfo = cdev_tty_handle_fdinfo, + .get_ipc_class = cdev_tty_get_ipc_class, +}; + static const struct cdev_ops *cdev_ops[] = { &cdev_tun_ops, &cdev_misc_ops, + &cdev_tty_ops, &cdev_generic_ops /* This must be at the end. */ }; -static const struct cdev_ops *cdev_probe(const struct cdev *cdev) +static const struct cdev_ops *cdev_probe(struct cdev *cdev) { const struct cdev_ops *r = NULL; @@ -366,6 +691,14 @@ static void free_cdev_content(struct file *file) cdev->cdev_ops->free(cdev); } +static void cdev_attach_xinfo(struct file *file) +{ + struct cdev *cdev = (struct cdev *)file; + + if (cdev->cdev_ops->attach_xinfo) + cdev->cdev_ops->attach_xinfo(cdev); +} + static int cdev_handle_fdinfo(struct file *file, const char *key, const char *value) { struct cdev *cdev = (struct cdev *)file; @@ -375,6 +708,15 @@ static int cdev_handle_fdinfo(struct file *file, const char *key, const char *va return 0; /* Should be handled in parents */ } +static const struct ipc_class *cdev_get_ipc_class(struct file *file) +{ + struct cdev *cdev = (struct cdev *)file; + + if (cdev->cdev_ops->get_ipc_class) + return cdev->cdev_ops->get_ipc_class(cdev); + return NULL; +} + const struct file_class cdev_class = { .super = &file_class, .size = sizeof(struct cdev), @@ -383,5 +725,7 @@ const struct file_class cdev_class = { .fill_column = cdev_fill_column, .initialize_content = init_cdev_content, .free_content = free_cdev_content, + .attach_xinfo = cdev_attach_xinfo, .handle_fdinfo = cdev_handle_fdinfo, + .get_ipc_class = cdev_get_ipc_class, }; diff --git a/misc-utils/lsfd.1.adoc b/misc-utils/lsfd.1.adoc index 1f7e889d46c..aef76ec5e3f 100644 --- a/misc-utils/lsfd.1.adoc +++ b/misc-utils/lsfd.1.adoc @@ -180,6 +180,7 @@ with the fd: FIFO type::: mqueue type::: +ptmx and pts sources::: _PID_,_COMMAND_,_ASSOC_[-r][-w] + The last characters ([-r][-w]) represents the read and/or @@ -304,6 +305,12 @@ state=_SOCK.STATE_[ id=_PING.ID_][ laddr=_INET.LADDR_ [ raddr=_INET.RADDR_]] PINGv6::: state=_SOCK.STATE_[ id=_PING.ID_][ laddr=_INET6.LADDR_ [ raddr=_INET6.RADDR_]] + +ptmx::: +tty-index=_PTMX.TTY-INDEX_ ++ +*lsfd* extracts _PTMX.TTY-INDEX_ from +``/proc/``_pid_``/fdinfo/``_fd_. ++ RAW::: state=_SOCK.STATE_[ protocol=_RAW.PROTOCOL_ [ laddr=_INET.LADDR_ [ raddr=_INET.RADDR_]]] + @@ -484,6 +491,9 @@ Interval. TIMERFD.REMAINING <``number``>:: Remaining time. +PTMX.TTY-INDEX <``number``>:: +TTY index of the counterpart. + TUN.IFACE <``string``>:: Network intrface behind the tun device. diff --git a/misc-utils/lsfd.c b/misc-utils/lsfd.c index 862a0d4e0b2..76e0e66e179 100644 --- a/misc-utils/lsfd.c +++ b/misc-utils/lsfd.c @@ -275,6 +275,9 @@ static const struct colinfo infos[] = { [COL_POS] = { "POS", 5, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("file position") }, + [COL_PTMX_TTY_INDEX] = { "PTMX.TTY-INDEX", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("tty index of the counterpart") }, [COL_RAW_PROTOCOL] = { "RAW.PROTOCOL", 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("protocol number of the raw socket") }, @@ -2323,9 +2326,9 @@ int main(int argc, char *argv[]) /* cleanup */ delete(&ctl.procs, &ctl); - finalize_ipc_table(); finalize_devdrvs(); finalize_classes(); + finalize_ipc_table(); finalize_nodevs(); return 0; diff --git a/misc-utils/lsfd.h b/misc-utils/lsfd.h index c9c975392fc..a819e65096c 100644 --- a/misc-utils/lsfd.h +++ b/misc-utils/lsfd.h @@ -92,6 +92,7 @@ enum { COL_PIDFD_PID, COL_PING_ID, COL_POS, + COL_PTMX_TTY_INDEX, COL_RAW_PROTOCOL, COL_RDEV, COL_SIGNALFD_MASK, diff --git a/tests/expected/lsfd/mkfds-pty b/tests/expected/lsfd/mkfds-pty new file mode 100644 index 00000000000..ac38fc974b7 --- /dev/null +++ b/tests/expected/lsfd/mkfds-pty @@ -0,0 +1,10 @@ +5 rw- CHR ptmx +ASSOC,MODE,TYPE,SOURCE: 0 +NAME: 0 +ENDPOINTS: 0 +PTMX.TTY-INDEX: 0 +6 r-- CHR +ASSOC,MODE,TYPE: 0 +SOURCE: 0 +NAME: 0 +ENDPOINTS: 0 diff --git a/tests/helpers/test_mkfds.c b/tests/helpers/test_mkfds.c index 8a1388adff4..cd5379bc2b7 100644 --- a/tests/helpers/test_mkfds.c +++ b/tests/helpers/test_mkfds.c @@ -3020,6 +3020,99 @@ static void *make_bpf_map(const struct factory *factory, struct fdesc fdescs[], return NULL; } +static void *make_pty(const struct factory *factory _U_, struct fdesc fdescs[], + int argc _U_, char ** argv _U_) +{ + int index, *indexp; + char *pts; + int pts_fd; + int ptmx_fd = posix_openpt(O_RDWR); + if (ptmx_fd < 0) + err(EXIT_FAILURE, "failed in opening /dev/ptmx"); + + if (unlockpt(ptmx_fd) < 0) { + int e = errno; + close(ptmx_fd); + errno = e; + err(EXIT_FAILURE, "failed in unlockpt()"); + } + + if (ioctl(ptmx_fd, TIOCGPTN, &index) < 0) { + int e = errno; + close(ptmx_fd); + errno = e; + err(EXIT_FAILURE, "failed in ioctl(TIOCGPTN)"); + } + + pts = ptsname(ptmx_fd); + if (pts == NULL) { + int e = errno; + close(ptmx_fd); + errno = e; + err(EXIT_FAILURE, "failed in ptsname()"); + } + + if (ptmx_fd != fdescs[0].fd) { + if (dup2(ptmx_fd, fdescs[0].fd) < 0) { + int e = errno; + close(ptmx_fd); + errno = e; + err(EXIT_FAILURE, "failed to dup %d -> %d", ptmx_fd, fdescs[0].fd); + } + close(ptmx_fd); + ptmx_fd = fdescs[0].fd; + } + + pts_fd = open(pts, O_RDONLY); + if (pts_fd < 0) { + int e = errno; + close(ptmx_fd); + errno = e; + err(EXIT_FAILURE, "failed in opening %s", pts); + } + + if (pts_fd != fdescs[1].fd) { + if (dup2(pts_fd, fdescs[1].fd) < 0) { + int e = errno; + close(pts_fd); + close(ptmx_fd); + errno = e; + err(EXIT_FAILURE, "failed to dup %d -> %d", pts_fd, fdescs[1].fd); + } + close(pts_fd); + pts_fd = fdescs[1].fd; + } + + fdescs[0] = (struct fdesc){ + .fd = fdescs[0].fd, + .close = close_fdesc, + .data = NULL + }; + fdescs[1] = (struct fdesc){ + .fd = fdescs[1].fd, + .close = close_fdesc, + .data = NULL + }; + + indexp = xmalloc(sizeof(index)); + *indexp = index; + return indexp; +} + +static void report_pty(const struct factory *factory _U_, + int nth, void *data, FILE *fp) +{ + if (nth == 0) { + int *index = data; + fprintf(fp, "%d", *index); + } +} + +static void free_pty(const struct factory * factory _U_, void *data) +{ + free(data); +} + #define PARAM_END { .name = NULL, } static const struct factory factories[] = { { @@ -3817,7 +3910,20 @@ static const struct factory factories[] = { PARAM_END } }, - + { + .name = "pty", + .desc = "make a pair of ptmx and pts", + .priv = false, + .N = 2, + .EX_N = 0, + .EX_R = 1, + .make = make_pty, + .report = report_pty, + .free = free_pty, + .params = (struct parameter []) { + PARAM_END + } + }, }; static int count_parameters(const struct factory *factory) diff --git a/tests/ts/lsfd/mkfds-pty b/tests/ts/lsfd/mkfds-pty new file mode 100755 index 00000000000..cc781ebe1e7 --- /dev/null +++ b/tests/ts/lsfd/mkfds-pty @@ -0,0 +1,73 @@ +#!/bin/bash +# +# Copyright (C) 2023 Masatake YAMATO +# +# This file is part of util-linux. +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +TS_TOPDIR="${0%/*}/../.." +TS_DESC="ptmx and associated pts" + +. "$TS_TOPDIR"/functions.sh +ts_init "$*" + +ts_check_test_command "$TS_CMD_LSFD" +ts_check_test_command "$TS_HELPER_MKFDS" + +ts_cd "$TS_OUTDIR" + +PID= +FD0=5 +FD1=6 +INDEX= + +col_test() +{ + local col=$1 + local expected=$2 + local output + + output=$(${TS_CMD_LSFD} -p "${PID}" -n --raw -o "$col" -Q "${EXPR}") + echo "$col": $? + if [[ "$output" != "$expected" ]]; then + echo "expected ${col} -Q ${EXPR}: ${expected}" + echo "output ${col} -Q ${EXPR}: ${output}" + fi +} + +{ + coproc MKFDS { "$TS_HELPER_MKFDS" pty $FD0 $FD1 ; } + if read -u ${MKFDS[0]} PID INDEX; then + EXPR='(FD == '$FD0')' + columns=ASSOC,MODE,TYPE,SOURCE + ${TS_CMD_LSFD} -p "${PID}" -n -r -o "$columns" -Q "${EXPR}" + echo "$columns": $? + + col_test NAME "tty-index=${INDEX}" + col_test ENDPOINTS "${PID},test_mkfds,${FD1}r-" + col_test PTMX.TTY-INDEX "$INDEX" + + EXPR='(FD == '$FD1')' + columns=ASSOC,MODE,TYPE + ${TS_CMD_LSFD} -p "${PID}" -n -r -o "$columns" -Q "${EXPR}" + echo "$columns": $? + + col_test SOURCE "pts:${INDEX}" + col_test NAME "/dev/pts/${INDEX}" + col_test ENDPOINTS "${PID},test_mkfds,${FD0}rw" + + echo DONE >&"${MKFDS[1]}" + fi + wait ${MKFDS_PID} +} > $TS_OUTPUT 2>&1 + +ts_finalize