diff options
author | Karel Zak <kzak@redhat.com> | 2024-04-02 12:58:02 +0200 |
---|---|---|
committer | Karel Zak <kzak@redhat.com> | 2024-04-02 12:58:02 +0200 |
commit | c984aa7426a64ed2dbad789ff568e600db40aaa3 (patch) | |
tree | 0aec5efafc82886154921d6754ee4447f419d1e3 | |
parent | 3400ca5a9f3174308fea9c106ee3678bd58deabd (diff) | |
parent | 0b517b611e42c813f4450442e0421a3d2bd021f6 (diff) | |
download | util-linux-c984aa7426a64ed2dbad789ff568e600db40aaa3.tar.gz |
Merge branch 'lsns--Q' of https://github.com/masatake/util-linux
* 'lsns--Q' of https://github.com/masatake/util-linux:
tests: (lsns) add a case testing -Q, --filter option
tests: (test_mkfds::userns) add a new factory
tests: (test_mkfds::multiplexing) fix the factory description
lsns: add -H, --list-columns option
lsns: implement -Q, --filter option
lsfd: (man) fix the decoration of an optional parameter
lsns: add a missing '=' character in the help message
lsns: (man) make the namespace parameter optional
lsblk: (refactor) refer to a parameter instead of a file static var
-rw-r--r-- | misc-utils/lsblk.c | 2 | ||||
-rw-r--r-- | misc-utils/lsfd.1.adoc | 2 | ||||
-rw-r--r-- | sys-utils/lsns.8.adoc | 18 | ||||
-rw-r--r-- | sys-utils/lsns.c | 274 | ||||
-rw-r--r-- | tests/expected/lsns/filter | 2 | ||||
-rw-r--r-- | tests/helpers/test_mkfds.c | 106 | ||||
-rwxr-xr-x | tests/ts/lsns/filter | 65 |
7 files changed, 397 insertions, 72 deletions
diff --git a/misc-utils/lsblk.c b/misc-utils/lsblk.c index 30bd2edfdf..fbc0356451 100644 --- a/misc-utils/lsblk.c +++ b/misc-utils/lsblk.c @@ -2264,7 +2264,7 @@ static void init_scols_filter(struct libscols_table *tb, struct libscols_filter } if (!col) { add_column(id); - col = scols_table_new_column(lsblk->table, ci->name, + col = scols_table_new_column(tb, ci->name, ci->whint, SCOLS_FL_HIDDEN); if (!col) err(EXIT_FAILURE,_("failed to allocate output column")); diff --git a/misc-utils/lsfd.1.adoc b/misc-utils/lsfd.1.adoc index f7d398da06..05cc9e9aee 100644 --- a/misc-utils/lsfd.1.adoc +++ b/misc-utils/lsfd.1.adoc @@ -90,7 +90,7 @@ counters by specifying this option multiple times. + See also *COUNTER EXAMPLES*. -*--summary*[=_when_]:: +*--summary*[**=**__when__]:: This option controls summary lines output. The optional argument _when_ can be *only*, *append* or *never*. If the _when_ argument is omitted, it defaults to *only*. diff --git a/sys-utils/lsns.8.adoc b/sys-utils/lsns.8.adoc index ea6c87e9d1..ec79cf0ed3 100644 --- a/sys-utils/lsns.8.adoc +++ b/sys-utils/lsns.8.adoc @@ -17,7 +17,7 @@ lsns - list namespaces == SYNOPSIS -*lsns* [options] _namespace_ +*lsns* [options] [_namespace_] == DESCRIPTION @@ -31,6 +31,9 @@ Note that *lsns* reads information directly from the _/proc_ filesystem and for == OPTIONS +*-H*, *--list-columns*:: +List the available columns, use with *--json* or *--raw* to get output in machine-readable format. + *-J*, *--json*:: Use JSON output format. @@ -55,6 +58,16 @@ Display only the namespaces without processes (aka persistent namespaces), creat *-p*, *--task* _PID_:: Display only the namespaces held by the process with this _PID_. +*-Q*, *--filter* _expr_:: +Print only the namespaces that meet the conditions specified by the expr. ++ +This feature is EXPERIMENTAL. See also *scols-filter*(5). For example +exclude root as username, but print every namespaces more than one process +belongs to: +____ + lsns --filter 'USER != "root" and NPROCS > 1' +____ + *-r*, *--raw*:: Use the raw output format. @@ -67,7 +80,7 @@ Do not truncate text in columns. *-W*, *--nowrap*:: Do not use multi-line text in columns. -*-T*, *--tree* _rel_:: +*-T*, *--tree*[**=**__rel__]:: Use tree-like output format. If *process* is given as _rel_, print process tree(s) in each name space. This is default when *--tree* is not specified. If *parent* is given, print tree(s) constructed by the parent/child relationship. If *owner* is given, print tree(s) constructed by the owner/owned relationship. *owner* is used as default when _rel_ is omitted. include::man-common/help-version.adoc[] @@ -95,6 +108,7 @@ mailto:kzak@redhat.com[Karel Zak] *namespaces*(7), *ioctl_ns*(2), *ip-netns*(8) +*scols-filter*(5) include::man-common/bugreports.adoc[] diff --git a/sys-utils/lsns.c b/sys-utils/lsns.c index e68bdbed46..f444f5e1a5 100644 --- a/sys-utils/lsns.c +++ b/sys-utils/lsns.c @@ -22,9 +22,9 @@ #include <wchar.h> #include <libsmartcols.h> #include <libmount.h> +# include <stdbool.h> #ifdef HAVE_LINUX_NET_NAMESPACE_H -# include <stdbool.h> # include <sys/socket.h> # include <linux/netlink.h> # include <linux/rtnetlink.h> @@ -50,6 +50,7 @@ #include "namespace.h" #include "idcache.h" #include "fileutils.h" +#include "column-list-table.h" #include "debug.h" @@ -59,6 +60,7 @@ UL_DEBUG_DEFINE_MASKNAMES(lsns) = UL_DEBUG_EMPTY_MASKNAMES; #define LSNS_DEBUG_INIT (1 << 1) #define LSNS_DEBUG_PROC (1 << 2) #define LSNS_DEBUG_NS (1 << 3) +#define LSNS_DEBUG_FILTER (1 << 4) #define LSNS_DEBUG_ALL 0xFFFF #define LSNS_NETNS_UNUSABLE -2 @@ -218,6 +220,7 @@ struct lsns { struct libmnt_table *tab; + struct libscols_filter *filter; }; struct netnsid_cache { @@ -226,6 +229,13 @@ struct netnsid_cache { struct list_head netnsids; }; +/* "userdata" used by callback for libsmartcols filter */ +struct filler_data { + struct lsns *ls; + struct lsns_namespace *ns; + struct lsns_process *proc; +}; + static struct list_head netnsids_cache; static int netlink_fd = -1; @@ -1012,6 +1022,87 @@ static int nsfs_xasputs(char **str, return 1; } + +static void fill_column(struct lsns *ls, + struct lsns_namespace *ns, + struct lsns_process *proc, + struct libscols_line *line, + size_t column_index) +{ + char *str = NULL; + + switch (get_column_id(column_index)) { + case COL_NS: + xasprintf(&str, "%ju", (uintmax_t)ns->id); + break; + case COL_PID: + if (proc) + xasprintf(&str, "%d", (int) proc->pid); + break; + case COL_PPID: + if (proc) + xasprintf(&str, "%d", (int) proc->ppid); + break; + case COL_TYPE: + xasprintf(&str, "%s", ns_names[ns->type]); + break; + case COL_NPROCS: + xasprintf(&str, "%d", ns->nprocs); + break; + case COL_COMMAND: + if (!proc) + break; + str = pid_get_cmdline(proc->pid); + if (!str) + str = pid_get_cmdname(proc->pid); + break; + case COL_PATH: + if (!proc) + break; + xasprintf(&str, "/proc/%d/ns/%s", (int) proc->pid, ns_names[ns->type]); + break; + case COL_UID: + xasprintf(&str, "%d", proc? (int) proc->uid: (int) ns->uid_fallback); + break; + case COL_USER: + xasprintf(&str, "%s", get_id(uid_cache, proc? proc->uid: ns->uid_fallback)->name); + break; + case COL_NETNSID: + if (!proc) + break; + if (ns->type == LSNS_ID_NET) + netnsid_xasputs(&str, proc->netnsid); + break; + case COL_NSFS: + nsfs_xasputs(&str, ns, ls->tab, ls->no_wrap ? ',' : '\n'); + break; + case COL_PNS: + xasprintf(&str, "%ju", (uintmax_t)ns->related_id[RELA_PARENT]); + break; + case COL_ONS: + xasprintf(&str, "%ju", (uintmax_t)ns->related_id[RELA_OWNER]); + break; + default: + break; + } + + if (str && scols_line_refer_data(line, column_index, str) != 0) + err_oom(); +} + + +static int filter_filler_cb( + struct libscols_filter *filter __attribute__((__unused__)), + struct libscols_line *line, + size_t column_index, + void *userdata) +{ + struct filler_data *fid = (struct filler_data *) userdata; + + fill_column(fid->ls, fid->ns, fid->proc, line, column_index); + return 0; +} + static void add_scols_line(struct lsns *ls, struct libscols_table *table, struct lsns_namespace *ns, struct lsns_process *proc) { @@ -1031,66 +1122,34 @@ static void add_scols_line(struct lsns *ls, struct libscols_table *table, return; } - for (i = 0; i < ncolumns; i++) { - char *str = NULL; + if (ls->filter) { + int status = 0; + struct filler_data fid = { + .ls = ls, + .ns = ns, + .proc = proc, + }; - switch (get_column_id(i)) { - case COL_NS: - xasprintf(&str, "%ju", (uintmax_t)ns->id); - break; - case COL_PID: - if (proc) - xasprintf(&str, "%d", (int) proc->pid); - break; - case COL_PPID: - if (proc) - xasprintf(&str, "%d", (int) proc->ppid); - break; - case COL_TYPE: - xasprintf(&str, "%s", ns_names[ns->type]); - break; - case COL_NPROCS: - xasprintf(&str, "%d", ns->nprocs); - break; - case COL_COMMAND: - if (!proc) - break; - str = pid_get_cmdline(proc->pid); - if (!str) - str = pid_get_cmdname(proc->pid); - break; - case COL_PATH: - if (!proc) - break; - xasprintf(&str, "/proc/%d/ns/%s", (int) proc->pid, ns_names[ns->type]); - break; - case COL_UID: - xasprintf(&str, "%d", proc? (int) proc->uid: (int) ns->uid_fallback); - break; - case COL_USER: - xasprintf(&str, "%s", get_id(uid_cache, proc? proc->uid: ns->uid_fallback)->name); - break; - case COL_NETNSID: - if (!proc) - break; - if (ns->type == LSNS_ID_NET) - netnsid_xasputs(&str, proc->netnsid); - break; - case COL_NSFS: - nsfs_xasputs(&str, ns, ls->tab, ls->no_wrap ? ',' : '\n'); - break; - case COL_PNS: - xasprintf(&str, "%ju", (uintmax_t)ns->related_id[RELA_PARENT]); - break; - case COL_ONS: - xasprintf(&str, "%ju", (uintmax_t)ns->related_id[RELA_OWNER]); - break; - default: - break; + scols_filter_set_filler_cb(ls->filter, + filter_filler_cb, (void *) &fid); + + if (scols_line_apply_filter(line, ls->filter, &status)) + err(EXIT_FAILURE, _("failed to apply filter")); + if (status == 0) { + struct libscols_line *x = scols_line_get_parent(line); + + if (x) + scols_line_remove_child(x, line); + + scols_table_remove_line(table, line); + return; } + } - if (str && scols_line_refer_data(line, i, str) != 0) - err_oom(); + for (i = 0; i < ncolumns; i++) { + if (scols_line_is_filled(line, i)) + continue; + fill_column(ls, ns, proc, line, i); } if (ls->tree == LSNS_TREE_OWNER || ls->tree == LSNS_TREE_PARENT) @@ -1139,7 +1198,7 @@ static struct libscols_table *init_scols_table(struct lsns *ls) warnx(_("failed to initialize output column")); goto err; } - if (ls->json) + if (ls->json || ls->filter) scols_column_set_json_type(cl, col->json_type); if (!ls->no_wrap && get_column_id(i) == COL_NSFS) { @@ -1179,6 +1238,55 @@ static void show_namespace(struct lsns *ls, struct libscols_table *tab, add_scols_line(ls, tab, ns, proc); } +static inline void add_column(int id) +{ + if (ncolumns >= ARRAY_SIZE(columns)) + errx(EXIT_FAILURE, _("too many columns specified, " + "the limit is %zu columns"), + ARRAY_SIZE(columns) - 1); + columns[ ncolumns++ ] = id; +} + +static void init_scols_filter(struct libscols_table *tb, struct libscols_filter *f) +{ + struct libscols_iter *itr; + const char *name = NULL; + int nerrs = 0; + + itr = scols_new_iter(SCOLS_ITER_FORWARD); + if (!itr) + err(EXIT_FAILURE, _("failed to allocate iterator")); + + while (scols_filter_next_holder(f, itr, &name, 0) == 0) { + struct libscols_column *col = scols_table_get_column_by_name(tb, name); + int id = column_name_to_id(name, strlen(name)); + const struct colinfo *ci = id >= 0 ? &infos[id] : NULL; + + if (!ci) { + nerrs++; + continue; /* report all unknown columns */ + } + if (!col) { + add_column(id); + col = scols_table_new_column(tb, ci->name, + ci->whint, SCOLS_FL_HIDDEN); + if (!col) + err(EXIT_FAILURE,_("failed to allocate output column")); + + scols_column_set_json_type(col, ci->json_type); + } + + scols_filter_assign_column(f, itr, name, col); + } + + scols_free_iter(itr); + + if (!nerrs) + return; + + errx(EXIT_FAILURE, _("failed to initialize filter")); +} + static int show_namespaces(struct lsns *ls) { struct libscols_table *tab; @@ -1189,6 +1297,8 @@ static int show_namespaces(struct lsns *ls) if (!tab) return -ENOMEM; + init_scols_filter(tab, ls->filter); + list_for_each(p, &ls->namespaces) { struct lsns_namespace *ns = list_entry(p, struct lsns_namespace, namespaces); @@ -1267,10 +1377,22 @@ static void free_all(struct lsns *ls) list_free(&ls->namespaces, struct lsns_namespace, namespaces, free_lsns_namespace); } +static struct libscols_filter *new_filter(const char *query) +{ + struct libscols_filter *f; + + f = scols_new_filter(NULL); + if (!f) + err(EXIT_FAILURE, _("failed to allocate filter")); + if (query && scols_filter_parse_string(f, query) != 0) + errx(EXIT_FAILURE, _("failed to parse \"%s\": %s"), query, + scols_filter_get_errmsg(f)); + return f; +} + static void __attribute__((__noreturn__)) usage(void) { FILE *out = stdout; - size_t i; fputs(USAGE_HEADER, out); @@ -1292,20 +1414,30 @@ static void __attribute__((__noreturn__)) usage(void) fputs(_(" -u, --notruncate don't truncate text in columns\n"), out); fputs(_(" -W, --nowrap don't use multi-line representation\n"), out); fputs(_(" -t, --type <name> namespace type (mnt, net, ipc, user, pid, uts, cgroup, time)\n"), out); - fputs(_(" -T, --tree <rel> use tree format (parent, owner, or process)\n"), out); + fputs(_(" -T, --tree[=<rel>] use tree format (parent, owner, or process)\n"), out); fputs(USAGE_SEPARATOR, out); + fputs(_(" -H, --list-columns list the available columns\n"), out); fprintf(out, USAGE_HELP_OPTIONS(24)); - - fputs(USAGE_COLUMNS, out); - for (i = 0; i < ARRAY_SIZE(infos); i++) - fprintf(out, " %11s %s\n", infos[i].name, _(infos[i].help)); - fprintf(out, USAGE_MAN_TAIL("lsns(8)")); exit(EXIT_SUCCESS); } +static void __attribute__((__noreturn__)) list_colunms(bool raw, bool json) +{ + struct libscols_table *col_tb = xcolumn_list_table_new("lsns-columns", stdout, raw, json); + + for (size_t i = 0; i < ARRAY_SIZE(infos); i++) + xcolumn_list_table_append_line(col_tb, infos[i].name, + infos[i].json_type, NULL, + _(infos[i].help)); + + scols_print_table(col_tb); + scols_unref_table(col_tb); + + exit(EXIT_SUCCESS); +} int main(int argc, char *argv[]) { @@ -1323,6 +1455,7 @@ int main(int argc, char *argv[]) { "output", required_argument, NULL, 'o' }, { "output-all", no_argument, NULL, OPT_OUTPUT_ALL }, { "persistent", no_argument, NULL, 'P' }, + { "filter", required_argument, NULL, 'Q' }, { "notruncate", no_argument, NULL, 'u' }, { "version", no_argument, NULL, 'V' }, { "noheadings", no_argument, NULL, 'n' }, @@ -1331,6 +1464,7 @@ int main(int argc, char *argv[]) { "raw", no_argument, NULL, 'r' }, { "type", required_argument, NULL, 't' }, { "tree", optional_argument, NULL, 'T' }, + { "list-columns", no_argument, NULL, 'H' }, { NULL, 0, NULL, 0 } }; @@ -1356,7 +1490,7 @@ int main(int argc, char *argv[]) INIT_LIST_HEAD(&netnsids_cache); while ((c = getopt_long(argc, argv, - "JlPp:o:nruhVt:T::W", long_opts, NULL)) != -1) { + "JlPp:o:nruhVt:T::WQ:H", long_opts, NULL)) != -1) { err_exclusive_options(c, long_opts, excl, excl_st); @@ -1416,6 +1550,11 @@ int main(int argc, char *argv[]) errx(EXIT_FAILURE, _("unknown tree type: %s"), optarg); } break; + case 'Q': + ls.filter = new_filter(optarg); + break; + case 'H': + list_colunms(ls.raw, ls.json); case 'h': usage(); @@ -1500,6 +1639,7 @@ int main(int argc, char *argv[]) r = show_namespaces(&ls); } + scols_unref_filter(ls.filter); mnt_free_table(ls.tab); if (netlink_fd >= 0) close(netlink_fd); diff --git a/tests/expected/lsns/filter b/tests/expected/lsns/filter new file mode 100644 index 0000000000..ac79b6b367 --- /dev/null +++ b/tests/expected/lsns/filter @@ -0,0 +1,2 @@ +-Q: pid == PID +--filter: pid == PID diff --git a/tests/helpers/test_mkfds.c b/tests/helpers/test_mkfds.c index b0d34c6050..dd0a128a88 100644 --- a/tests/helpers/test_mkfds.c +++ b/tests/helpers/test_mkfds.c @@ -3207,6 +3207,99 @@ static void *make_mmap(const struct factory *factory, struct fdesc fdescs[] _U_, return data; } +/* Do as unshare --map-root-user. */ +static void map_root_user(uid_t uid, uid_t gid) +{ + char buf[BUFSIZ]; + int n; + int mapfd; + int r; + + n = snprintf(buf, sizeof(buf), "0 %d 1", uid); + mapfd = open("/proc/self/uid_map", O_WRONLY); + if (mapfd < 0) + err(EXIT_FAILURE, + "failed to open /proc/self/uid_map"); + r = write (mapfd, buf, n); + if (r < 0) + err(EXIT_FAILURE, + "failed to write to /proc/self/uid_map"); + if (r != n) + errx(EXIT_FAILURE, + "failed to write to /proc/self/uid_map"); + close(mapfd); + + mapfd = open("/proc/self/setgroups", O_WRONLY); + if (mapfd < 0) + err(EXIT_FAILURE, + "failed to open /proc/self/setgroups"); + r = write (mapfd, "deny", 4); + if (r < 0) + err(EXIT_FAILURE, + "failed to write to /proc/self/setgroups"); + if (r != 4) + errx(EXIT_FAILURE, + "failed to write to /proc/self/setgroups"); + close(mapfd); + + n = snprintf(buf, sizeof(buf), "0 %d 1", gid); + mapfd = open("/proc/self/gid_map", O_WRONLY); + if (mapfd < 0) + err(EXIT_FAILURE, + "failed to open /proc/self/gid_map"); + r = write (mapfd, buf, n); + if (r < 0) + err(EXIT_FAILURE, + "failed to write to /proc/self/gid_map"); + if (r != n) + errx(EXIT_FAILURE, + "failed to write to /proc/self/gid_map"); + close(mapfd); +} + +static void *make_userns(const struct factory *factory _U_, struct fdesc fdescs[], + int argc _U_, char ** argv _U_) +{ + uid_t uid = geteuid(); + uid_t gid = getegid(); + + if (unshare(CLONE_NEWUSER) < 0) + err((errno == EPERM? EXIT_EPERM: EXIT_FAILURE), + "failed in the 1st unshare(2)"); + + map_root_user(uid, gid); + + int userns = open("/proc/self/ns/user", O_RDONLY); + if (userns < 0) + err(EXIT_FAILURE, "failed to open /proc/self/ns/user for the new user ns"); + + if (unshare(CLONE_NEWUSER) < 0) { + int e = errno; + close(userns); + errno = e; + err((errno == EPERM? EXIT_EPERM: EXIT_FAILURE), + "failed in the 2nd unshare(2)"); + } + + if (userns != fdescs[0].fd) { + if (dup2(userns, fdescs[0].fd) < 0) { + int e = errno; + close(userns); + errno = e; + err(EXIT_FAILURE, "failed to dup %d -> %d", userns, fdescs[0].fd); + } + close(userns); + } + + fdescs[0] = (struct fdesc){ + .fd = fdescs[0].fd, + .close = close_fdesc, + .data = NULL + }; + + return NULL; +} + static void free_mmap(const struct factory * factory _U_, void *data) { munmap(((struct mmap_data *)data)->addr, @@ -3999,7 +4092,7 @@ static const struct factory factories[] = { }, { .name = "multiplexing", - .desc = "making pipes monitored by multiplexers", + .desc = "make pipes monitored by multiplexers", .priv = false, .N = 12, .EX_N = 0, @@ -4063,6 +4156,17 @@ static const struct factory factories[] = { PARAM_END }, }, + { + .name = "userns", + .desc = "open a user namespae", + .priv = false, + .N = 1, + .EX_N = 0, + .make = make_userns, + .params = (struct parameter []) { + PARAM_END + } + }, }; static int count_parameters(const struct factory *factory) diff --git a/tests/ts/lsns/filter b/tests/ts/lsns/filter new file mode 100755 index 0000000000..fa8b090aed --- /dev/null +++ b/tests/ts/lsns/filter @@ -0,0 +1,65 @@ +#!/bin/bash +# +# Copyright (C) 2024 Masatake YAMATO <yamato@redhat.com> +# +# 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="-Q, --filter option" + +. "$TS_TOPDIR"/functions.sh +# for $EPERM +. "$TS_TOPDIR"/ts/lsfd/lsfd-functions.bash + +ts_init "$*" + +ts_check_test_command "$TS_CMD_LSNS" +ts_check_test_command "$TS_CMD_LSFD" + +ts_check_test_command "$TS_HELPER_MKFDS" + +# unshare(2) used in userns factory of test_mkfds reports "Invalid argument". +ts_skip_qemu_user + +ts_cd "$TS_OUTDIR" +PID= +FD=4 +EXPR= + +{ + coproc MKFDS { "$TS_HELPER_MKFDS" --comm ABC userns $FD; } + if read -u ${MKFDS[0]} PID; then + expr="PID == \"${PID}\" and ASSOC == \"user\"" + inode=$(${TS_CMD_LSFD} -n --raw -o INODE -Q "${expr}") + for opt in -Q --filter; do + pid=$(${TS_CMD_LSNS} -n --raw -o PID "$opt" "NS == $inode && NPROCS == 1") + if [[ "$pid" = "$PID" ]]; then + echo "$opt: pid == PID" + else + echo "$opt: pid != PID" + echo expr: "${expr}" + ${TS_CMD_LSFD} -n --raw -o INODE -Q "${expr}" + echo inode: "${inode}" + ${TS_CMD_LSNS} -n --raw -o PID -Q "NS == $inode && NPROCS == 1" + echo pid: "${pid}" + echo PID: "${PID}" + fi + done + echo DONE >&"${MKFDS[1]}" + fi + wait "${MKFDS_PID}" +} > $TS_OUTPUT 2>&1 +if [ "$?" == "$EPERM" ]; then + ts_skip "unshare(2) is not permitted on this platform" +fi +ts_finalize |