From d22d51229f2a8a9b594ad3fc642f2845b4ef44e8 Mon Sep 17 00:00:00 2001 From: Richard Lawrence Date: Wed, 4 Nov 2026 14:20:19 -0600 Subject: pasta: Add --pass-fds option to skip closing file descriptors When pasta spawns a command (`pasta [options] -- [command]`), all open file descriptors inherited from the parent process except standard streams (0-2) and the tap descriptor (`--fd`) are normally closed by `isolate_fds()`. However, when pasta is used for testing, development, or launching tools like Bubblewrap (`bwrap`), the spawned command may require file descriptors passed directly from the parent process. In this case, closing inherited file descriptors breaks expected behavior. Add a boolean `--pass-fds` flag for pasta mode that skips the `close_range()` cleanup step in `isolate_fds()`, leaving all open file descriptors intact for the child command. Summary of changes: - Update `conf_tap_fd()` to `conf_fds()`, allowing early parsing of both `-F`/`--fd` and `--pass-fds` before main option processing. - Update `isolate_fds()` to check `c->pass_fds`: if set, skip calling `close_range()` while still relocating the tap fd out of standard stream range (0-2) if needed. - Enforce validation in `conf()` ensuring `--pass-fds` is restricted to pasta mode when spawning a command (incompatible with attaching to an existing netns). - Update man page and usage help output. Link: https://bugs.passt.top/show_bug.cgi?id=204 Signed-off-by: Richard Lawrence [sbrivio: Fix trivial merge conflict and minor coding style issues] Signed-off-by: Stefano Brivio --- conf.c | 55 +++++++++++++++++++++++++++++------------------- conf.h | 2 +- isolation.c | 70 ++++++++++++++++++++++++++++++++++++------------------------- isolation.h | 2 +- passt.1 | 5 +++++ passt.c | 2 +- passt.h | 2 ++ 7 files changed, 85 insertions(+), 53 deletions(-) diff --git a/conf.c b/conf.c index 2e18a66..ef58e56 100644 --- a/conf.c +++ b/conf.c @@ -753,7 +753,8 @@ pasta_opts: " Don't copy all addresses to namespace\n" " --ns-mac-addr ADDR Set MAC address on tap interface\n" " --no-splice Disable inbound socket splicing\n" - " --splice-only Only enable loopback forwarding\n"); + " --splice-only Only enable loopback forwarding\n" + " --pass-fds Preserve open file descriptors\n"); passt_exit(status); } @@ -1144,35 +1145,38 @@ static void conf_sock_listen(const struct ctx *c) } /** - * conf_tap_fd() - Read tap fd as supplied by -F command line option + * conf_fds() - Read fds as supplied by -F/--fd and --pass-fds options + * @c: Execution context * @argc: Argument count * @argv: Command line options - * - * Return: fd number from --fd option, or -1 if not supplied */ -int conf_tap_fd(int argc, char **argv) +void conf_fds(struct ctx *c, int argc, char **argv) { - const struct option optfd[] = { { "fd", required_argument, NULL, 'F' }, - { 0 }, }; - const char *fdarg = NULL, *p; - unsigned long val; + const struct option opt[] = { + { "fd", required_argument, NULL, 'F' }, + { "pass-fds", no_argument, NULL, 2 }, + { 0 }, + }; int name; + c->fd_tap = -1; + c->pass_fds = false; optind = 0; do { - name = getopt_long(argc, argv, "-:F:", optfd, NULL); - if (name == 'F') - fdarg = optarg; - } while (name != -1); - - if (!fdarg) - return -1; + name = getopt_long(argc, argv, "-:F:", opt, NULL); + if (name == 'F') { + const char *p = optarg; + unsigned long val; - p = fdarg; - if (!parse_unsigned(&p, 0, &val) || !parse_eoi(p) || val > INT_MAX) - die("Invalid --fd: %s", fdarg); + if (!parse_unsigned(&p, 0, &val) || !parse_eoi(p) || + val > INT_MAX) + die("Invalid --fd: %s", optarg); - return val; + c->fd_tap = (int)val; + } else if (name == 2) { + c->pass_fds = true; + } + } while (name != -1); } /** @@ -1326,6 +1330,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-pidns", no_argument, NULL, 33 }, {"dhcp-opt", required_argument, NULL, 34 }, {"dhcp-boot", required_argument, NULL, 35 }, + {"pass-fds", no_argument, NULL, 36 }, { 0 }, }; const char *optstring = "+dqfel:hs:c:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:"; @@ -1595,6 +1600,11 @@ void conf(struct ctx *c, int argc, char **argv) case 35: dhcp_set_opt(67, optarg); break; + case 36: + /* --pass-fds was parsed early */ + if (c->mode != MODE_PASTA) + die("--pass-fds is for pasta mode only"); + break; case 'd': c->debug = 1; c->quiet = 0; @@ -1631,7 +1641,7 @@ void conf(struct ctx *c, int argc, char **argv) c->fd_control_listen = c->fd_control = -1; break; case 'F': - /* --fd was parsed early and c->fd_tap set in main() */ + /* --fd was parsed early */ c->one_off = true; *c->sock_path = 0; break; @@ -1957,6 +1967,9 @@ void conf(struct ctx *c, int argc, char **argv) else if (optind != argc) die("Extra non-option argument: %s", argv[optind]); + if (c->pass_fds && *netns) + die("--pass-fds can only be used when spawning a command"); + if (c->no_pidns && *netns) die("--no-pidns is incompatible with PID or --netns"); diff --git a/conf.h b/conf.h index 19bf9bc..654fd16 100644 --- a/conf.h +++ b/conf.h @@ -7,7 +7,7 @@ #define CONF_H enum passt_modes conf_mode(int argc, char *argv[]); -int conf_tap_fd(int argc, char **argv); +void conf_fds(struct ctx *c, int argc, char **argv); void conf(struct ctx *c, int argc, char **argv); void conf_listen_handler(struct ctx *c, uint32_t events); void conf_handler(struct ctx *c, uint32_t events); diff --git a/isolation.c b/isolation.c index a00475f..0cb6adb 100644 --- a/isolation.c +++ b/isolation.c @@ -249,47 +249,59 @@ void isolate_initial(void) drop_caps_ep_except(keep); } -/* - * isolate_fds() - Close leaked files, but not --fd, stdin, stdout, stderr +/** + * isolate_fds() - Close leaked files if applicable + * @c: Execution context * @argc: Argument count - * @argv: Command line options, as we need to skip any file given via --fd + * @argv: Command line options * * Should: - * - close all open files except for standard streams and the one from --fd + * - close all open files except for standard streams and the one from --fd, + * unless --pass-fds is used, in which case all fds are preserved * - move the --fd descriptor out of the range 0-2 - * - * Return: new fd number for descriptor from --fd, or -1 if not specified + * - update the execution context */ -int isolate_fds(int argc, char **argv) +void isolate_fds(struct ctx *c, int argc, char **argv) { - int fd, close_from = STDERR_FILENO + 1; + conf_fds(c, argc, argv); - fd = conf_tap_fd(argc, argv); + if (c->pass_fds) { + if (c->fd_tap >= 0 && c->fd_tap < STDERR_FILENO + 1) { + /* Move the tap fd to a safer location */ + int new_fd = fcntl(c->fd_tap, F_DUPFD, + STDERR_FILENO + 1); - if (fd >= 0) { - /* Move the passed fd to a more convenient location */ - if (fd != close_from && - (dup2(fd, close_from) != close_from || - close(fd))) - die_perror("Could not move --fd descriptor"); - fd = close_from++; - } + if (new_fd < 0 || close(c->fd_tap)) + die_perror("Could not move --fd descriptor"); + c->fd_tap = new_fd; + } + } else { + int close_from = STDERR_FILENO + 1; + + if (c->fd_tap >= 0) { + /* Move the tap fd to a more convenient location */ + if (c->fd_tap != close_from && + (dup2(c->fd_tap, close_from) != close_from || + close(c->fd_tap))) + die_perror("Could not move --fd descriptor"); + c->fd_tap = close_from++; + } - if (close_range(close_from, ~0U, CLOSE_RANGE_UNSHARE)) { - if (errno == ENOSYS || errno == EINVAL) { - /* This probably means close_range() or the - * CLOSE_RANGE_UNSHARE flag is not supported by the - * kernel. Not much we can do here except carry on and - * hope for the best. - */ - warn( + /* Close leaked file descriptors */ + if (close_range(close_from, ~0U, CLOSE_RANGE_UNSHARE)) { + if (errno == ENOSYS || errno == EINVAL) { + /* This probably means close_range() or the + * CLOSE_RANGE_UNSHARE flag is not supported by + * the kernel. Not much we can do here except + * carry on and hope for the best. + */ + warn( "Can't use close_range() to ensure no files leaked by parent"); - } else { - die_perror("Failed to close files leaked by parent"); + } else { + die_perror("Failed to close files leaked by parent"); + } } } - - return fd; } /** diff --git a/isolation.h b/isolation.h index ec47038..fe8d7d5 100644 --- a/isolation.h +++ b/isolation.h @@ -11,7 +11,7 @@ #include void isolate_initial(void); -int isolate_fds(int argc, char **argv); +void isolate_fds(struct ctx *c, int argc, char **argv); void isolate_user(const struct ctx *c, uid_t uid, gid_t gid, bool use_userns, const char *userns); int isolate_prefork(const struct ctx *c); diff --git a/passt.1 b/passt.1 index 0176094..9a5cc87 100644 --- a/passt.1 +++ b/passt.1 @@ -814,6 +814,11 @@ of local traffic in pasta\fR in the \fBNOTES\fR for more details. Do not create a tap device in the namespace. In this mode, \fIpasta\fR only forwards loopback traffic between namespaces. +.TP +.BR \-\-pass\-fds +Preserve all open file descriptors and pass them to the spawned command. +This option may only be used when \fBpasta\fR spawns a command. + .SH EXAMPLES .SS \fBpasta diff --git a/passt.c b/passt.c index 5054551..fa7afc5 100644 --- a/passt.c +++ b/passt.c @@ -345,7 +345,7 @@ int main(int argc, char **argv) arch_avx2_exec(argv); isolate_initial(); - c->fd_tap = isolate_fds(argc, argv); + isolate_fds(c, argc, argv); if ((devnull_fd = open("/dev/null", O_RDWR | O_CLOEXEC)) < 0) die_perror("Failed to open /dev/null"); diff --git a/passt.h b/passt.h index afd8e9f..947e859 100644 --- a/passt.h +++ b/passt.h @@ -196,6 +196,7 @@ struct ip6_ctx { * @pasta_ifi: Index of namespace interface for pasta * @pasta_conf_ns: Configure namespace after creating it * @no_pidns: Don't create a new PID namespace for spawned command + * @pass_fds: Don't close leaked file descriptors * @fwd: Forwarding tables * @fwd_pending: Pending forward tables * @no_tcp: Disable TCP operation @@ -280,6 +281,7 @@ struct ctx { unsigned int pasta_ifi; int pasta_conf_ns; bool no_pidns; + bool pass_fds; struct fwd_table *fwd[PIF_NUM_TYPES]; struct fwd_table *fwd_pending[PIF_NUM_TYPES]; -- cgit v1.2.3