From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=pass (p=quarantine dis=none) header.from=tamu.edu Authentication-Results: passt.top; dkim=pass (2048-bit key; secure) header.d=tamu.edu header.i=@tamu.edu header.a=rsa-sha256 header.s=ppae6d7b header.b=oJdBe2AR; dkim-atps=neutral Received: from mx0a-00178102.pphosted.com (mx0a-00178102.pphosted.com [148.163.135.245]) by passt.top (Postfix) with ESMTPS id 54F015A0265 for ; Fri, 31 Jul 2026 15:26:10 +0200 (CEST) Received: from pps.filterd (m0231227.ppops.net [127.0.0.1]) by mx0b-00178102.pphosted.com (8.18.1.11/8.18.1.11) with ESMTP id 66VD9MID840777 for ; Fri, 31 Jul 2026 08:26:08 -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=Etp/9jxGXY0+OWRLsQNgQWmh/3gHMUOWKzpz rWj2mS4=; b=oJdBe2ARMj4oJyCHIz4vY8bMXebnAiRIs5vrv8u6GbzHVlAGgIG0 qXYommkBeY9sYFq3sTcHK42kes0Ess0ueCotw94l1R3/hlBnbwFz3si0lBbaTevB FwFTphiaFFMYPKOD73O3vn9d/2No4M0R9ygqYy2edilUjhofflM4ES19wEHJ16K4 /M8Pa9WpqFGBwwet1mTxq7Bcm9N58ckVS2i2rCh/JUe1L9r3c5G2Ocuws+B+SBhH YVQ03aKBCBJ29zke0sZq3FAUOunAW6y9XWKfiCZ7ew/xMdk7KyuLV0kfEHD7gFml XDFOoISSlF0tHX8PLu2Dj7nhmwRx259kxg== Received: from bespin.localdomain ([128.194.2.191]) by mx0b-00178102.pphosted.com (PPS) with ESMTPS id 4frjva23vd-1 (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384 bits=256 verify=NOT); Fri, 31 Jul 2026 08:26:07 -0500 (CDT) Received: by bespin.localdomain (Postfix, from userid 1000) id 51AD7AE; Fri, 31 Jul 2026 08:26:07 -0500 (CDT) From: Richard Lawrence To: passt-dev@passt.top Subject: [PATCH v2] feat: Pass open files to child in pasta mode Date: Fri, 31 Jul 2026 08:26:01 -0500 Message-ID: <20260731132601.422518-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: cBlUAABosjSNE28BueIIDftA6C2BV3Yb X-Authority-Analysis: v=2.4 cv=d+rFDxjE c=1 sm=1 tr=0 ts=6a6ca270 cx=c_pps a=RM/V1YFJ8wA4X+FDJsk8mQ==:117 a=RM/V1YFJ8wA4X+FDJsk8mQ==:17 a=RAioF0-LDSMA:10 a=x7bEGLp0ZPQA:10 a=VkNPw1HP01LnGYTKEx00:22 a=HuL5yKgYSaDc2Nh3iM7q:22 a=efzhd9O6aJ8jRLlRsDx9:22 a=fFyUFXRT386AqKsRkF0A:9 a=O8hF6Hzn-FEA:10 X-Proofpoint-Spam-Details-Enc: AW1haW4tMjYwNzMxMDEwMSBTYWx0ZWRfX4930WBsvJNc3 zv+f+ZbEs9/Jz/QqObZjuoHkmYVB+gr0bgWrQNV1vVf+w50c+AzxGyV0kobfb6MXQmdhSbJApgW JIfxSfADmTMtSfexKaqZSn5gSQv5Z6wmIiufQ1q+caCzaB7JXTaJze96930r4uGboVQ90c+3j1j U0HYByoQIrxBnhzhlHiiKePNH5V6RqO0qC+w9schguS0m3RT/+qKuUMSgKwlmYY1U73N84yx6XO CM8y/h9Qezj3r6ZPP+PitTckvuutzO/aASIpU3qaH3X8IBlLsC7yxFwCSJit8zCxd+YLDDCK6k+ KmxV3Rmn3IqMvk4hOdOJHwMYQig82A2OkjxHmZdcND/XPMMT8+AM2RxNDPuaKScbOoNT/tZkARJ 9xgwV7SXw04iHiGRRRxKSwYTnrN/Q2Bedy7/H0f8F0R4DFb/6umlq+guKwWm4zCjVoNnZkRjadr 76rNmfiyvnwuFmIhV9A== X-Proofpoint-Spam-Info: AW1haW4tMjYwNzMxMDEwMSBTYWx0ZWRfX6T66IU9ERQ9J xb3USTdiVYaBJ4o5vU7Xmnn8Y4NUXBbOcPYI1u2RQKM5kqK++96BDy6F2lY0yKuL0wPUIB1yCrv zO2sYUGUjjVRy1fFFiaHimuXOFPbYU4= X-Proofpoint-ORIG-GUID: jtyDX3hMs6nlOhkEyVUoavK5AghgO6SJ 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: U76AGGGRIVP75HSMQ5Z5LSL553OD7IG2 X-Message-ID-Hash: U76AGGGRIVP75HSMQ5Z5LSL553OD7IG2 X-Mailman-Approved-At: Fri, 31 Jul 2026 20:11:06 +0200 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 mode is used to launch an executable (`pasta [COMMAND]`) and that executable accepts inputs in the form of arbitrary file descriptors (such as `bwrap`), then passt should not stand in the way of the parent process handing off those file descriptors to the child process. See bug 204 for additional discussion. The strategy used here is to delay the closing of inherited file descriptors until after pasta has forked to execute the COMMAND, which happens during the call to `conf()`. A nice side-effect is that the `--fd` argument no longer needs to be parsed early, and can rejoin the other args parsed normally in `conf()`. Highlighted changes: - `conf_tap_fd()` is now called directly by `conf()`. - The tap fd is relocated to a number at least 3, rather than exactly 3. - `snapshot_initial_fds()` memorizes inherited fds early in startup. - Inherited fds are discovered by reading from `/proc` if available. - `isolate_fds()` is now called after `conf()` in `main()`. Signed-off-by: Richard Lawrence --- conf.c | 39 ++++++++------- conf.h | 1 - isolation.c | 135 ++++++++++++++++++++++++++++++++++++---------------- isolation.h | 9 +++- passt.c | 7 ++- 5 files changed, 129 insertions(+), 62 deletions(-) diff --git a/conf.c b/conf.c index faf2681..6bfdfb6 100644 --- a/conf.c +++ b/conf.c @@ -1169,26 +1169,20 @@ static void conf_sock_listen(const struct ctx *c) } /** - * conf_tap_fd() - Read tap fd as supplied by -F command line option - * @argc: Argument count - * @argv: Command line options + * conf_tap_fd() - Read and relocate tap fd as supplied by -F command line option + * @fdarg: String containing fd + * + * Should: + * - move the --fd descriptor out of the range 0-2 * * Return: fd number from --fd option, or -1 if not supplied */ -int conf_tap_fd(int argc, char **argv) +int conf_tap_fd(const char *fdarg) { - const struct option optfd[] = { { "fd", required_argument, NULL, 'F' }, - { 0 }, }; - const char *fdarg = NULL, *p; unsigned long val; - int name; - - optind = 0; - do { - name = getopt_long(argc, argv, "-:F:", optfd, NULL); - if (name == 'F') - fdarg = optarg; - } while (name != -1); + const char *p; + int new_fd; + int fd; if (!fdarg) return -1; @@ -1197,7 +1191,18 @@ int conf_tap_fd(int argc, char **argv) if (!parse_unsigned(&p, 0, &val) || !parse_eoi(p) || val > INT_MAX) die("Invalid --fd: %s", fdarg); - return val; + fd = (int)val; + if (fd >= 0 && fd < 3) { + new_fd = fcntl(fd, F_DUPFD, 3); + + if (new_fd < 0) + die_perror("Could not relocate --fd descriptor"); + + close(fd); + fd = new_fd; + } + + return fd; } /** @@ -1625,7 +1630,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() */ + c->fd_tap = conf_tap_fd(optarg); c->one_off = true; *c->sock_path = 0; break; diff --git a/conf.h b/conf.h index 19bf9bc..16f9718 100644 --- a/conf.h +++ b/conf.h @@ -7,7 +7,6 @@ #define CONF_H enum passt_modes conf_mode(int argc, char *argv[]); -int conf_tap_fd(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 94cbe7f..2434748 100644 --- a/isolation.c +++ b/isolation.c @@ -24,19 +24,13 @@ * done anything we need to do with those resources, so we have * multiple stages of self-isolation. In order these are: * - * 1a. isolate_initial() + * 1. isolate_initial() * ==================== * * Executed immediately after startup, drops capabilities we don't * need at any point during execution (or which we gain back when we * need by joining other namespaces). * - * 1b. isolate_fds() - * ================ - * - * Executed immediately after isolate_initial(). Closes any leaked - * files we might have inherited from the parent process. - * * 2. isolate_user() * ================= * @@ -44,14 +38,20 @@ * operate in. Sets our final UID & GID, and enters the correct user * namespace. * - * 3. isolate_prefork() + * 3. isolate_fds() + * ================ + * + * Executed after conf(). Closes any leaked + * files we might have inherited from the parent process. + * + * 4. isolate_prefork() * ==================== * * Executed after all setup, but before daemonising (fork()ing into * the background). Uses mount namespace and pivot_root() to remove * our access to the filesystem. * - * 4. isolate_postfork() + * 5. isolate_postfork() * ===================== * * Executed immediately after daemonizing, but before entering the @@ -61,6 +61,7 @@ * runtime operation. */ +#include #include #include #include @@ -77,6 +78,7 @@ #include #include #include +#include #include #include #include @@ -248,47 +250,96 @@ void isolate_initial(void) drop_caps_ep_except(keep); } -/* - * isolate_fds() - Close leaked files, but not --fd, stdin, stdout, stderr - * @argc: Argument count - * @argv: Command line options, as we need to skip any file given via --fd - * - * Should: - * - close all open files except for standard streams and the one from --fd - * - move the --fd descriptor out of the range 0-2 - * - * Return: new fd number for descriptor from --fd, or -1 if not specified +/** + * snapshot_initial_fds() - Snapshot initial file descriptors inherited from parent + * other than standard streams (stdin, stdout, stderr) + * @ifds: Snapshot struct of initial file descriptors to populate */ -int isolate_fds(int argc, char **argv) +void snapshot_initial_fds(struct initial_fd_snapshot *ifds) { - int fd, close_from = STDERR_FILENO + 1; + struct dirent *entry; + size_t capacity = 16; + int max_fd = 1024; + struct rlimit rl; + long parsed_fd; + char *endptr; + int dir_fd; + DIR *dir; + int fd; + + ifds->arr = NULL; + ifds->count = 0; + + dir = opendir("/proc/self/fd"); + if (dir) { + dir_fd = dirfd(dir); + ifds->arr = malloc(sizeof(int) * capacity); + if (!ifds->arr) + die_perror("Failed to allocate memory for inherited fds"); + + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] < '0' || entry->d_name[0] > '9') + continue; + parsed_fd = strtol(entry->d_name, &endptr, 10); + if (*endptr != '\0' || parsed_fd < 0 || parsed_fd > INT_MAX) + continue; + fd = (int)parsed_fd; + /* Ignore stdin/stdout/stderr and the opendir handle itself */ + if (fd > STDERR_FILENO && fd != dir_fd) { + if (ifds->count >= capacity) { + size_t new_cap = capacity * 2; + int *new_arr = realloc(ifds->arr, sizeof(int) * new_cap); + + if (!new_arr) + die_perror("Failed to reallocate memory for inherited fds"); + + ifds->arr = new_arr; + capacity = new_cap; + } + ifds->arr[ifds->count++] = fd; + } + } + closedir(dir); + return; + } + + /* Fallback for environments without /proc (e.g. minimal chroots) */ + if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_cur != RLIM_INFINITY) + max_fd = (int)rl.rlim_cur; - fd = conf_tap_fd(argc, argv); + ifds->arr = malloc(sizeof(int) * max_fd); + if (!ifds->arr) + die_perror("Failed to allocate memory for inherited fds fallback"); - 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++; + for (fd = STDERR_FILENO + 1; fd < max_fd; fd++) { + if (fcntl(fd, F_GETFD) >= 0) + ifds->arr[ifds->count++] = 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"); - } +/** + * isolate_fds() - Close leaked files from the parent process + * @ifds: Snapshot of initial file descriptors + * @keep_fd: File descriptor to keep open, if any + * + * Should: + * - close all file descriptors that were open at startup, except for keep_fd + */ +void isolate_fds(struct initial_fd_snapshot *ifds, int keep_fd) +{ + size_t i; + + if (!ifds || !ifds->arr) + return; + + for (i = 0; i < ifds->count; i++) { + if (ifds->arr[i] != keep_fd) + close(ifds->arr[i]); } - return fd; + free(ifds->arr); + ifds->arr = NULL; + ifds->count = 0; } /** diff --git a/isolation.h b/isolation.h index ec47038..8ada988 100644 --- a/isolation.h +++ b/isolation.h @@ -8,10 +8,17 @@ #define ISOLATION_H #include +#include #include +struct initial_fd_snapshot { + int *arr; + size_t count; +}; + void isolate_initial(void); -int isolate_fds(int argc, char **argv); +void snapshot_initial_fds(struct initial_fd_snapshot *ifds); +void isolate_fds(struct initial_fd_snapshot *ifds, int keep_fd); 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.c b/passt.c index 5054551..c4335bf 100644 --- a/passt.c +++ b/passt.c @@ -335,6 +335,7 @@ int main(int argc, char **argv) struct epoll_event events[NUM_EPOLL_EVENTS]; int nfds, devnull_fd = -1, fd; struct ctx *c = &passt_ctx; + struct initial_fd_snapshot ifds; struct rlimit limit; struct timespec now; struct sigaction sa; @@ -344,8 +345,9 @@ int main(int argc, char **argv) arch_avx2_exec(argv); + snapshot_initial_fds(&ifds); + isolate_initial(); - c->fd_tap = isolate_fds(argc, argv); if ((devnull_fd = open("/dev/null", O_RDWR | O_CLOEXEC)) < 0) die_perror("Failed to open /dev/null"); @@ -390,6 +392,9 @@ int main(int argc, char **argv) sock_probe_features(c); conf(c, argc, argv); + + isolate_fds(&ifds, c->fd_tap); + trace_init(c->trace); pasta_netns_quit_init(c); -- 2.52.0