Message ID | 87v7s6h21b.fsf@gnu.org |
---|---|
State | New |
Headers |
Return-Path: <guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org> X-Original-To: patchwork@mira.cbaines.net Delivered-To: patchwork@mira.cbaines.net Received: by mira.cbaines.net (Postfix, from userid 113) id 7F61B27BBE2; Tue, 18 Mar 2025 14:02:10 +0000 (GMT) X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on mira.cbaines.net X-Spam-Level: X-Spam-Status: No, score=-7.6 required=5.0 tests=BAYES_00,DKIMWL_WL_HIGH, DKIM_SIGNED,DKIM_VALID,MAILING_LIST_MULTI,RCVD_IN_DNSWL_BLOCKED, RCVD_IN_VALIDITY_CERTIFIED,RCVD_IN_VALIDITY_RPBL,RCVD_IN_VALIDITY_SAFE, SPF_HELO_PASS,URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.6 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mira.cbaines.net (Postfix) with ESMTPS id AFF3B27BBE9 for <patchwork@mira.cbaines.net>; Tue, 18 Mar 2025 14:02:06 +0000 (GMT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from <guix-patches-bounces@gnu.org>) id 1tuXVC-0006vq-DP; Tue, 18 Mar 2025 10:01:06 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from <Debian-debbugs@debbugs.gnu.org>) id 1tuXVA-0006vV-Ma for guix-patches@gnu.org; Tue, 18 Mar 2025 10:01:04 -0400 Received: from debbugs.gnu.org ([2001:470:142:5::43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from <Debian-debbugs@debbugs.gnu.org>) id 1tuXVA-0001ny-6J for guix-patches@gnu.org; Tue, 18 Mar 2025 10:01:04 -0400 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=debbugs.gnu.org; s=debbugs-gnu-org; h=MIME-Version:Date:References:In-Reply-To:From:To:Subject; bh=xBjgdla+kQknEBqI+xyUQw+JCaa5THMk8Wk84l/80ac=; b=p56yV8oYsMBDDcYV2A6m6OyLwWBwDgFHWul7o9Uwii5AwzpkHJFxyCIfXIr7CWIv4YMpMlqh95mGmOcCuKp+93yk96Cw4Gn/bgY4OJQzuHK+27+yX3pysThUlw4Zbs+qxK23J4oa1+kcvu2DQUrXVsJgAwzhgvwi7RzvWU1Iu+9EuFOYJ0qXgm84slsh9gK8GsseOrVZ/qrzx/36FvedPtnblPBQpHsoykJEs7vZBRuFM4rgJhoDGnK38rVqDflXJPhq7JVDQnpVCfroO3oPVBZqtj3a4xwUrplJwwyVP+j7voTTprANwmgwp2FRWyYkHVfVegPjK8v62D8ZzaGX6g==; Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from <Debian-debbugs@debbugs.gnu.org>) id 1tuXV8-0000sx-Vq for guix-patches@gnu.org; Tue, 18 Mar 2025 10:01:03 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#75810] [PATCH v6 00/16] Rootless guix-daemon Resent-From: Ludovic =?utf-8?q?Court=C3=A8s?= <ludo@gnu.org> Original-Sender: "Debbugs-submit" <debbugs-submit-bounces@debbugs.gnu.org> Resent-CC: guix-patches@gnu.org Resent-Date: Tue, 18 Mar 2025 14:01:02 +0000 Resent-Message-ID: <handler.75810.B75810.17423064383257@debbugs.gnu.org> Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 75810 X-GNU-PR-Package: guix-patches X-GNU-PR-Keywords: patch To: Reepca Russelstein <reepca@russelstein.xyz> Cc: 75810@debbugs.gnu.org Received: via spool by 75810-submit@debbugs.gnu.org id=B75810.17423064383257 (code B ref 75810); Tue, 18 Mar 2025 14:01:02 +0000 Received: (at 75810) by debbugs.gnu.org; 18 Mar 2025 14:00:38 +0000 Received: from localhost ([127.0.0.1]:41297 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from <debbugs-submit-bounces@debbugs.gnu.org>) id 1tuXUj-0000qT-7d for submit@debbugs.gnu.org; Tue, 18 Mar 2025 10:00:38 -0400 Received: from hera.aquilenet.fr ([185.233.100.1]:43422) by debbugs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.84_2) (envelope-from <ludo@gnu.org>) id 1tuXUZ-0000p9-Gy for 75810@debbugs.gnu.org; Tue, 18 Mar 2025 10:00:34 -0400 Received: from localhost (localhost [127.0.0.1]) by hera.aquilenet.fr (Postfix) with ESMTP id BCC793D7; Tue, 18 Mar 2025 15:00:19 +0100 (CET) Authentication-Results: hera.aquilenet.fr; none X-Virus-Scanned: Debian amavis at hera.aquilenet.fr Received: from hera.aquilenet.fr ([127.0.0.1]) by localhost (hera.aquilenet.fr [127.0.0.1]) (amavis, port 10024) with ESMTP id b3XMQXplc12u; Tue, 18 Mar 2025 15:00:18 +0100 (CET) Received: from ribbon (unknown [193.50.110.142]) by hera.aquilenet.fr (Postfix) with ESMTPSA id EFC5930A; Tue, 18 Mar 2025 15:00:17 +0100 (CET) From: Ludovic =?utf-8?q?Court=C3=A8s?= <ludo@gnu.org> In-Reply-To: <875xk7594u.fsf@russelstein.xyz> (Reepca Russelstein's message of "Mon, 17 Mar 2025 22:07:45 -0500") References: <cover.1742230219.git.ludo@gnu.org> <875xk7594u.fsf@russelstein.xyz> Date: Tue, 18 Mar 2025 15:00:16 +0100 Message-ID: <87v7s6h21b.fsf@gnu.org> User-Agent: Gnus/5.13 (Gnus v5.13) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Rspamd-Queue-Id: BCC793D7 X-Spamd-Result: default: False [4.90 / 15.00]; SPAM_FLAG(5.00)[]; BAYES_HAM(-3.00)[100.00%]; NEURAL_SPAM(3.00)[1.000]; MIME_GOOD(-0.10)[multipart/mixed,text/plain,text/x-patch]; RCVD_COUNT_TWO(0.00)[2]; FROM_EQ_ENVFROM(0.00)[]; MIME_TRACE(0.00)[0:+,1:+,2:+,3:+]; RCPT_COUNT_TWO(0.00)[2]; ARC_NA(0.00)[]; TO_MATCH_ENVRCPT_ALL(0.00)[]; RCVD_VIA_SMTP_AUTH(0.00)[]; RCVD_TLS_ALL(0.00)[]; FROM_HAS_DN(0.00)[]; TO_DN_SOME(0.00)[]; MID_RHS_MATCH_FROM(0.00)[] X-Rspamd-Action: no action X-Spamd-Bar: ++++ X-Rspamd-Server: hera X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: guix-patches@gnu.org List-Id: <guix-patches.gnu.org> List-Unsubscribe: <https://lists.gnu.org/mailman/options/guix-patches>, <mailto:guix-patches-request@gnu.org?subject=unsubscribe> List-Archive: <https://lists.gnu.org/archive/html/guix-patches> List-Post: <mailto:guix-patches@gnu.org> List-Help: <mailto:guix-patches-request@gnu.org?subject=help> List-Subscribe: <https://lists.gnu.org/mailman/listinfo/guix-patches>, <mailto:guix-patches-request@gnu.org?subject=subscribe> Errors-To: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org Sender: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org X-getmail-retrieved-from-mailbox: Patches |
Commit Message
Ludovic Courtès
March 18, 2025, 2 p.m. UTC
Hi! Reepca Russelstein <reepca@russelstein.xyz> skribis: > (in patch 7/16, in nix/libstore/build.cc) > > Strictly speaking we should check whether the fds are >= 0, not > 0, > since 0 is technically a valid file descriptor, and we use -1 to > indicate the absence of a file descriptor. > > Also, readiness.readSide isn't explicitly closed in the child after > we're done with it. Right, fixed. >> + # The unprivileged daemon cannot create the log directory by itself. >> + mkdir /var/log/guix >> + chown guix-daemon:guix-daemon /var/log/guix >> + chmod 755 /var/log/guix > > (in patch 15/16, in etc/guix-install.sh) > > Should this be 'mkdir -p' or some other conditional creation? If I > understand correctly this will fail when overwriting an existing install > using GUIX_ALLOW_OVERWRITE. Correct, fixed as well. (I’ve updated the branch on Codeberg.) > Concerning guix-install.sh, to be clear, is the intent to specifically > not support installing the rootful daemon on systemd systems? > > For my two cents, I do think that it's still a tradeoff - not just > because of the reliance on different kernel mechanisms for security, but > also because the rootless daemon currently causes visible changes to the > build environment (EROFS on /, and nothing owned by root, for example). > Which one should we consider the "canonical" build environment going > forward? The way I see it, we would gradually move to the non-root daemon: • First step here is to enable it by default on systemd distros. • Second step would be to allow Guix System users to migrate to non-root daemon, keeping the default unchanged. • Third step (a year later maybe?) would be to default to non-root daemon on Guix System and on remaining distros (though for these it might be trickier because we probably cannot rely on CAP_SYS_CHOWN, not as easily as with systemd at least). The visible changes in the build environment are unfortunate; I’m hoping they won’t have any practical impact, not any more than the other parameters that may change currently (build UID, binfmt_misc, file system, etc.) We could test this hypothesis by rebuilding at least the entire set of packages up to ‘hello’. (I tried doing it just now in a Debian VM but failed since the main partition cannot easily be extended; it’ll be easier to do with Guix System.) > The demo, modified for guix circumstances, would go something like this: > > 1. A derivation is created whose builder is /proc/self/exe, and whose > LD_PRELOAD environment variable points into a malicious store item > for one of its shared libraries - for example, libc. > 2. The daemon reads this in, and, to my knowledge, does no verification > of the builder string. Note that this aspect isn't actually > necessary, as the builder could also be a symlink to /proc/self/exe > from the store. > 3. The daemon sets up the build environment, and execs /proc/self/exe. Then there are 3 possibilities: 1. If /proc/self/exe points to (say) /usr/bin/guix-daemon, outside the store, execve fails with ENOENT because that file is not mounted in the chroot. 2. If /proc/self/exe points to a guix-daemon file inside the store: 2a. If the store item containing guix-daemon is not an input of the derivation, execv fails with ENOENT. 2b. If the store item containing guix-daemon is an input of the derivation, then it’s been remounted read-only and attempts to write to it fail with EROFS. Here’s a test that fails both “rootfull” and “rootless”: --8<---------------cut here---------------start------------->8--- (let* ((builder (add-file-tree-to-store %store `("builder" symlink "/proc/self/exe"))) (drv (derivation %store "attempt-to-run-guix-daemon" builder '() #:env-vars '(("LD_PRELOAD" . "attacker-controlled.so"))))) (guard (c ((store-protocol-error? c) c)) (build-derivations %store (list drv)) #f)) --8<---------------cut here---------------end--------------->8--- > There are several points at which that particular attack could be > stopped, and I'd like to try to stop it at as many of them as possible. > A good start would be canonicalizing the builder prior to executing it > and then checking to make sure it is in the store. A more general > solution could look like writing out and then executing a tiny binary, > something like /tmp/runbuilder, that does nothing but unlink itself and > then exec the actual program. If the above is correct, we’re already safe against this particular attack. Canonicalizing the builder cannot hurt (it’s useful in the ‘--disable-chroot’ case though mostly to prevent programming errors rather than from a security perspective since there are many other issues in that case), apart from adding more code to an already long function. For reference, the extra check I tried but that I’m inclined to not include since it cannot catch anything new: Let me know what you think and I’ll send v7 accordingly. Ludo’.
Comments
Ludovic Courtès <ludo@gnu.org> writes: >> For my two cents, I do think that it's still a tradeoff - not just >> because of the reliance on different kernel mechanisms for security, but >> also because the rootless daemon currently causes visible changes to the >> build environment (EROFS on /, and nothing owned by root, for example). >> Which one should we consider the "canonical" build environment going >> forward? > > The way I see it, we would gradually move to the non-root daemon: > > • First step here is to enable it by default on systemd distros. > > • Second step would be to allow Guix System users to migrate to > non-root daemon, keeping the default unchanged. > > • Third step (a year later maybe?) would be to default to non-root > daemon on Guix System and on remaining distros (though for these it > might be trickier because we probably cannot rely on CAP_SYS_CHOWN, > not as easily as with systemd at least). > > The visible changes in the build environment are unfortunate; I’m hoping > they won’t have any practical impact, not any more than the other > parameters that may change currently (build UID, binfmt_misc, file > system, etc.) We could test this hypothesis by rebuilding at least the > entire set of packages up to ‘hello’. (I tried doing it just now in a > Debian VM but failed since the main partition cannot easily be extended; > it’ll be easier to do with Guix System.) > For what it's worth, the visible changes could be avoided with subordinate ids, as I wrote in https://debbugs.gnu.org/cgi/bugreport.cgi?bug=75810#86. We could try it as-is and see how it goes, and if there are problems with reproducibility add on using subordinate ids. I would expect it to be a much smaller change than the root->rootless transition. Hopefully it works well enough as-is that offload systems could be set up without any special permissions. > Then there are 3 possibilities: > > 1. If /proc/self/exe points to (say) /usr/bin/guix-daemon, outside the > store, execve fails with ENOENT because that file is not mounted in > the chroot. No, like I wrote, /proc/self/exe, despite being reported as a symlink by stat, does not follow the usual symlink semantics. This is much like how the files in /proc/self/fd work, e.g.: scheme@(guile-user)> (open-file "/tmp/freshfile" "w+") $1 = #<input-output: /tmp/freshfile 13> scheme@(guile-user)> (delete-file "/tmp/freshfile") scheme@(guile-user)> (stat:type (lstat "/proc/self/fd/13")) $3 = symlink scheme@(guile-user)> (readlink "/proc/self/fd/13") $4 = "/tmp/freshfile (deleted)" scheme@(guile-user)> (open-file "/proc/self/fd/13" "w+") $5 = #<input-output: /proc/self/fd/13 14> Here is a test program to demonstrate this (it's unfortunately rather tricky to demonstrate using usual command line tools): --8<---------------cut here---------------start------------->8--- /* Test program to demonstrate that /proc/self/exe does not behave like a symlink, and a process can exec /proc/self/exe even if there is no other filename by which the currently-executing program can be reached. Compile with -static (may require a 'guix shell gcc-toolchain glibc:static') and run. */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <unistd.h> #include <sched.h> #include <sys/param.h> #include <sys/mount.h> #include <sys/syscall.h> #include <sys/wait.h> #include <sys/stat.h> #include <errno.h> #include <signal.h> #define die(msg, status) \ do \ { \ perror(msg); \ exit(status); \ } while(0); \ #define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root,put_old)) int child_main(void * arg) { int *pipefds = (int *) arg; char *argv[2] = {"???", NULL}; int saved_errno; char c; if(close(pipefds[1])) die("Child closing write end of pipe", 20); if(read(pipefds[0], &c, 1 * sizeof(char)) < (1 * sizeof(char))) die("Reading from pipe", 21); if(c != 'y') die("Parent setup failed", 22); if(close(pipefds[0])) die("Child closing read end of pipe", 23); if(mkdir("/tmp/test-chroot", 0700) && errno != EEXIST) die("mkdir(\"/tmp/test-chroot\")", 24); if(mount(0, "/", 0, MS_REC|MS_PRIVATE, 0)) die("making / private", 25); if(mount("/tmp/test-chroot", "/tmp/test-chroot", 0, MS_BIND, 0)) die("making /tmp/test-chroot its own filesystem", 26); if(mkdir("/tmp/test-chroot/proc", 0700) && errno != EEXIST) die("mkdir(\"/tmp/test-chroot/proc\")", 27); if(mount("none", "/tmp/test-chroot/proc", "proc", 0, 0)) die("mount procfs", 28); if(chdir("/tmp/test-chroot")) die("chdir to /tmp/test-chroot", 29); if(mkdir("real-root", 0)) die("mkdir(\"real-root\")", 30); if(pivot_root(".", "real-root")) die("pivot_root", 31); if(chroot(".")) die("chroot", 32); if(umount2("real-root", MNT_DETACH)) die("unmounting real root", 33); if(rmdir("real-root")) die("removing real root directory", 34); if(execve("/proc/self/exe", argv, environ)) { saved_errno = errno; fprintf(stderr, "execve errno: %d\n", saved_errno); errno = saved_errno; die("execve", 35); } } int main(int argc, char **argv) { pid_t child_pid = -1; char stack[32 * 1024]; int flags; int result; int status; FILE * f; char strbuf[512]; int j; int pipefds[2]; if(getenv("TEST_PROGRAM_MAGIC_ENV_VAR")) { fprintf(stderr, "Self-exec'ed!\n"); fprintf(stderr, "Arguments: \n"); for(j = 0; j < argc; j++) fprintf(stderr, "%s\n", argv[j]); j = readlink("/proc/self/exe", strbuf, sizeof(strbuf) - 1); if(j >= 0) { strbuf[j] = '\0'; fprintf(stderr, "/proc/self/exe readlink result: %s\n", strbuf); f = fopen(strbuf, "r"); if(f) { fprintf(stderr, "... and that file exists!\n"); fclose(f); } else { perror("fopen /proc/self/exe readlink result error"); fprintf(stderr, "... and that file does not exist.\n"); } f = fopen("/proc/self/exe", "r"); if(f) { fprintf(stderr, "/proc/self/exe can be opened\n"); fclose(f); } else { perror("fopen /proc/self/exe error"); fprintf(stderr, "/proc/self/exe cannot be opened\n"); } } return 0; } setenv("TEST_PROGRAM_MAGIC_ENV_VAR", "1", 1); if(pipe(pipefds)) die("pipe()", 1); flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWUSER; flags = flags | SIGCHLD; child_pid = clone(child_main, (char *)(((uintptr_t)stack + sizeof(stack) - 8) & ~((uintptr_t)0xf)), flags, (void *) pipefds); if(child_pid == -1) die("clone()", 1); if(close(pipefds[0])) die("parent closing read pipe end", 2); if(snprintf(strbuf, sizeof(strbuf), "/proc/%d/uid_map", child_pid) >= sizeof(strbuf)) die("uid_map snprintf", 3); f = fopen(strbuf, "w"); if(!f) die("uid_map fopen", 4); if(snprintf(strbuf, sizeof(strbuf), "%u %u 1", geteuid(), geteuid()) >= sizeof(strbuf)) die("uid_map contents snprintf", 5); if(fwrite(strbuf, strlen(strbuf) * sizeof(char), 1, f) < 1) die("uid_map fwrite", 6); if(fclose(f) == EOF) die("uid_map fclose", 7); if(snprintf(strbuf, sizeof(strbuf), "/proc/%d/setgroups", child_pid) >= sizeof(strbuf)) die("setgroups snprintf", 8); f = fopen(strbuf, "w"); if(!f) die("setgroups fopen", 9); if(fwrite("deny", 4 * sizeof(char), 1, f) < 1) die("setgroups fwrite", 10); if(fclose(f) == EOF) die("setgroups fclose", 11); if(snprintf(strbuf, sizeof(strbuf), "/proc/%d/gid_map", child_pid) >= sizeof(strbuf)) die("gid_map snprintf", 12); f = fopen(strbuf, "w"); if(!f) die("gid_map fopen", 13); if(snprintf(strbuf, sizeof(strbuf), "%u %u 1", getegid(), getegid()) >= sizeof(strbuf)) die("gid_map contents snprintf", 14); if(fwrite(strbuf, strlen(strbuf) * sizeof(char), 1, f) < 1) die("gid_map fwrite", 15); if(fclose(f) == EOF) die("gid_map fclose", 16); if(write(pipefds[1], "y", sizeof("y") - (1 * sizeof(char))) < (1 * sizeof(char))) die("writing to pipe", 17); if(close(pipefds[1])) die("parent closing write pipe end", 18) while (1) { result = waitpid(child_pid, &status, 0); if(result == child_pid) return WEXITSTATUS(status); if(result == -1 && errno != EINTR) die("waitpid", 19); } } --8<---------------cut here---------------end--------------->8--- Here's what it looks like when I run it: --8<---------------cut here---------------start------------->8--- $ /tmp/test-program Self-exec'ed! Arguments: ??? /proc/self/exe readlink result: /tmp/test-program fopen /proc/self/exe readlink result error: No such file or directory ... and that file does not exist. /proc/self/exe can be opened --8<---------------cut here---------------end--------------->8--- > Here’s a test that fails both “rootfull” and “rootless”: > > (let* ((builder (add-file-tree-to-store %store > `("builder" symlink "/proc/self/exe"))) > (drv (derivation %store "attempt-to-run-guix-daemon" builder '() > #:env-vars > '(("LD_PRELOAD" . "attacker-controlled.so"))))) > (guard (c ((store-protocol-error? c) c)) > (build-derivations %store (list drv)) > #f)) From 'man 2 execve': ENOENT The file pathname or a script *or ELF interpreter* does not exist. (emphasis mine). The dynamic linker registered in guix-daemon's binary is not likely to exist in the container in this test, but an attacker could easily make it so as long as it's in the store. > diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc > index 7484ea8fcf..970107c265 100644 > --- a/nix/libstore/build.cc > +++ b/nix/libstore/build.cc > @@ -2374,6 +2374,15 @@ void DerivationGoal::runChild() > writeFull(STDERR_FILENO, "error: " + string(e.what()) + "\n"); > _exit(1); > } > + } else { > + /* Ensure that the builder is within the store. This prevents > + users from using /proc/self/exe (or a symlink to it) as their > + builder, which could allow them to overwrite the guix-daemon > + binary (CVE-2019-5736). */ > + drv.builder = canonPath(drv.builder, true); > + printMsg(lvlError, format("builder is `%1%'") % drv.builder); > + if (!isInStore(drv.builder)) > + throw Error(format("derivation builder '%1%' is outside the store") % drv.builder); > } > > /* Fill in the arguments. */ Note that we should still supply the original name or basename as argv[0]. While ensuring that what actually gets execve'd is in the store suffices to eliminate the vulnerability, it may be "conceptually purer" to require that the links pointing to it are all in the store as well. For example, while a builder that is a symlink pointing to /proc/self/exe wouldn't be able to modify the daemon binary, it's still a piece of basically "undefined behavior" as far as the build environment is concerned, which could be closed up. But that can come later just as well. One more consideration I noticed when looking at v6's patch 14/16 (the guix-daemon.service one): we don't do anything to set the gid. I know on guix system we usually use both dedicated privilege separation users and groups for services. Should we use a dedicated group for guix-daemon as well? Note that currently the chroot directories have 0750 permissions, so it's very important that their group not be accessible to others. - reepca
Hi, Reepca Russelstein <reepca@russelstein.xyz> skribis: >> The visible changes in the build environment are unfortunate; I’m hoping >> they won’t have any practical impact, not any more than the other >> parameters that may change currently (build UID, binfmt_misc, file >> system, etc.) We could test this hypothesis by rebuilding at least the >> entire set of packages up to ‘hello’. (I tried doing it just now in a >> Debian VM but failed since the main partition cannot easily be extended; >> it’ll be easier to do with Guix System.) >> > > For what it's worth, the visible changes could be avoided with > subordinate ids, as I wrote in > https://debbugs.gnu.org/cgi/bugreport.cgi?bug=75810#86. Yes. The newuidmap/newgidmap mechanism looks like a hack to me so I’m reluctant, but if we have to, we could take that route. > We could try it as-is and see how it goes, and if there are problems > with reproducibility add on using subordinate ids. I would expect it to > be a much smaller change than the root->rootless transition. Yes. >> 1. If /proc/self/exe points to (say) /usr/bin/guix-daemon, outside the >> store, execve fails with ENOENT because that file is not mounted in >> the chroot. > > No, like I wrote, /proc/self/exe, despite being reported as a symlink by > stat, does not follow the usual symlink semantics. This is much like > how the files in /proc/self/fd work, e.g.: Oh right; apologies, and thanks for taking the time to (re)explain and to come up with a reproducer. > From 'man 2 execve': > > ENOENT The file pathname or a script *or ELF interpreter* does not exist. > > (emphasis mine). The dynamic linker registered in guix-daemon's binary > is not likely to exist in the container in this test, but an attacker > could easily make it so as long as it's in the store. Yes. And it wouldn’t be so hard when one is running guix-daemon from the store (from the ‘guix’ package or from ‘guix pull’). >> + drv.builder = canonPath(drv.builder, true); >> + printMsg(lvlError, format("builder is `%1%'") % drv.builder); >> + if (!isInStore(drv.builder)) >> + throw Error(format("derivation builder '%1%' is outside the store") % drv.builder); >> } >> >> /* Fill in the arguments. */ > > Note that we should still supply the original name or basename as > argv[0]. Right, noted! > While ensuring that what actually gets execve'd is in the store suffices > to eliminate the vulnerability, it may be "conceptually purer" to > require that the links pointing to it are all in the store as well. For > example, while a builder that is a symlink pointing to /proc/self/exe > wouldn't be able to modify the daemon binary, it's still a piece of > basically "undefined behavior" as far as the build environment is > concerned, which could be closed up. But that can come later just as > well. Yes. But in practice, “normal” symlinks (i.e., not /proc/self/exe) will lead ‘canonPath’ to throw if one component is outside the store, since ‘canonPath’ operates within the chroot. > One more consideration I noticed when looking at v6's patch 14/16 (the > guix-daemon.service one): we don't do anything to set the gid. I know > on guix system we usually use both dedicated privilege separation users > and groups for services. Should we use a dedicated group for > guix-daemon as well? Note that currently the chroot directories have > 0750 permissions, so it's very important that their group not be > accessible to others. Quoth <https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#User=>: User=, Group= Set the UNIX user or group that the processes are executed as, respectively. […] If no group is set, the default group of the user is used. Since we don’t have ‘Group=’ in the .service file, the daemon runs as the group of ‘guix-daemon’, also called ‘guix-daemon’ (created by ‘guix-install.sh’). I confirmed in a VM that the process is indeed running as guix-daemon:guix-daemon. I’ve just sent v7 with the ‘canonPath’ change discussed above. Thank you! Ludo’.
Ludovic Courtès <ludo@gnu.org> writes: >> While ensuring that what actually gets execve'd is in the store suffices >> to eliminate the vulnerability, it may be "conceptually purer" to >> require that the links pointing to it are all in the store as well. For >> example, while a builder that is a symlink pointing to /proc/self/exe >> wouldn't be able to modify the daemon binary, it's still a piece of >> basically "undefined behavior" as far as the build environment is >> concerned, which could be closed up. But that can come later just as >> well. > > Yes. But in practice, “normal” symlinks (i.e., not /proc/self/exe) will > lead ‘canonPath’ to throw if one component is outside the store, since > ‘canonPath’ operates within the chroot. Unless the component actually exists and is outside of the store. If we just rely on canonPath throwing an exception to be safe, then if there ever arose a situation where a non-symlink executable existed outside of the store, it would still be possible to convince the daemon to execute it. For example, suppose Linus Torvalds wakes up one day and decides "you know what, it really is wrong that we're lying about /proc/self/exe being a symlink" and changes it so that lstat reports a regular file (or a special file, really anything other than a symlink) that happens to be a valid argument to readlink. Or suppose in the interest of backwards-compatibility he instead adds /proc/self/exe-hardlink. canonPath won't throw if a symlink points to these hypothetical files, and the daemon would execute them. A similar situation could also happen if an executable showed up in /dev somehow. I mention this because I see that patch 07/16 of v7 has left out the isInStore check, and I think it should remain. > Since we don’t have ‘Group=’ in the .service file, the daemon runs as > the group of ‘guix-daemon’, also called ‘guix-daemon’ (created by > ‘guix-install.sh’). > > I confirmed in a VM that the process is indeed running as > guix-daemon:guix-daemon. Great. While researching container escape vulnerabilities, I recently came across CAP_DAC_READ_SEARCH and open_by_handle_at, which is a system call so insanely powerful it is outright banned in all but the root user namespace. Or at least, it was. 10 months ago, in commit 620c266f394932e5decc4b34683a75dfc59dc2f4 of https://github.com/torvalds/linux, the requirements were relaxed so that, in certain cases, processes in non-root user namespaces could use open_by_handle_at. The consequences of this for same-user containers are not clear to me yet, as I haven't studied the kernel source enough to know what exactly that commit message means by "privileges over the filesystem" or "privileges over a subtree". I also haven't been able to test this behavior yet, because my kernel is actually too old (I do my rebases and upgrades rather less regularly than is recommended). I'll try to look into this more once I update my system (and man-pages!), but figured I should mention it, because aside from that, and the aforementioned isInStore check, I can't think of any remaining concerns. - reepca
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc index 7484ea8fcf..970107c265 100644 --- a/nix/libstore/build.cc +++ b/nix/libstore/build.cc @@ -2374,6 +2374,15 @@ void DerivationGoal::runChild() writeFull(STDERR_FILENO, "error: " + string(e.what()) + "\n"); _exit(1); } + } else { + /* Ensure that the builder is within the store. This prevents + users from using /proc/self/exe (or a symlink to it) as their + builder, which could allow them to overwrite the guix-daemon + binary (CVE-2019-5736). */ + drv.builder = canonPath(drv.builder, true); + printMsg(lvlError, format("builder is `%1%'") % drv.builder); + if (!isInStore(drv.builder)) + throw Error(format("derivation builder '%1%' is outside the store") % drv.builder); } /* Fill in the arguments. */