From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: passt.top; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=goP9eaHE; dkim-atps=neutral Received: from mail-wr1-x42b.google.com (mail-wr1-x42b.google.com [IPv6:2a00:1450:4864:20::42b]) by passt.top (Postfix) with ESMTPS id C348C5A026E for ; Wed, 24 Jun 2026 12:00:06 +0200 (CEST) Received: by mail-wr1-x42b.google.com with SMTP id ffacd0b85a97d-46cdcec58c2so47002f8f.1 for ; Wed, 24 Jun 2026 03:00:06 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782295206; x=1782900006; darn=passt.top; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=oYvdJEq4EXp4f+RLR86LA8JAnP+oLsgJrJ/gvle0Jrg=; b=goP9eaHEjJygQdkb20HS+9gVvgmQHJ1AwNA7JcAJJeZPb0VyVdhbt8QdPvEIWvgGBG 7YWJdtbPl6sPyemzu0k8+WAX2Kh2yG/wrbYjMbBPWiSVP4tQSH1mxsjhVwvUcSGu3WIx rFS3R+PRMvl9swMywMhBhsPmDXoVzVlC/WKqxdKK44I9HyrDzGj2h5KMEPDEbyngCSGJ QVixRG39RBefqFfZWoxmC3V4oiAZBCRPnAQXrhHs4jm8eAcd9oJIro2Wrw/PYV/qO6ky 2cDPROC2MVTI1pssnpBnFLiDeERK99ADNjdnNfDDRv5URQb2D6D2b4QE5dtmFJrFXw9l it6A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782295206; x=1782900006; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=oYvdJEq4EXp4f+RLR86LA8JAnP+oLsgJrJ/gvle0Jrg=; b=s1oysb7YbpuLzsX0/zgHinV/vcKEcyE+WT/kUzoC6ndqwSPs3NOYXWTjBXqWWYwylG 9VTcjthtPysg0gicq5l331ddG7litPNWdkHJ8sbq7bZv8m5ynDWHnKF3a2uAKk+Y0hWG gx4m6HM7j/aUfvB5ns8+ky2gCd7XEdXJhVfigWsMq8v9CE62Sdum/uTEcLQL4UXR9q/A k7IG+u4WuNnh3ijbiyYL65OWPLJTsBaJgbBcOhj2LWZibGAjOBGD8F0zPWD1kOsdUnGZ GsbLaxDz1Ok0X2yco9B3w7YJpKQN4RQsUmBjwAH+GaNSGJ7fl1Zv5wut4OW1dSWVsG3b hZSg== X-Gm-Message-State: AOJu0YxVAQ+cF7B6uot8ulMmjMszLyQb0Ig2qayEUnR1/XZGtUq8O0PY IjnVyZ9oRLggSdwl8VkTl2xB54eRepEVLGsRFSXetL7WacyEjSMZmn4Orw1Xfs36yjwxAA+J X-Gm-Gg: AfdE7cm0llZ2T1egyRT+kHugj4hkzO4oYXwVuaBYKUolTVsr0zH4N4MWzLBaDdDRh6y 6dszm1dZkNLpLYdKDryxqugVQVxjmizcU/VmoxFht2L5FdDJBD9HhKH+ELNhMcu0lb83Lt2h1PP ifilqE4Ou83b0DyAzOUnrmLSoI9d9T83q4lBbNMXBC1LtXv8Yc97RR6L1aeICAAKONXZBqtqvFz j7MayUHKUwq145OxCwcWaSWbsxdo7WWN8yuBWRe7lSAnWIqhjm4Mym/Ftznx80mNptUhCrADovp IWPLVZ4HVqi9GvgedA51LWAfwngCzATWRqML+dNbXlf+ZZkcKCv4vni/9/IL+bjrJ9bFNWduTCt AxpR6JaIN4oRkZMB2zKtSLcCJ7doUtCm9htQv8S2bkr3M9E3tRFJtpTsWV3PKmaZAdWzOX13fsa kmkqnfOn2Dq5tckf82/wMPUh7dG0DAhOQzqqpdSe3IqGXTvAZy711o+K5SKC95rxilUfgZpUB8B ELrakTc86MHfB9ooIkkTg== X-Received: by 2002:a05:6000:2f88:b0:460:2e53:a6f6 with SMTP id ffacd0b85a97d-46a7fc2f584mr12134879f8f.12.1782295206106; Wed, 24 Jun 2026 03:00:06 -0700 (PDT) Received: from k29844791.mikronika.com.pl (91-213-123-13.static.ip.netia.com.pl. [91.213.123.13]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-46caa798f43sm1859879f8f.8.2026.06.24.03.00.05 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 24 Jun 2026 03:00:05 -0700 (PDT) From: Mateusz Andrzejewski X-Google-Original-From: Mateusz Andrzejewski To: passt-dev@passt.top Subject: [PATCH] isolation: Add --use-chroot-fallback option Date: Wed, 24 Jun 2026 11:59:53 +0200 Message-ID: <20260624095953.758482-1-mateusz.andrzejewski@mikronika.com.pl> X-Mailer: git-send-email 2.43.0 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-MailFrom: mandrzejewski06@gmail.com X-Mailman-Rule-Hits: nonmember-moderation X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation Message-ID-Hash: CGO4VGLQ2SHDJFAFOKUG5NGU2DAO7JI7 X-Message-ID-Hash: CGO4VGLQ2SHDJFAFOKUG5NGU2DAO7JI7 X-Mailman-Approved-At: Wed, 24 Jun 2026 12:31:44 +0200 CC: piotr.bzdrega@mikronika.com.pl, mateusz.andrzejewski@mikronika.com.pl 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: For integrations, which use rootfs (tmpfs, initramfs), it is not allowed to use pivot_root(). It results with invalid argument (EINVAL) error. Introduce --use-chroot-fallback option as a workaround with MS_MOVE + chroot(). Due to weaker isolation of chroot() method (we don't umount old root), user has tu explicitly enable fallback with the new option. First, always try to sandbox with pivot_root(). In both cases the new root is placed into an empty tmpfs. For the solution to work we keep CAP_SYS_CHROOT capability, which is dropped at the end of the isolate_prefork() function. Signed-off-by: Mateusz Andrzejewski --- conf.c | 9 ++++++-- isolation.c | 63 ++++++++++++++++++++++++++++++++++++++++++++--------- isolation.h | 2 +- passt.h | 2 ++ 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/conf.c b/conf.c index 4755a9f..fe4d5e9 100644 --- a/conf.c +++ b/conf.c @@ -654,6 +654,7 @@ static void usage(const char *name, FILE *f, int status) " --no-ra Disable router advertisements\n" " --freebind Bind to any address for forwarding\n" " --no-map-gw Don't map gateway address to host\n" + " --use-chroot-fallback Use chroot in case pivot_root fails\n" " -4, --ipv4-only Enable IPv4 operation only\n" " -6, --ipv6-only Enable IPv6 operation only\n" " -t, --tcp-ports SPEC TCP port forwarding to %s\n" @@ -1158,7 +1159,7 @@ static void conf_sock_listen(const struct ctx *c) */ void conf(struct ctx *c, int argc, char **argv) { - int netns_only = 0, no_map_gw = 0; + int netns_only = 0, no_map_gw = 0, use_chroot_fallback = 0; const struct option options[] = { {"debug", no_argument, NULL, 'd' }, {"quiet", no_argument, NULL, 'q' }, @@ -1192,6 +1193,7 @@ void conf(struct ctx *c, int argc, char **argv) {"splice-only", no_argument, &c->splice_only, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, + {"use-chroot-fallback", no_argument, &use_chroot_fallback, 1 }, {"ipv4-only", no_argument, NULL, '4' }, {"ipv6-only", no_argument, NULL, '6' }, {"one-off", no_argument, NULL, '1' }, @@ -1777,6 +1779,8 @@ void conf(struct ctx *c, int argc, char **argv) c->no_dns_search = 1; } + c->use_chroot_fallback = use_chroot_fallback; + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out); @@ -1879,7 +1883,8 @@ void conf(struct ctx *c, int argc, char **argv) conf_open_files(c); /* Before any possible setuid() / setgid() */ - isolate_user(uid, gid, !netns_only, userns, c->mode); + isolate_user(uid, gid, !netns_only, userns, c->mode, + c->use_chroot_fallback); if (c->no_icmp) c->no_ndp = 1; diff --git a/isolation.c b/isolation.c index 7e6225d..4e6fd32 100644 --- a/isolation.c +++ b/isolation.c @@ -166,6 +166,31 @@ static void clamp_caps(void) die_perror("Couldn't drop inheritable capabilities"); } +/** + * move_root() - Use chroot instead of pivot_root for sandboxing. + * + * Return: negative error code on failure, zero on success. + */ +static int move_root(void) +{ + if (mount(TMPDIR, "/", "", MS_MOVE, "")) { + err_perror("Failed to move root into empty tmpfs"); + return -errno; + } + + if (chroot(".")) { + err_perror("Failed to chroot into empty tmpfs"); + return -errno; + } + + if (chdir("/")) { + err_perror("Failed to change directory into new root"); + return -errno; + } + + return 0; +} + /** * isolate_initial() - Early, mostly config independent self isolation * @argc: Argument count @@ -201,8 +226,8 @@ void isolate_initial(int argc, char **argv) * isolate_prefork(). */ keep = BIT(CAP_NET_BIND_SERVICE) | BIT(CAP_SETUID) | BIT(CAP_SETGID) | - BIT(CAP_SYS_ADMIN) | BIT(CAP_NET_ADMIN) | BIT(CAP_DAC_OVERRIDE); - + BIT(CAP_SYS_ADMIN) | BIT(CAP_NET_ADMIN) | BIT(CAP_DAC_OVERRIDE) | + BIT(CAP_SYS_CHROOT); /* Since Linux 5.12, if we want to update /proc/self/uid_map to create * a mapping from UID 0, which only happens with pasta spawning a child * from a non-init user namespace (pasta can't run as root), we need to @@ -225,6 +250,7 @@ void isolate_initial(int argc, char **argv) * @use_userns: Whether to join or create a userns * @userns: userns path to enter, may be empty * @mode: Mode (passt or pasta) + * @use_chroot_fallback: Whether to use chroot() as fallback for sandboxing. * * Should: * - set our final UID and GID @@ -233,7 +259,7 @@ void isolate_initial(int argc, char **argv) * - remove filesystem access (we need that for further setup) */ void isolate_user(uid_t uid, gid_t gid, bool use_userns, const char *userns, - enum passt_modes mode) + enum passt_modes mode, bool use_chroot_fallback) { uint64_t ns_caps = 0; @@ -277,6 +303,13 @@ void isolate_user(uid_t uid, gid_t gid, bool use_userns, const char *userns, * netns */ ns_caps |= BIT(CAP_SYS_ADMIN); + + /* Keep CAP_SYS_CHROOT, so we can fallback to chroot + * in case of use-chroot-fallback option being enabled + */ + if (use_chroot_fallback) + ns_caps |= BIT(CAP_SYS_CHROOT); + if (mode == MODE_PASTA) { /* Keep CAP_NET_ADMIN, so we can configure the if */ ns_caps |= BIT(CAP_NET_ADMIN); @@ -310,6 +343,7 @@ int isolate_prefork(const struct ctx *c) { int flags = CLONE_NEWIPC | CLONE_NEWNS | CLONE_NEWUTS; uint64_t ns_caps = 0; + int result = -1; /* If we run in foreground, we have no chance to actually move to a new * PID namespace. For passt, use CLONE_NEWPID anyway, in case somebody @@ -331,7 +365,7 @@ int isolate_prefork(const struct ctx *c) if (mount("", TMPDIR, "tmpfs", MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_RDONLY, "nr_inodes=2,nr_blocks=0")) { - err_perror("Failed to mount empty tmpfs for pivot_root()"); + err_perror("Failed to mount empty tmpfs for sandboxing"); return -errno; } @@ -341,13 +375,22 @@ int isolate_prefork(const struct ctx *c) } if (syscall(SYS_pivot_root, ".", ".")) { - err_perror("Failed to pivot_root() into empty tmpfs"); - return -errno; + if (c->use_chroot_fallback) { + info("Failed to pivot_root(), fallback to chroot()..."); + result = move_root(); + if (result) + return result; + } + else { + err_perror("Failed to pivot_root() into empty tmpfs"); + return -errno; + } } - - if (umount2(".", MNT_DETACH | UMOUNT_NOFOLLOW)) { - err_perror("Failed to unmount original root filesystem"); - return -errno; + else { + if (umount2(".", MNT_DETACH | UMOUNT_NOFOLLOW)) { + err_perror("Failed to unmount original root filesystem"); + return -errno; + } } /* Now that initialization is more-or-less complete, we can diff --git a/isolation.h b/isolation.h index 0576168..ea8dacb 100644 --- a/isolation.h +++ b/isolation.h @@ -12,7 +12,7 @@ void isolate_initial(int argc, char **argv); void isolate_user(uid_t uid, gid_t gid, bool use_userns, const char *userns, - enum passt_modes mode); + enum passt_modes mode, bool use_chroot_fallback); int isolate_prefork(const struct ctx *c); void isolate_postfork(const struct ctx *c); diff --git a/passt.h b/passt.h index 16506dc..2085cca 100644 --- a/passt.h +++ b/passt.h @@ -214,6 +214,7 @@ struct ip6_ctx { * @splice_only: Only enable loopback forwarding * @host_lo_to_ns_lo: Map host loopback addresses to ns loopback addresses * @freebind: Allow binding of non-local addresses for forwarding + * @use_chroot_fallback: Use chroot in case pivot_root fails * @low_wmem: Low probed net.core.wmem_max * @low_rmem: Low probed net.core.rmem_max * @no_bindtodevice: Unprivileged SO_BINDTODEVICE not available @@ -299,6 +300,7 @@ struct ctx { int splice_only; int host_lo_to_ns_lo; int freebind; + bool use_chroot_fallback; int low_wmem; int low_rmem; -- 2.43.7