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 22E015A0269 for ; Mon, 26 Oct 2026 18:03:55 +0100 (CET) Received: from pps.filterd (m0169867.ppops.net [127.0.0.1]) by mx0a-00178102.pphosted.com (8.18.1.11/8.18.1.11) with ESMTP id 69QGYSk02152909 for ; Mon, 26 Oct 2026 12:03:54 -0500 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=OdXoKjqJDPS3E1akKPtqIdd/c4eYrwk97Kbe SsdD3sA=; b=lMgLhtDcqn/xzFEF1p7gFfyXRw+b7sfZSha5DMSXiFkl7SOTsheA XatZZDVWPNxpIWPHK+NiqL5GOz2Z0uQ61wX1NcKxL6cffvVPMekI3U5T3dkOG6Mz O2SeJF7g7JLg4xOf7nPUCVQ9BWf+zWWlFKsV+jxhXyPiO8NY33rzQY5nPaDGaVt8 xlgBPaqXSJPZL/y6INEM5fyqbBJRM5TLiBTXcNd9V/YkymAIMUkrt3ScII77xuj6 mulU2nWkS2u4v38vFdj3CAWoaYh0Oy69CZZYnK0od7CrGd5vsqVef4/IJwEePLlz 5LzGcb3x0CIKcJ1Y07XWZJdsuePimC51rA== Received: from bespin.localdomain ([128.194.2.191]) by mx0a-00178102.pphosted.com (PPS) with ESMTPS id 4hja5s0mts-1 (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384 bits=256 verify=NOT); Mon, 26 Oct 2026 12:03:53 -0500 (CDT) Received: by bespin.localdomain (Postfix, from userid 1000) id 0C4DC68D3; Mon, 26 Oct 2026 12:03:53 -0500 (CDT) From: Richard Lawrence To: passt-dev@passt.top Subject: [PATCH v3] pasta: Add --pass-fds option to skip closing file descriptors Date: Mon, 26 Oct 2026 12:03:45 -0500 Message-ID: <20261026170345.634781-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-GUID: fiTPjHyZ9CmwvsMWHRO8gcweLelA6TUZ X-Authority-Analysis: v=2.4 cv=PbVqFShd c=1 sm=1 tr=0 ts=6adf87f9 cx=c_pps a=RM/V1YFJ8wA4X+FDJsk8mQ==:117 a=RM/V1YFJ8wA4X+FDJsk8mQ==:17 a=660iZSQnnn4A:10 a=x7bEGLp0ZPQA:10 a=VkNPw1HP01LnGYTKEx00:22 a=HuL5yKgYSaDc2Nh3iM7q:22 a=_Zy6Tl1u7MzQiFvf_v7L:22 a=pNgXkJcRAAAA:8 a=vYQHhxxEAWlcRbwsuHEA:9 a=O8hF6Hzn-FEA:10 a=tKe0A8swS_RTMQ4Dt1Wu:22 X-Proofpoint-Spam-Details-Enc: AW1haW4tMjYxMDI2MDA2NyBTYWx0ZWRfXwHJSceY0MQ2u Tup+wq/nTGFP9lnGOL+icRSMdnkPLV4qyaTLE2xXAyFpy9+1SUWJOAQEG+uHKb4gVYpxRP0oW5Y r5mWK09hVgyDCxpa9N3YCbKO95VlZrTrr55FKNivf2Qvu2SvEK+evF7t2ZnnDq9ZI45CNYNTbqO cJEJbo9FZeMEERjeZoDDbTfnJzIPVGB8VmTMyhgBca9oPua/7RiIffcmTK5thRzL5YxP9pUJDg6 XUBVhrHV2zEMeej7+Ox5B/qk0Mwy1skqIEG31+5ORZxzS5gRVfoGF4t8+XWvFjMsMOnbwoSCqUh VpGCcgCtnWNbihDTkklaCHAHmoGeMMb9mCIFhA9Rd8zbnnTU2eEDvOZXz83hWl/shehg/IhEoRb JEN6PV7VX7ADe55KZ0VXnlI+SlFVvGNa32QzJTffrhLoqks6lqmR0ZarDL70X/l1PENRqEwHPQ2 Y7FKZ0syvU+qUap0HEQ== X-Proofpoint-ORIG-GUID: 3ANnDWzAQGPQNcgYHfVVvzECcO133mhJ X-Proofpoint-Spam-Info: AW1haW4tMjYxMDI2MDA2NyBTYWx0ZWRfX7DCxtdyPcDio sqy+6Nw8IXuAm5RygCyik/uXxcfdbvGPYGFu4oFaVW1TBFmMBh0F9oN17L76fyo/0r1OK4SH1gc 3UFF/kvxafyP6u6rUqZ4wpCh+ItZQ+s= 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: N7SJETLCV5VTUMYCJPBSCRXFKFH2KEBC X-Message-ID-Hash: N7SJETLCV5VTUMYCJPBSCRXFKFH2KEBC X-Mailman-Approved-At: Mon, 26 Oct 2026 18:29:48 +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->pasta_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 | 56 +++++++++++++++++++++++++--------------- conf.h | 2 +- isolation.c | 73 +++++++++++++++++++++++++++++++++-------------------- isolation.h | 2 +- passt.1 | 5 ++++ passt.c | 2 +- passt.h | 2 ++ 7 files changed, 91 insertions(+), 51 deletions(-) diff --git a/conf.c b/conf.c index 149a4c7..30edef9 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,41 @@ 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 + * Should: + * - Set the fd_tap and pasta_pass_fds values in the execution context */ -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->pasta_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; /* May be overwritten in isolate_fd() */ + } else if (name == 2) { + c->pasta_pass_fds = true; + } + } while (name != -1); } /** @@ -1317,6 +1324,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", required_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 +1575,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 +1616,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 +1942,9 @@ void conf(struct ctx *c, int argc, char **argv) else if (optind != argc) die("Extra non-option argument: %s", argv[optind]); + if (c->pasta_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..766c297 100644 --- a/isolation.c +++ b/isolation.c @@ -250,46 +250,63 @@ 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->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) + die_perror("Could not relocate --fd descriptor"); + + close(c->fd_tap); + c->fd_tap = new_fd; } - 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 unless --pass-fds is used */ + if (!c->pasta_pass_fds) { + int rc = 0; + /* Keep standard streams and the tap fd (if given) */ + int keep = c->fd_tap >= STDERR_FILENO ? c->fd_tap : STDERR_FILENO; + + if (keep > STDERR_FILENO + 1) { + /* Tap fd was relocated to above some other leaked fd(s)*/ + /* Close fds between stderr and the tap fd */ + if (close_range(STDERR_FILENO + 1, keep - 1, CLOSE_RANGE_UNSHARE)) + rc = -1; } - } - return fd; + /* Close all other fds */ + if (close_range(keep + 1, ~0U, CLOSE_RANGE_UNSHARE)) + rc = -1; + + if (rc) { + 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"); + } + } + } } /** 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..b065d41 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 + * @pasta_pass_fds: Do not 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 pasta_pass_fds; struct fwd_table *fwd[PIF_NUM_TYPES]; struct fwd_table *fwd_pending[PIF_NUM_TYPES]; -- 2.52.0