From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=fail (p=quarantine dis=none) header.from=tamu.edu Received: from mx0a-00178102.pphosted.com (mx0a-00178102.pphosted.com [148.163.135.245]) by passt.top (Postfix) with ESMTPS id 0C0315A026E for ; Wed, 04 Nov 2026 21:20:23 +0100 (CET) Received: from pps.filterd (m0169864.ppops.net [127.0.0.1]) by mx0a-00178102.pphosted.com (8.18.1.11/8.18.1.11) with ESMTP id 6A4KAsvP511040 for ; Wed, 4 Nov 2026 14:20:22 -0600 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tamu.edu; h=cc :content-transfer-encoding:date:from:message-id:mime-version :subject:to; s=ppae6d7b; bh=I4stJuTtuFiDFMXtAd++wuO5oLqmcUMqQM+x Lmk7alY=; b=JjMXmsT28pJM/2QKHbu6Sqpt8yzO+6nz/SF+K9bKpQUvaxl/U/HD Ky+c7rQuH0SdAu/Daxj6S5HsIBte2GUi8NrCMDQKItVel0YYgpUS5779KDHSvrlw njToICbey36Uueol2aAdYL8XsmnCF/NEj8YvhuN167IERZnVXeOtDERYAvlrhwN5 F4Pf7OhxGC6ktn+31yzQqXJkP9Ttc1tgydz68QazMmy8/u6paNDxdkrz74bFD9rF 3aXqTI5q7VCdZtIkPIUXnVVwySg81/Cg+Jp7HWUxQrjGJfNdxncEUHq9Y3cy4VLv Uxp3/U2y9zwkHqjeUSU/+z9nCop3aJXfag== Received: from bespin.localdomain ([128.194.2.191]) by mx0a-00178102.pphosted.com (PPS) with ESMTPS id 4hr83c9v4u-1 (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384 bits=256 verify=NOT); Wed, 04 Nov 2026 14:20:21 -0600 (CST) Received: by bespin.localdomain (Postfix, from userid 1000) id E6BF268C6; Wed, 4 Nov 2026 14:20:20 -0600 (CST) From: Richard Lawrence To: passt-dev@passt.top Subject: [PATCH v4] pasta: Add --pass-fds option to skip closing file descriptors Date: Wed, 4 Nov 2026 14:20:19 -0600 Message-ID: <20261104202019.452348-1-rlawrence@tamu.edu> X-Mailer: git-send-email 2.52.0 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Proofpoint-Reinject: loops=2 maxloops=12 X-Proofpoint-ORIG-GUID: tQJrh2kC3nS5IymU0Tp9qiMVc3ezhbaq X-Proofpoint-GUID: CwH6EgPdPbDpv2bNi_v5suPDWH-8WL5Z X-Proofpoint-Spam-Details-Enc: AW1haW4tMjYxMTA0MDA4MCBTYWx0ZWRfX/6VfVtucHNJ0 x86Ma1hTLQKhSyTqe/mmmM+uKjKjYJN3aO4R+9dnfaOUaJ6LW3i1tSry9sPcEOoe5OOnUM/bXvN oIdtineG0ko/qd1QjrmFxrwsmkVooIIalJBcYYKv93eq1dTJypr9CE2jud9+Rf3ZOmqGmp1tIG3 GH25xG61Y804n7CNOycvIJSG2+3hfeqnlfIfpAq2MPyAhg5hNahWRJKiK15qKZNUmffvtAazijc BiQD/zw/jt5jrkhj9/MF1OJ22zwMsvC06FL8NO0w2MJWEZvuIbLEUIeJAe1MJBKpWgWZLJ0oN6r g5XqkKxct2tBbpGStq8qcsETzFrxP6evSDakwMuQhgMoXc3+KhyydHRsof2Qrc2Q4Gy7NTthibo U9vmJro9qjFfcIeU0+bzLofo1UijWV90ChhyschOTEUW237fHwPHWjBTGeh/hiI4HnVdnZG2tOc PLkfKZcbwlzYeibuzNw== X-Authority-Analysis: v=2.4 cv=cdZHPXDM c=1 sm=1 tr=0 ts=6aeb9385 cx=c_pps a=RM/V1YFJ8wA4X+FDJsk8mQ==:117 a=RM/V1YFJ8wA4X+FDJsk8mQ==:17 a=zJRp6_cAMcgA:10 a=x7bEGLp0ZPQA:10 a=VkNPw1HP01LnGYTKEx00:22 a=HuL5yKgYSaDc2Nh3iM7q:22 a=JH6xHg1a2rU9ozGLTmte:22 a=pNgXkJcRAAAA:8 a=vYQHhxxEAWlcRbwsuHEA:9 a=O8hF6Hzn-FEA:10 a=tKe0A8swS_RTMQ4Dt1Wu:22 X-Proofpoint-Spam-Info: AW1haW4tMjYxMTA0MDA4MCBTYWx0ZWRfX5l9YPtwkxToF MfyR+eO7iEowO6mNr+sM+i+VT3SOR5sdnsTqHhTpLcEnEIJx72c1iCn7XTA2yTBUDLhd6au5sy4 sszxIkU7pAEXFLwflrd0yaaST7O2fx4= X-MailFrom: rlawrence@bespin.localdomain X-Mailman-Rule-Hits: nonmember-moderation X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation Message-ID-Hash: I5NWIACECFCDB3P4ZW25CNTHVCPG2BQ4 X-Message-ID-Hash: I5NWIACECFCDB3P4ZW25CNTHVCPG2BQ4 X-Mailman-Approved-At: Wed, 04 Nov 2026 23:44:02 +0100 CC: Richard Lawrence X-Mailman-Version: 3.3.8 Precedence: list List-Id: Development discussion and patches for passt Archived-At: Archived-At: List-Archive: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- conf.c | 55 ++++++++++++++++++++++++++---------------- conf.h | 2 +- isolation.c | 69 +++++++++++++++++++++++++++++++---------------------- isolation.h | 2 +- passt.1 | 5 ++++ passt.c | 2 +- passt.h | 2 ++ 7 files changed, 84 insertions(+), 53 deletions(-) diff --git a/conf.c b/conf.c index 149a4c7..9f7eafb 100644 --- a/conf.c +++ b/conf.c @@ -750,7 +750,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); } @@ -1137,35 +1138,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); } /** @@ -1317,6 +1321,7 @@ void conf(struct ctx *c, int argc, char **argv) {"conf-path", required_argument, NULL, 'c' }, {"chroot-fallback", no_argument, NULL, 32 }, {"no-pidns", no_argument, NULL, 33 }, + {"pass-fds", no_argument, NULL, 34 }, { 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:"; @@ -1567,6 +1572,11 @@ void conf(struct ctx *c, int argc, char **argv) c->no_pidns = true; break; + case 34: + /* --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; @@ -1603,7 +1613,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; @@ -1929,6 +1939,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..1875e6b 100644 --- a/isolation.c +++ b/isolation.c @@ -250,46 +250,57 @@ void isolate_initial(void) } /* - * 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 relocate --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( -"Can't use close_range() to ensure no files leaked by parent"); - } else { - die_perror("Failed to close files leaked by parent"); + /* 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"); + } } } - - 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 0b780fe..56e241f 100644 --- a/passt.1 +++ b/passt.1 @@ -766,6 +766,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]; -- 2.52.0