From 553ae61ffd69e43845f227f3b3108536b52a31f2 Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Fri, 17 Jul 2026 15:01:36 +0200 Subject: [PATCH] libstore: fix AddBuildLog denial of service adding logs to a daemon store can fail before the entire request has been read, leaving non-command data in socket buffers. if the daemon does not terminate immediately after the failure it will try to read the remaining data as commands, which is very likely to not work and cause unbounded memory allocation instead. this doesn't give clients an attack vector they did not have before though, they could've just as well sent malformed commands without a bad preceding AddBuildLog. Change-Id: I86c22890eace19164d932cbd342ac9f52cee6531 --- lix/libstore/daemon.cc | 5 +-- tests/functional2/store/test_logs.py | 47 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/functional2/store/test_logs.py diff --git a/lix/libstore/daemon.cc b/lix/libstore/daemon.cc index 5a1163f03..6de6690d4 100644 --- a/lix/libstore/daemon.cc +++ b/lix/libstore/daemon.cc @@ -845,11 +845,12 @@ static void performOp(AsyncIoRoot & aio, TunnelLogger * logger, ref store case WorkerProto::Op::AddBuildLog: { StorePath path{readString(from)}; logger->startWork(); - if (!trusted) - throw Error("you are not privileged to add logs"); auto & logStore = require(*store); { FramedSource source(from); + if (!trusted) { + throw Error("you are not privileged to add logs"); + } StringSink sink; source.drainInto(sink); aio.blockOn(logStore.addBuildLog(path, sink.s)); diff --git a/tests/functional2/store/test_logs.py b/tests/functional2/store/test_logs.py new file mode 100644 index 000000000..679d55a0b --- /dev/null +++ b/tests/functional2/store/test_logs.py @@ -0,0 +1,47 @@ +from pathlib import Path +import shutil + +import pytest + +from testlib.fixtures.file_helper import with_files +from testlib.fixtures.nix import Nix, NixDaemon +from testlib.utils import get_global_asset_pack + + +@with_files({"simple": get_global_asset_pack("simple-drv")}) +class TestCopyLog: + @pytest.fixture(autouse=True) + def setup(self, nix: Nix, files: Path): + nix.settings.add_xp_feature("nix-command") + + # build test drv in a second store so we actually have a log to copy + nix.nix(["build", "-f", f"{files}/simple/simple.nix"]).run().ok() + self.drv = nix.nix_store(["--query", "--deriver", "result"]).run().ok().stdout_plain + self.log_text = nix.nix(["log", self.drv]).run().ok().stdout_s + + @pytest.mark.no_daemon + def test_copy_log_to_cache(self, nix: Nix): + nix.nix( + ["store", "copy-log", "--to", f"file://{nix.env.dirs.cache_dir}", self.drv] + ).run().ok() + shutil.rmtree(nix.env.dirs.nix_log_dir) + nix.nix(["log", self.drv]).run().expect(1) + nix.nix( + ["store", "copy-log", "--from", f"file://{nix.env.dirs.cache_dir}", self.drv] + ).run().ok() + assert self.log_text == nix.nix(["log", self.drv]).run().ok().stdout_s + + def test_copy_log_untrusted(self, nix: Nix, daemon: NixDaemon): + nix.nix(["store", "copy-log", "--to", nix.env.dirs.cache_dir, self.drv]).run().ok() + with daemon(nix) as inner: + cmd = inner.nix(["store", "copy-log", "--from", nix.env.dirs.cache_dir, self.drv]).run() + cmd.expect(1) + assert "you are not privileged to add logs" in cmd.stderr_plain + + @pytest.mark.nix_settings(trusted_users="*") + def test_copy_log_trusted(self, nix: Nix): + nix.nix(["store", "copy-log", "--to", nix.env.dirs.cache_dir, self.drv]).run().ok() + shutil.rmtree(nix.env.dirs.nix_log_dir) + nix.nix(["log", self.drv]).run().expect(1) + nix.nix(["store", "copy-log", "--from", nix.env.dirs.cache_dir, self.drv]).run().ok() + assert self.log_text == nix.nix(["log", self.drv]).run().ok().stdout_s