From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=none (p=none dis=none) header.from=gibson.dropbear.id.au Authentication-Results: passt.top; dkim=pass (2048-bit key; secure) header.d=gibson.dropbear.id.au header.i=@gibson.dropbear.id.au header.a=rsa-sha256 header.s=202608 header.b=P1vBFwSW; dkim-atps=neutral Received: from mail.ozlabs.org (mail.ozlabs.org [IPv6:2404:9400:2221:ea00::3]) by passt.top (Postfix) with ESMTPS id C69FF5A0265 for ; Mon, 03 Aug 2026 06:44:32 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gibson.dropbear.id.au; s=202608; t=1785732270; bh=wY9BBLeRX/QSCT026wb9jsBzJnSiuaVPtDa7zMVYzc8=; h=Date:From:To:Cc:Subject:References:In-Reply-To:From; b=P1vBFwSWqgyDFE7NMbFvYUf/GezL2Exlm4hJ8FYoMmW4Pxp2BL5licYboeOQ2AjMj qXU6EOyp2zeZ8oW0DW6ziUScTzEu1pWLD3pocFbbIRG2PvzTlrMEhB+5n3BLsinFVy whLYuYs27nV5nebkaq6YoP0viEpc3pm+1TH0ZzWuPhGMVF1D+avW9axF6fg10dbVCp 0o6Z5EFptKPCyuDlhewclqjFUUckK/jFbSNv7RteP+4MNlXP6PacFp0ZVgYboi1Aa4 Lj2qZoaRN8Krv0KMQg44OHtEkoAqNh260UoDrIJS1tUSTCC+7zlwXO1ieh2B4iFQzY qI6PwH8VR/6Gw== Received: by gandalf.ozlabs.org (Postfix, from userid 1007) id 4hD3xB3cRLz4wCB; Mon, 03 Aug 2026 14:44:30 +1000 (AEST) Date: Mon, 3 Aug 2026 14:44:23 +1000 From: David Gibson To: Richard Lawrence Subject: Re: [PATCH v2] feat: Pass open files to child in pasta mode Message-ID: References: <20260731132601.422518-1-rlawrence@tamu.edu> MIME-Version: 1.0 Content-Type: multipart/signed; micalg=pgp-sha512; protocol="application/pgp-signature"; boundary="CLfF75ArKySiSrNI" Content-Disposition: inline In-Reply-To: <20260731132601.422518-1-rlawrence@tamu.edu> Message-ID-Hash: 5VMKWNWR3EJNFSRDQZX56FHIWFIJITTK X-Message-ID-Hash: 5VMKWNWR3EJNFSRDQZX56FHIWFIJITTK X-MailFrom: dgibson@gandalf.ozlabs.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: passt-dev@passt.top 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: --CLfF75ArKySiSrNI Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Content-Transfer-Encoding: quoted-printable On Fri, Jul 31, 2026 at 08:26:01AM -0500, Richard Lawrence wrote: > 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. >=20 > 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()`. >=20 > 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()`. >=20 > 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()`. Thanks for the new version, I like the behaviour of this one much better. However.. maybe I'm missing something, but it seems like it's more complicated than it needs to be. In particular, you've completely replaced the implementation of isolate_fds() - rather than using close_range(), it's enumerating the open fds via /proc and close()ing them individually. Is there a reason to do this? AFAICT now that that isolate_fds() is moved after the spawning of the the pasta shell (or whatever), we're free to close every fd except tap_fd, and the existing implementation should do that just fine. (close_range() is non-portable, of course, but that's true of /proc/self/fd too). Some more minor points noted below. > 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(-) >=20 > 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) > } > =20 > /** > - * 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 li= ne 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) Now that isolate_fds() is after conf(), we shouldn't need to preparse -F any more - we can use the normal parse in conf(). I think that means this can become static. > { > - const struct option optfd[] =3D { { "fd", required_argument, NULL, 'F' = }, > - { 0 }, }; > - const char *fdarg =3D NULL, *p; > unsigned long val; > - int name; > - > - optind =3D 0; > - do { > - name =3D getopt_long(argc, argv, "-:F:", optfd, NULL); > - if (name =3D=3D 'F') > - fdarg =3D optarg; > - } while (name !=3D -1); > + const char *p; > + int new_fd; > + int fd; > =20 > 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); > =20 > - return val; > + fd =3D (int)val; > + if (fd >=3D 0 && fd < 3) { > + new_fd =3D fcntl(fd, F_DUPFD, 3); > + > + if (new_fd < 0) > + die_perror("Could not relocate --fd descriptor"); > + > + close(fd); > + fd =3D new_fd; I think we can delay this relocation to isolate_fds() - in which case, again, the existing implementation should already do what we need. > + } > + > + return fd; > } > =20 > /** > @@ -1625,7 +1630,7 @@ void conf(struct ctx *c, int argc, char **argv) > c->fd_control_listen =3D c->fd_control =3D -1; > break; > case 'F': > - /* --fd was parsed early and c->fd_tap set in main() */ > + c->fd_tap =3D conf_tap_fd(optarg); > c->one_off =3D true; > *c->sock_path =3D 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 > =20 > 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() > * =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D > * > * 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() > - * =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D > - * > - * Executed immediately after isolate_initial(). Closes any leaked > - * files we might have inherited from the parent process. > - * > * 2. isolate_user() > * =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D > * > @@ -44,14 +38,20 @@ > * operate in. Sets our final UID & GID, and enters the correct user > * namespace. > * > - * 3. isolate_prefork() > + * 3. isolate_fds() > + * =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D > + * > + * Executed after conf(). Closes any leaked > + * files we might have inherited from the parent process. > + * > + * 4. isolate_prefork() > * =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D > * > * 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() > * =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D > * > * Executed immediately after daemonizing, but before entering the > @@ -61,6 +61,7 @@ > * runtime operation. > */ > =20 > +#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); > } > =20 > -/* > - * isolate_fds() - Close leaked files, but not --fd, stdin, stdout, stde= rr > - * @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 = =66rom 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 =3D STDERR_FILENO + 1; > + struct dirent *entry; > + size_t capacity =3D 16; > + int max_fd =3D 1024; > + struct rlimit rl; > + long parsed_fd; > + char *endptr; > + int dir_fd; > + DIR *dir; > + int fd; > + > + ifds->arr =3D NULL; > + ifds->count =3D 0; > + > + dir =3D opendir("/proc/self/fd"); > + if (dir) { > + dir_fd =3D dirfd(dir); > + ifds->arr =3D malloc(sizeof(int) * capacity); We don't allocate() memory in passt as a matter of policy. We enforce that with seccomp, although you'll get away with it here because the seccomp filter is only applied in isolate_postfork(). Nonetheless a static array should be used instead. If this function is even needed, which as noted above, I don't actually see a reason for at the moment. > + if (!ifds->arr) > + die_perror("Failed to allocate memory for inherited fds"); > + > + while ((entry =3D readdir(dir)) !=3D NULL) { > + if (entry->d_name[0] < '0' || entry->d_name[0] > '9') > + continue; > + parsed_fd =3D strtol(entry->d_name, &endptr, 10); > + if (*endptr !=3D '\0' || parsed_fd < 0 || parsed_fd > INT_MAX) > + continue; > + fd =3D (int)parsed_fd; > + /* Ignore stdin/stdout/stderr and the opendir handle itself */ > + if (fd > STDERR_FILENO && fd !=3D dir_fd) { > + if (ifds->count >=3D capacity) { > + size_t new_cap =3D capacity * 2; > + int *new_arr =3D realloc(ifds->arr, sizeof(int) * new_cap); > + > + if (!new_arr) > + die_perror("Failed to reallocate memory for inherited fds"); > + > + ifds->arr =3D new_arr; > + capacity =3D new_cap; > + } > + ifds->arr[ifds->count++] =3D fd; > + } > + } > + closedir(dir); > + return; > + } > + > + /* Fallback for environments without /proc (e.g. minimal chroots) */ > + if (getrlimit(RLIMIT_NOFILE, &rl) =3D=3D 0 && rl.rlim_cur !=3D RLIM_INF= INITY) > + max_fd =3D (int)rl.rlim_cur; > =20 > - fd =3D conf_tap_fd(argc, argv); > + ifds->arr =3D malloc(sizeof(int) * max_fd); > + if (!ifds->arr) > + die_perror("Failed to allocate memory for inherited fds fallback"); > =20 > - if (fd >=3D 0) { > - /* Move the passed fd to a more convenient location */ > - if (fd !=3D close_from && > - (dup2(fd, close_from) !=3D close_from || > - close(fd))) > - die_perror("Could not move --fd descriptor"); > - fd =3D close_from++; > + for (fd =3D STDERR_FILENO + 1; fd < max_fd; fd++) { I don't think the RLIMIT_NOFILE guarantees that all fds will be less than it. They generally will be, but I don't think anything will stop you using dup2() to move a single fd to some very high fd number. > + if (fcntl(fd, F_GETFD) >=3D 0) > + ifds->arr[ifds->count++] =3D fd; > } > +} > =20 > - if (close_range(close_from, ~0U, CLOSE_RANGE_UNSHARE)) { > - if (errno =3D=3D ENOSYS || errno =3D=3D 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 k= eep_fd > + */ > +void isolate_fds(struct initial_fd_snapshot *ifds, int keep_fd) The rewritten isolate_fds() no longer ensures that fds 0, 1, 2 are populated (with /dev/null, if necessary). That's not strictly necessary, but I did implement it on purpose, because I think it avoids confusion. > +{ > + size_t i; > + > + if (!ifds || !ifds->arr) > + return; > + > + for (i =3D 0; i < ifds->count; i++) { > + if (ifds->arr[i] !=3D keep_fd) > + close(ifds->arr[i]); > } > =20 > - return fd; > + free(ifds->arr); > + ifds->arr =3D NULL; > + ifds->count =3D 0; > } > =20 > /** > 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 > =20 > #include > +#include > #include > =20 > +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_us= erns, > 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 =3D -1, fd; > struct ctx *c =3D &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) > =20 > arch_avx2_exec(argv); > =20 > + snapshot_initial_fds(&ifds); > + > isolate_initial(); > - c->fd_tap =3D isolate_fds(argc, argv); > =20 > if ((devnull_fd =3D 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); > =20 > conf(c, argc, argv); > + > + isolate_fds(&ifds, c->fd_tap); > +=09 > trace_init(c->trace); > =20 > pasta_netns_quit_init(c); > --=20 > 2.52.0 >=20 --=20 David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson --CLfF75ArKySiSrNI Content-Type: application/pgp-signature; name=signature.asc -----BEGIN PGP SIGNATURE----- iQIzBAEBCgAdFiEEO+dNsU4E3yXUXRK2zQJF27ox2GcFAmpwHJsACgkQzQJF27ox 2Gc4WA/8Cg35jP0Pv46qJbSIUD84dH8+5cz6deNEBly26YwpwTW3SQ2qaa0iEH25 AApfa8NQBfJ/HLYpXH1XSLkPQndsaF4T19/sImZopbDNMPl5e+kseYYR70fdWiFs zUbJ2V7L75HjJb2V86hegGM3LpQBppAdRnI49P1TIAj9uYx8iADs4HX9jlGO7sZ6 Ly+fNNWMmSFSm5F3EW21b1vMZip60xTwV7A2GjX2lZ2GFjyu4vezZPl9SdFsklpJ MPdA7RpCXW4d4r+R6+1msV4EgFEO6k1C+kAumsI0twgZHEa3vxBkxlmayskCwXx2 p4UKEWqYwYFkKW9pDDzQiqhmmpaGkH/1HlrR+Q4ehP9rqT4+lC7ffKLffojsUXup Hh2cs3MaSDPtRrh6mCd/7sQftnmJjbI8ERapT6upnUtV7/s8zKtX4uMoxW7X5i00 vvyWVpygpq7GDnwWq3IXYriCMszc+EU1G/Sn4aRGruqTTMn2EzkZ2ul0hVUXBdGp 09fxtry5W87JE1SHrW9ngYVUGdJ9/Ne2gubYWnGreIz6QioWfyg/PO6nOfm0m7xC GXQfq2/8fSw79FxP2wER+fouCSZHewlSpMXPP60IOPVywSARRpI7/fTziZ7t6jCS dR/6Q0lRQ1sPnEfvu1ucqSS/wb1kcIuAeWFFuor03u/W9y+IspA= =45HC -----END PGP SIGNATURE----- --CLfF75ArKySiSrNI--