]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: startup: support unprivileged chroot if possible
authorMaxime Henrion <mhenrion@haproxy.com>
Tue, 12 May 2026 14:09:11 +0000 (10:09 -0400)
committerWilly Tarreau <w@1wt.eu>
Wed, 20 May 2026 06:34:17 +0000 (08:34 +0200)
Try to use unshare(CLONE_NEWUSER) if available so we can have a chroot
as an unprivileged user. This is a Linux-only mechanism.

src/haproxy.c

index 72c68bb200335b563a22a670879527123f2818ec..ab55d2f129bff7d8ad5277637e6e3a9078526a94 100644 (file)
@@ -3266,6 +3266,67 @@ static void set_identity(const char *program_name)
 #endif
 }
 
+#if defined(CLONE_NEWUSER)
+/* Setup the user namespace after a successful unshare(CLONE_NEWUSER). We do not
+ * return a value because this is best-effort; it is only useful in very rare
+ * situations (see below), and if it fails, we let subsequent setuid() and/or
+ * setgid() calls fail later.
+ */
+static void setup_user_ns(uid_t euid, gid_t egid)
+{
+       char buf[64];
+       int n, ret, fd;
+
+       /* Creating uid_map and gid_map files is required for some specific
+        * situations where we attempt to setuid()/setgid() to the user/group
+        * we are already running as after a successful unshare(CLONE_NEWUSER).
+        * While these directives would effectively be no-ops, we still support
+        * them because it is possible that such setups exist in the wild. For
+        * instance, if haproxy is run through a systemd file containing
+        * "User=someuser" while the configuration file has "user someuser", we
+        * would be in this situation, and a user enabling "chroot auto" in this
+        * case would end up with seemingly unrelated setuid() failures.
+        *
+        * See user_namespaces(7) for more information.
+        */
+       if (global.uid > 0) {
+               n = snprintf(buf, sizeof(buf), "%u %u 1\n", euid, euid);
+               fd = open("/proc/self/uid_map", O_WRONLY);
+               if (fd == -1)
+                       return;
+
+               ret = write(fd, buf, n);
+               close(fd);
+               if (ret != n)
+                       return;
+       }
+
+       if (global.gid > 0) {
+               /* In order to write to the gid_map file, we first need to write
+                * "deny" to the setgroups file. We allow for failure because
+                * older kernels do not support the setgroups file.
+                */
+               fd = open("/proc/self/setgroups", O_WRONLY);
+               if (fd != -1) {
+                       ret = write(fd, "deny", 4);
+                       close(fd);
+                       if (ret != 4)
+                               return;
+               }
+
+               n = snprintf(buf, sizeof(buf), "%u %u 1\n", egid, egid);
+               fd = open("/proc/self/gid_map", O_WRONLY);
+               if (fd == -1)
+                       return;
+
+               ret = write(fd, buf, n);
+               close(fd);
+               if (ret != n)
+                       return;
+       }
+}
+#endif
+
 int main(int argc, char **argv)
 {
        struct rlimit limit;
@@ -3582,6 +3643,20 @@ int main(int argc, char **argv)
                }
        }
 
+#ifdef CLONE_NEWUSER
+       /* When we aren't root and intend to chroot, we try the Linux-only
+        * unshare(CLONE_NEWUSER) mechanism if available to allow chroot as an
+        * unprivileged user. If that doesn't work, we just let the subsequent
+        * chroot() fail as it would have previously.
+        */
+       if (geteuid() != 0 && global.chroot != NULL) {
+               uid_t euid = geteuid();
+               gid_t egid = getegid();
+               if (unshare(CLONE_NEWUSER) == 0)
+                       setup_user_ns(euid, egid);
+       }
+#endif
+
        /* Must chroot and setgid/setuid in the children */
        /* chroot if needed */
        if (global.chroot != NULL) {