diff --git a/tests/functional2/flakes/test_pure.py b/tests/functional2/flakes/test_pure.py new file mode 100644 index 000000000..45c6fdf1b --- /dev/null +++ b/tests/functional2/flakes/test_pure.py @@ -0,0 +1,67 @@ +from pathlib import Path +from textwrap import dedent +from functional2.testlib.fixtures import Nix +import re + + +def test_purity_traversal(nix: Nix, tmp_path: Path): + ERROR_RE = re.compile(r"error: access to absolute path '.+' is forbidden in pure eval mode") + + flake_dir = tmp_path / 'flake' + flake_dir.mkdir() + + evilpath = tmp_path / 'sekrit.txt' + evilpath.write_text('kitty kitty') + + evilnix = tmp_path / 'default.nix' + evilnix.write_text('"woof"') + + goodnix = flake_dir / 'good.nix' + goodnix.write_text("1") + + nested_dir = flake_dir / 'nested' / 'nested2' + nested_dir.mkdir(parents=True) + (nested_dir / 'good.nix').write_text("1") + + (flake_dir / 'evil-link').symlink_to(evilpath) + (flake_dir / 'evil-default-nix').symlink_to(tmp_path) + (flake_dir / 'less-evil-link').symlink_to(tmp_path / 'link-to-flake') + (tmp_path / 'link-to-flake').symlink_to(flake_dir / 'flake.nix') + + (flake_dir / 'nested-good.nix').symlink_to('nested/nested2/good.nix') + (flake_dir / 'nested-bad.nix').symlink_to('nested/../../nested2/good.nix') + + (flake_dir / 'flake.nix').write_text(dedent(""" + { + inputs = {}; + outputs = inputs: { + bad1 = "${@ABSPATH@}"; + bad2 = builtins.readFile "${@ABSPATH@}"; + bad3 = builtins.readFile @ABSPATH@; + bad4 = builtins.readFile ./evil-link; + bad5 = builtins.readFile "${./evil-link}"; + bad6 = builtins.readFile ./less-evil-link; + bad7 = builtins.readFile "${./less-evil-link}"; + bad8 = import ./evil-default-nix; + bad9 = import ./nested-bad.nix; + + good1 = builtins.readFile "${inputs.self.outPath}/good.nix"; + good2 = builtins.readFile ./good.nix; + good3 = builtins.readFile "${./good.nix}"; + good4 = toString (import ./good.nix); + good5 = toString (import ./nested-good.nix); + }; + } + """).replace('@ABSPATH@', str(evilpath.absolute()))) + + for idx in range(1, 10): + cmd = nix.nix(['eval', f'.#bad{idx}'], flake=True) + cmd.cwd = flake_dir + res = cmd.run().expect(1) + print(res.stderr_plain) + assert ERROR_RE.search(res.stderr_plain) + for idx in range(1, 6): + cmd = nix.nix(['eval', f'.#good{idx}'], flake=True) + cmd.cwd = flake_dir + res = cmd.run().expect(0) + assert res.stdout_plain == '"1"' diff --git a/tests/functional2/testlib/fixtures.py b/tests/functional2/testlib/fixtures.py index fa00e3e58..259132c14 100644 --- a/tests/functional2/testlib/fixtures.py +++ b/tests/functional2/testlib/fixtures.py @@ -3,7 +3,7 @@ import json import subprocess from typing import Any from pathlib import Path -from functools import partial, partialmethod +from functools import partialmethod from functional2.testlib.terminal_code_eater import eat_terminal_codes import dataclasses @@ -198,17 +198,21 @@ class Nix: settings.store = str(store_path) return settings - def nix_cmd(self, argv: list[str], allow_builds: bool = False): + def nix_cmd(self, argv: list[str], flake: bool = False): """ Constructs a NixCommand with the appropriate settings. """ + settings = self.settings() + if flake: + settings.feature('nix-command', 'flakes') + return NixCommand(argv=argv, cwd=self.test_root, env=self.make_env(), - settings=self.settings()) + settings=settings) - def nix(self, cmd: list[str], nix_exe: str = 'nix') -> NixCommand: - return self.nix_cmd([nix_exe, *cmd]) + def nix(self, cmd: list[str], nix_exe: str = 'nix', flake: bool = False) -> NixCommand: + return self.nix_cmd([nix_exe, *cmd], flake=flake) nix_build = partialmethod(nix, nix_exe='nix-build') nix_shell = partialmethod(nix, nix_exe='nix-shell')