diff options
84 files changed, 2042 insertions, 712 deletions
diff --git a/.clang-tidy b/.clang-tidy index 3b5dcd91a..0cc1f2520 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -16,3 +16,6 @@ Checks: - -bugprone-unchecked-optional-access # many warnings, seems like a questionable lint - -bugprone-branch-clone + +CheckOptions: + bugprone-reserved-identifier.AllowedIdentifiers: '__asan_default_options' diff --git a/doc/manual/change-authors.yml b/doc/manual/change-authors.yml index 49df7f5df..630af29ff 100644 --- a/doc/manual/change-authors.yml +++ b/doc/manual/change-authors.yml @@ -57,6 +57,11 @@ ericson: display_name: John Ericson github: ericson2314 +goldstein: + display_name: goldstein + forgejo: goldstein + github: GoldsteinE + horrors: display_name: eldritch horrors forgejo: pennae diff --git a/doc/manual/rl-next/block-io-uring.md b/doc/manual/rl-next/block-io-uring.md new file mode 100644 index 000000000..6ebba9a20 --- /dev/null +++ b/doc/manual/rl-next/block-io-uring.md @@ -0,0 +1,12 @@ +--- +synopsis: "Block io_uring in the Linux sandbox" +cls: 1611 +credits: alois31 +category: Breaking Changes +--- + +The io\_uring API has the unfortunate property that it is not possible to selectively decide which operations should be allowed. +This, together with the fact that new operations are routinely added, makes it a hazard to the proper function of the sandbox. + +Therefore, any access to io\_uring has been made unavailable inside the sandbox. +As such, attempts to execute any system calls forming part of this API will fail with the error `ENOSYS`, as if io\_uring support had not been configured into the kernel. diff --git a/doc/manual/rl-next/repl-edit-store.md b/doc/manual/rl-next/repl-edit-store.md new file mode 100644 index 000000000..e93bde07d --- /dev/null +++ b/doc/manual/rl-next/repl-edit-store.md @@ -0,0 +1,11 @@ +--- +synopsis: "`:edit`ing a file in Nix store no longer reloads the repl" +issues: [fj#341] +cls: [1620] +category: Improvements +credits: [goldstein] +--- + +Calling `:edit` from the repl now only reloads if the file being edited was outside of Nix store. +That means that all the local variables are now preserved across `:edit`s of store paths. +This is always safe because the store is read-only. diff --git a/flake.lock b/flake.lock index a3306f941..84e578bc3 100644 --- a/flake.lock +++ b/flake.lock @@ -19,11 +19,11 @@ "nix2container": { "flake": false, "locked": { - "lastModified": 1712990762, - "narHash": "sha256-hO9W3w7NcnYeX8u8cleHiSpK2YJo7ecarFTUlbybl7k=", + "lastModified": 1720642556, + "narHash": "sha256-qsnqk13UmREKmRT7c8hEnz26X3GFFyIQrqx4EaRc1Is=", "owner": "nlewo", "repo": "nix2container", - "rev": "20aad300c925639d5d6cbe30013c8357ce9f2a2e", + "rev": "3853e5caf9ad24103b13aa6e0e8bcebb47649fe4", "type": "github" }, "original": { @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1718111384, - "narHash": "sha256-7tSst0S5FOmcgvNtfy6cjZX5w8CabCVAfAeCkhY4OVg=", + "lastModified": 1721931987, + "narHash": "sha256-1Zg8LY0T5EfXtv0Kf4M6SFnjH7Eto4VV+EKJ/YSnhiI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a508a44af0c1b1b57785c34d8b54783536273eeb", + "rev": "e21630230c77140bc6478a21cd71e8bb73706fce", "type": "github" }, "original": { @@ -67,11 +67,11 @@ "pre-commit-hooks": { "flake": false, "locked": { - "lastModified": 1712055707, - "narHash": "sha256-4XLvuSIDZJGS17xEwSrNuJLL7UjDYKGJSbK1WWX2AK8=", + "lastModified": 1721042469, + "narHash": "sha256-6FPUl7HVtvRHCCBQne7Ylp4p+dpP3P/OYuzjztZ4s70=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "e35aed5fda3cc79f88ed7f1795021e559582093a", + "rev": "f451c19376071a90d8c58ab1a953c6e9840527fd", "type": "github" }, "original": { @@ -33,7 +33,7 @@ # This notice gets echoed as a dev shell hook, and can be turned off with # `touch .nocontribmsg` - sgr = ''[''; + sgr = builtins.fromJSON ''"\u001b["''; freezePage = "https://wiki.lix.systems/books/lix-contributors/page/freezes-and-recommended-contributions"; codebaseOverview = "https://wiki.lix.systems/books/lix-contributors/page/codebase-overview"; contribNotice = builtins.toFile "lix-contrib-notice" '' @@ -59,7 +59,8 @@ (Run `touch .nocontribmsg` to hide this message.) ''; - officialRelease = false; + versionJson = builtins.fromJSON (builtins.readFile ./version.json); + officialRelease = versionJson.official_release; # Set to true to build the release notes for the next release. buildUnreleasedNotes = true; @@ -140,10 +141,7 @@ system = crossSystem; } // lib.optionalAttrs (crossSystem == "x86_64-freebsd") { useLLVM = true; }; - overlays = [ - (overlayFor (p: p.${stdenv})) - (final: prev: { nixfmt = final.callPackage ./nix-support/nixfmt.nix { }; }) - ]; + overlays = [ (overlayFor (p: p.${stdenv})) ]; }; stdenvs = forAllStdenvs (make-pkgs null); native = stdenvs.stdenvPackages; @@ -167,6 +165,7 @@ nixUnstable = prev.nixUnstable; check-headers = final.buildPackages.callPackage ./maintainers/check-headers.nix { }; + check-syscalls = final.buildPackages.callPackage ./maintainers/check-syscalls.nix { }; default-busybox-sandbox-shell = final.busybox.override { useMusl = true; @@ -198,8 +197,6 @@ busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell; }; - pegtl = final.nix.passthru.pegtl; - # Export the patched version of boehmgc that Lix uses into the overlay # for consumers of this flake. boehmgc-nix = final.nix.passthru.boehmgc-nix; @@ -279,6 +276,19 @@ # System tests. tests = import ./tests/nixos { inherit lib nixpkgs nixpkgsFor; } // { + # This is x86_64-linux only, just because we have significantly + # cheaper x86_64-linux compute in CI. + # It is clangStdenv because clang's sanitizers are nicer. + asanBuild = self.packages.x86_64-linux.nix-clangStdenv.override { + sanitize = [ + "address" + "undefined" + ]; + # it is very hard to make *every* CI build use this option such + # that we don't wind up building Lix twice, so we do it here where + # we are already doing so. + werror = true; + }; # Make sure that nix-env still produces the exact same result # on a particular version of Nixpkgs. @@ -410,7 +420,7 @@ pkgs: stdenv: let nix = pkgs.callPackage ./package.nix { - inherit stdenv officialRelease versionSuffix; + inherit stdenv versionSuffix; busybox-sandbox-shell = pkgs.busybox-sandbox-shell or pkgs.default-busybox-sandbox; internalApiDocs = false; }; diff --git a/maintainers/check-syscalls.nix b/maintainers/check-syscalls.nix new file mode 100644 index 000000000..1a3de5c6d --- /dev/null +++ b/maintainers/check-syscalls.nix @@ -0,0 +1,16 @@ +{ + runCommandNoCC, + lib, + libseccomp, + writeShellScriptBin, +}: +let + syscalls-csv = runCommandNoCC "syscalls.csv" { } '' + echo ${lib.escapeShellArg libseccomp.src} + tar -xf ${lib.escapeShellArg libseccomp.src} --strip-components=2 ${libseccomp.name}/src/syscalls.csv + mv syscalls.csv "$out" + ''; +in +writeShellScriptBin "check-syscalls" '' + ${./check-syscalls.sh} ${syscalls-csv} +'' diff --git a/maintainers/check-syscalls.sh b/maintainers/check-syscalls.sh new file mode 100755 index 000000000..cd72ac23b --- /dev/null +++ b/maintainers/check-syscalls.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +diff -u <(awk < src/libstore/build/local-derivation-goal.cc '/BEGIN extract-syscalls/ { extracting = 1; next } +match($0, /allowSyscall\(ctx, SCMP_SYS\(([^)]*)\)\);|\/\/ skip ([^ ]*)/, result) { print result[1] result[2] } +/END extract-syscalls/ { extracting = 0; next }') <(tail -n+2 "$1" | cut -d, -f 1) diff --git a/meson.build b/meson.build index 8ce06800b..ed50dff78 100644 --- a/meson.build +++ b/meson.build @@ -199,7 +199,11 @@ configdata = { } # Dependencies # -boehm = dependency('bdw-gc', required : get_option('gc'), version : '>=8.2.6') +gc_opt = get_option('gc').disable_if( + 'address' in get_option('b_sanitize'), + error_message: 'gc does far too many memory crimes for ASan' +) +boehm = dependency('bdw-gc', required : gc_opt, version : '>=8.2.6') configdata += { 'HAVE_BOEHMGC': boehm.found().to_int(), } @@ -314,6 +318,10 @@ nlohmann_json = dependency('nlohmann_json', required : true) # *absolutely* are not going to make it work) lix_doc = declare_dependency(link_args : [ '-llix_doc' ]) +if is_freebsd + libprocstat = declare_dependency(link_args : [ '-lprocstat' ]) +endif + # # Build-time tools # @@ -445,6 +453,7 @@ add_project_arguments( '-Werror=unused-result', '-Wdeprecated-copy', '-Wignored-qualifiers', + '-Werror=suggest-override', # Enable assertions in libstdc++ by default. Harmless on libc++. Benchmarked # at ~1% overhead in `nix search`. # @@ -477,7 +486,14 @@ if cxx.get_id() == 'clang' and get_option('b_sanitize') != '' add_project_link_arguments('-shared-libsan', language : 'cpp') endif +# Clang gets grumpy about missing libasan symbols if -shared-libasan is not +# passed when building shared libs, at least on Linux +if cxx.get_id() == 'clang' and 'address' in get_option('b_sanitize') + add_project_link_arguments('-shared-libasan', language : 'cpp') +endif + add_project_link_arguments('-pthread', language : 'cpp') + if cxx.get_linker_id() in ['ld.bfd', 'ld.gold'] add_project_link_arguments('-Wl,--no-copy-dt-needed-entries', language : 'cpp') endif @@ -492,7 +508,7 @@ endif # maintainers/buildtime_report.sh BUILD-DIR to simply work in clang builds. # # They can also be manually viewed at https://ui.perfetto.dev -if get_option('profile-build').require(meson.get_compiler('cpp').get_id() == 'clang').enabled() +if get_option('profile-build').require(cxx.get_id() == 'clang').enabled() add_project_arguments('-ftime-trace', language: 'cpp') endif diff --git a/misc/pegtl.nix b/misc/pegtl.nix deleted file mode 100644 index 3fd999d9d..000000000 --- a/misc/pegtl.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - stdenv, - cmake, - ninja, - fetchFromGitHub, -}: - -stdenv.mkDerivation { - pname = "pegtl"; - version = "3.2.7"; - - src = fetchFromGitHub { - repo = "PEGTL"; - owner = "taocpp"; - rev = "refs/tags/3.2.7"; - hash = "sha256-IV5YNGE4EWVrmg2Sia/rcU8jCuiBynQGJM6n3DCWTQU="; - }; - - nativeBuildInputs = [ - cmake - ninja - ]; -} diff --git a/misc/pre-commit.nix b/misc/pre-commit.nix index 292940e83..59ad2f874 100644 --- a/misc/pre-commit.nix +++ b/misc/pre-commit.nix @@ -106,7 +106,7 @@ pre-commit-run { }; treefmt = { enable = true; - settings.formatters = [ pkgs.nixfmt ]; + settings.formatters = [ pkgs.nixfmt-rfc-style ]; }; }; } diff --git a/nix-support/nixfmt.nix b/nix-support/nixfmt.nix deleted file mode 100644 index b51e26295..000000000 --- a/nix-support/nixfmt.nix +++ /dev/null @@ -1,65 +0,0 @@ -# Copy of `nixfmt-rfc-style` vendored from `nixpkgs` master: -# https://github.com/NixOS/nixpkgs/blob/ab6071eb54cc9b66dda436111d4f569e4e56cbf4/pkgs/by-name/ni/nixfmt-rfc-style/package.nix -{ - haskell, - haskellPackages, - fetchFromGitHub, -}: -let - inherit (haskell.lib.compose) justStaticExecutables; - raw-pkg = haskellPackages.callPackage ( - { - mkDerivation, - base, - cmdargs, - directory, - fetchzip, - filepath, - lib, - megaparsec, - mtl, - parser-combinators, - safe-exceptions, - scientific, - text, - transformers, - unix, - }: - mkDerivation { - pname = "nixfmt"; - version = "0.6.0-unstable-2024-03-14"; - src = fetchFromGitHub { - owner = "serokell"; - repo = "nixfmt"; - rev = "8d13b593fa8d8d6e5075f541f3231222a08e84df"; - hash = "sha256-HtXvzmfN4wk45qiKZ7V+/5WBV7jnTHfd7iBwF4XGl64="; - }; - isLibrary = true; - isExecutable = true; - libraryHaskellDepends = [ - base - megaparsec - mtl - parser-combinators - scientific - text - transformers - ]; - executableHaskellDepends = [ - base - cmdargs - directory - filepath - safe-exceptions - text - unix - ]; - jailbreak = true; - homepage = "https://github.com/serokell/nixfmt"; - description = "An opinionated formatter for Nix"; - license = lib.licenses.mpl20; - mainProgram = "nixfmt"; - } - ) { }; -in -justStaticExecutables raw-pkg diff --git a/package.nix b/package.nix index b9ef0273c..62927e569 100644 --- a/package.nix +++ b/package.nix @@ -36,7 +36,7 @@ meson, ninja, openssl, - pegtl ? __forDefaults.pegtl, + pegtl, pkg-config, python3, rapidcheck, @@ -53,16 +53,24 @@ pname ? "lix", versionSuffix ? "", - officialRelease ? false, + officialRelease ? __forDefaults.versionJson.official_release, # Set to true to build the release notes for the next release. buildUnreleasedNotes ? true, internalApiDocs ? false, + # List of Meson sanitize options. Accepts values of b_sanitize, e.g. + # "address", "undefined", "thread". + sanitize ? null, + # Turn compiler warnings into errors. + werror ? false, + # Not a real argument, just the only way to approximate let-binding some # stuff for argument defaults. __forDefaults ? { canRunInstalled = stdenv.buildPlatform.canExecute stdenv.hostPlatform; + versionJson = builtins.fromJSON (builtins.readFile ./version.json); + boehmgc-nix = boehmgc.override { enableLargeConfig = true; }; editline-lix = editline.overrideAttrs (prev: { @@ -71,8 +79,6 @@ lix-doc = callPackage ./lix-doc/package.nix { }; build-release-notes = callPackage ./maintainers/build-release-notes.nix { }; - - pegtl = callPackage ./misc/pegtl.nix { }; }, }: let @@ -80,8 +86,7 @@ let inherit (lib) fileset; inherit (stdenv) hostPlatform buildPlatform; - versionJson = builtins.fromJSON (builtins.readFile ./version.json); - version = versionJson.version + versionSuffix; + version = __forDefaults.versionJson.version + versionSuffix; aws-sdk-cpp-nix = aws-sdk-cpp.override { apis = [ @@ -93,31 +98,19 @@ let # Reimplementation of Nixpkgs' Meson cross file, with some additions to make # it actually work. - mesonCrossFile = - let - cpuFamily = - platform: - with platform; - if isAarch32 then - "arm" - else if isx86_32 then - "x86" - else - platform.uname.processor; - in - builtins.toFile "lix-cross-file.conf" '' - [properties] - # Meson is convinced that if !buildPlatform.canExecute hostPlatform then we cannot - # build anything at all, which is not at all correct. If we can't execute the host - # platform, we'll just disable tests and doc gen. - needs_exe_wrapper = false - - [binaries] - # Meson refuses to consider any CMake binary during cross compilation if it's - # not explicitly specified here, in the cross file. - # https://github.com/mesonbuild/meson/blob/0ed78cf6fa6d87c0738f67ae43525e661b50a8a2/mesonbuild/cmake/executor.py#L72 - cmake = 'cmake' - ''; + mesonCrossFile = builtins.toFile "lix-cross-file.conf" '' + [properties] + # Meson is convinced that if !buildPlatform.canExecute hostPlatform then we cannot + # build anything at all, which is not at all correct. If we can't execute the host + # platform, we'll just disable tests and doc gen. + needs_exe_wrapper = false + + [binaries] + # Meson refuses to consider any CMake binary during cross compilation if it's + # not explicitly specified here, in the cross file. + # https://github.com/mesonbuild/meson/blob/0ed78cf6fa6d87c0738f67ae43525e661b50a8a2/mesonbuild/cmake/executor.py#L72 + cmake = 'cmake' + ''; # The internal API docs need these for the build, but if we're not building # Nix itself, then these don't need to be propagated. @@ -181,6 +174,12 @@ stdenv.mkDerivation (finalAttrs: { dontBuild = false; mesonFlags = + let + sanitizeOpts = lib.optionals (sanitize != null) ( + [ "-Db_sanitize=${builtins.concatStringsSep "," sanitize}" ] + ++ lib.optional (builtins.elem "address" sanitize) "-Dgc=disabled" + ); + in lib.optionals hostPlatform.isLinux [ # You'd think meson could just find this in PATH, but busybox is in buildInputs, # which don't actually get added to PATH. And buildInputs is correct over @@ -196,8 +195,10 @@ stdenv.mkDerivation (finalAttrs: { (lib.mesonEnable "internal-api-docs" internalApiDocs) (lib.mesonBool "enable-tests" finalAttrs.finalPackage.doCheck) (lib.mesonBool "enable-docs" canRunInstalled) + (lib.mesonBool "werror" werror) ] - ++ lib.optional (hostPlatform != buildPlatform) "--cross-file=${mesonCrossFile}"; + ++ lib.optional (hostPlatform != buildPlatform) "--cross-file=${mesonCrossFile}" + ++ sanitizeOpts; # We only include CMake so that Meson can locate toml11, which only ships CMake dependency metadata. dontUseCmakeConfigure = true; @@ -384,8 +385,6 @@ stdenv.mkDerivation (finalAttrs: { pegtl ; - inherit officialRelease; - # The collection of dependency logic for this derivation is complicated enough that # it's easier to parameterize the devShell off an already called package.nix. mkDevShell = @@ -399,13 +398,14 @@ stdenv.mkDerivation (finalAttrs: { glibcLocales, just, llvmPackages, - nixfmt, + nixfmt-rfc-style, skopeo, xonsh, # Lix specific packages pre-commit-checks, contribNotice, + check-syscalls, }: let glibcFix = lib.optionalAttrs (buildPlatform.isLinux && glibcLocales != null) { @@ -456,11 +456,12 @@ stdenv.mkDerivation (finalAttrs: { # `bash` from inside `nix develop`, say, because you are using it # via direnv, you will by default get bash (unusable edition). bashInteractive + check-syscalls pythonEnv # docker image tool skopeo just - nixfmt + nixfmt-rfc-style # Included above when internalApiDocs is true, but we set that to # false intentionally to save dev build time. # To build them in a dev shell, you can set -Dinternal-api-docs=enabled when configuring. diff --git a/releng/README.md b/releng/README.md index cfacf4b8e..2aa3b959f 100644 --- a/releng/README.md +++ b/releng/README.md @@ -30,7 +30,7 @@ First, we prepare the release. `python -m releng prepare` is used for this. Then we tag the release with `python -m releng tag`: * Git HEAD is detached. -* `officialRelease = true` is set in `flake.nix`, this is committed, and a +* `"official_release": true` is set in `version.json`, this is committed, and a release is tagged. * The tag is merged back into the last branch (either `main` for new releases or `release-MAJOR` for maintenance releases) with `git merge -s ours VERSION` diff --git a/releng/create_release.xsh b/releng/create_release.xsh index 358124359..62114350b 100644 --- a/releng/create_release.xsh +++ b/releng/create_release.xsh @@ -11,7 +11,7 @@ from . import environment from .environment import RelengEnvironment from . import keys from . import docker -from .version import VERSION, RELEASE_NAME, MAJOR +from .version import VERSION, RELEASE_NAME, MAJOR, OFFICIAL_RELEASE from .gitutils import verify_are_on_tag, git_preconditions from . import release_notes @@ -39,12 +39,18 @@ def setup_creds(env: RelengEnvironment): def official_release_commit_tag(force_tag=False): - print('[+] Setting officialRelease in flake.nix and tagging') + print('[+] Setting officialRelease in version.json and tagging') prev_branch = $(git symbolic-ref --short HEAD).strip() git switch --detach - sed -i 's/officialRelease = false/officialRelease = true/' flake.nix - git add flake.nix + + # Must be done in two parts due to buffering (opening the file immediately + # would truncate it). + new_version_json = $(jq --indent 4 '.official_release = true' version.json) + with open('version.json', 'w') as fh: + fh.write(new_version_json) + git add version.json + message = f'release: {VERSION} "{RELEASE_NAME}"\n\nRelease produced with releng/create_release.xsh' git commit -m @(message) git tag @(['-f'] if force_tag else []) -a -m @(message) @(VERSION) @@ -250,15 +256,14 @@ def build_manual(eval_result): def upload_manual(env: RelengEnvironment): - stable = json.loads($(nix eval --json '.#nix.officialRelease')) - if stable: + if OFFICIAL_RELEASE: version = MAJOR else: version = 'nightly' print('[+] aws s3 sync manual') aws s3 sync @(MANUAL)/ @(env.docs_bucket)/manual/lix/@(version)/ - if stable: + if OFFICIAL_RELEASE: aws s3 sync @(MANUAL)/ @(env.docs_bucket)/manual/lix/stable/ diff --git a/releng/docker.xsh b/releng/docker.xsh index 13bdd7868..2ea7aecff 100644 --- a/releng/docker.xsh +++ b/releng/docker.xsh @@ -44,23 +44,8 @@ def upload_docker_images(target: DockerTarget, paths: list[Path]): for path in paths: digest_file = tmp / (path.name + '.digest') - tmp_image = tmp / 'tmp-image.tar.gz' - # insecure-policy: we don't have any signature policy, we are just uploading an image - # - # Absurd: we copy it into an OCI image first so we can get the hash - # we need to upload it untagged, because skopeo has no "don't tag - # this" option. - # The reason for this is that forgejo's container registry throws - # away old versions of tags immediately, so we cannot use a temp - # tag, and it *does* reduce confusion to not upload tags that - # should not be used. - # - # Workaround for: https://github.com/containers/skopeo/issues/2354 - log.info('skopeo copy to temp oci-archive %s', tmp_image) - skopeo --insecure-policy copy --format oci --all --digestfile @(digest_file) docker-archive:@(path) oci-archive:@(tmp_image) - - inspection = json.loads($(skopeo inspect oci-archive:@(tmp_image))) + inspection = json.loads($(skopeo inspect docker-archive:@(path))) docker_arch = inspection['Architecture'] docker_os = inspection['Os'] @@ -68,8 +53,9 @@ def upload_docker_images(target: DockerTarget, paths: list[Path]): log.info('Pushing image %s for %s to %s', path, docker_arch, target.registry_path) + # insecure-policy: we don't have any signature policy, we are just uploading an image + skopeo --insecure-policy copy --digestfile @(digest_file) --all docker-archive:@(path) f'docker://{target.registry_path}@@unknown-digest@@' digest = digest_file.read_text().strip() - skopeo --insecure-policy copy --preserve-digests --all oci-archive:@(tmp_image) f'docker://{target.registry_path}@{digest}' # skopeo doesn't give us the manifest size directly, so we just ask the registry metadata = reg.image_info(target.registry_path, digest) diff --git a/releng/version.py b/releng/version.py index 47ef23504..4ad188d46 100644 --- a/releng/version.py +++ b/releng/version.py @@ -4,3 +4,4 @@ version_json = json.load(open('version.json')) VERSION = version_json['version'] MAJOR = '.'.join(VERSION.split('.')[:2]) RELEASE_NAME = version_json['release_name'] +OFFICIAL_RELEASE = version_json['official_release'] diff --git a/src/asan-options/asan-options.cc b/src/asan-options/asan-options.cc new file mode 100644 index 000000000..c4cf360af --- /dev/null +++ b/src/asan-options/asan-options.cc @@ -0,0 +1,17 @@ +/// @file This is very bothersome code that has to be included in every +/// executable to get the correct default ASan options. I am so sorry. + +extern "C" [[gnu::retain]] const char *__asan_default_options() +{ + // We leak a bunch of memory knowingly on purpose. It's not worthwhile to + // diagnose that memory being leaked for now. + // + // Instruction bytes are useful for finding the actual code that + // corresponds to an ASan report. + // + // TODO: setting log_path=asan.log or not: neither works, since you can't + // write to the fs in certain places in the testsuite, but you also cannot + // write arbitrarily to stderr in other places so the reports get eaten. + // pain 🥖 + return "halt_on_error=1:abort_on_error=1:detect_leaks=0:print_summary=1:dump_instruction_bytes=1"; +} diff --git a/src/build-remote/build-remote.cc b/src/build-remote/build-remote.cc index fb90403a0..99bbc62d5 100644 --- a/src/build-remote/build-remote.cc +++ b/src/build-remote/build-remote.cc @@ -236,9 +236,9 @@ static int main_build_remote(int argc, char * * argv) } #if __APPLE__ - futimes(bestSlotLock.get(), NULL); + futimes(bestSlotLock.get(), nullptr); #else - futimens(bestSlotLock.get(), NULL); + futimens(bestSlotLock.get(), nullptr); #endif lock.reset(); diff --git a/src/libcmd/installable-attr-path.cc b/src/libcmd/installable-attr-path.cc index eb15fecc3..9891b263c 100644 --- a/src/libcmd/installable-attr-path.cc +++ b/src/libcmd/installable-attr-path.cc @@ -1,23 +1,11 @@ -#include "globals.hh" #include "installable-attr-path.hh" #include "outputs-spec.hh" #include "command.hh" #include "attr-path.hh" #include "common-eval-args.hh" -#include "derivations.hh" -#include "eval-inline.hh" #include "eval.hh" #include "get-drvs.hh" -#include "store-api.hh" -#include "shared.hh" #include "flake/flake.hh" -#include "eval-cache.hh" -#include "url.hh" -#include "registry.hh" -#include "build-result.hh" - -#include <regex> -#include <queue> #include <nlohmann/json.hpp> diff --git a/src/libcmd/installable-attr-path.hh b/src/libcmd/installable-attr-path.hh index 86c2f8219..edc9c2906 100644 --- a/src/libcmd/installable-attr-path.hh +++ b/src/libcmd/installable-attr-path.hh @@ -1,25 +1,11 @@ #pragma once ///@file -#include "globals.hh" #include "installable-value.hh" #include "outputs-spec.hh" #include "command.hh" -#include "attr-path.hh" #include "common-eval-args.hh" -#include "derivations.hh" -#include "eval-inline.hh" #include "eval.hh" -#include "get-drvs.hh" -#include "store-api.hh" -#include "shared.hh" -#include "eval-cache.hh" -#include "url.hh" -#include "registry.hh" -#include "build-result.hh" - -#include <regex> -#include <queue> #include <nlohmann/json.hpp> diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index 635b54958..5086e9999 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -652,9 +652,12 @@ ProcessLineResult NixRepl::processLine(std::string line) // using runProgram2 to allow editors to display their UI runProgram2(RunOptions { .program = editor, .searchPath = true, .args = args }).wait(); - // Reload right after exiting the editor - state->resetFileCache(); - reloadFiles(); + // Reload right after exiting the editor if path is not in store + // Store is immutable, so there could be no changes, so there's no need to reload + if (!state->store->isInStore(path.resolveSymlinks().path.abs())) { + state->resetFileCache(); + reloadFiles(); + } } else if (command == ":t") { diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index c0e7a9a2e..a925ce2d8 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -494,6 +494,14 @@ std::ostream & operator<<(std::ostream & output, PrimOp & primOp) } +Value::Value(primop_t, PrimOp & primop) + : internalType(tPrimOp) + , primOp(&primop) + , _primop_pad(0) +{ + primop.check(); +} + PrimOp * Value::primOpAppPrimOp() const { Value * left = primOpApp.left; @@ -506,7 +514,6 @@ PrimOp * Value::primOpAppPrimOp() const return left->primOp; } - void Value::mkPrimOp(PrimOp * p) { p->check(); diff --git a/src/libexpr/flake/flakeref.cc b/src/libexpr/flake/flakeref.cc index a95df04ba..3be4ea550 100644 --- a/src/libexpr/flake/flakeref.cc +++ b/src/libexpr/flake/flakeref.cc @@ -169,14 +169,13 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment( if (subdir != "") { if (parsedURL.query.count("dir")) throw Error("flake URL '%s' has an inconsistent 'dir' parameter", url); - parsedURL.query.insert_or_assign("dir", subdir); } if (pathExists(flakeRoot + "/.git/shallow")) parsedURL.query.insert_or_assign("shallow", "1"); return std::make_pair( - FlakeRef(Input::fromURL(parsedURL, isFlake), getOr(parsedURL.query, "dir", "")), + FlakeRef(Input::fromURL(parsedURL, isFlake), subdir), fragment); } @@ -204,7 +203,13 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment( std::string fragment; std::swap(fragment, parsedURL.fragment); - auto input = Input::fromURL(parsedURL, isFlake); + // This has a special meaning for flakes and must not be passed to libfetchers. + // Of course this means that libfetchers cannot have fetchers + // expecting an argument `dir` 🫠+ ParsedURL urlForFetchers(parsedURL); + urlForFetchers.query.erase("dir"); + + auto input = Input::fromURL(urlForFetchers, isFlake); input.parent = baseDir; return std::make_pair( diff --git a/src/libexpr/gc-alloc.hh b/src/libexpr/gc-alloc.hh index 04ac28ea8..fc034045f 100644 --- a/src/libexpr/gc-alloc.hh +++ b/src/libexpr/gc-alloc.hh @@ -10,6 +10,8 @@ #include <string_view> #include <vector> +#include "checked-arithmetic.hh" + #if HAVE_BOEHMGC #include <functional> // std::less #include <utility> // std::pair @@ -18,8 +20,6 @@ #include <gc/gc_allocator.h> #include <gc/gc_cpp.h> -#include "checked-arithmetic.hh" - /// calloc, transparently GC-enabled. #define LIX_GC_CALLOC(size) GC_MALLOC(size) diff --git a/src/libexpr/get-drvs.cc b/src/libexpr/get-drvs.cc index 8e3969aac..d7869d09b 100644 --- a/src/libexpr/get-drvs.cc +++ b/src/libexpr/get-drvs.cc @@ -1,6 +1,7 @@ #include "get-drvs.hh" #include "eval-inline.hh" #include "derivations.hh" +#include "eval.hh" #include "store-api.hh" #include "path-with-outputs.hh" @@ -101,66 +102,134 @@ StorePath DrvInfo::queryOutPath() return *outPath; } +void DrvInfo::fillOutputs(bool withPaths) +{ + auto fillDefault = [&]() { + std::optional<StorePath> outPath = std::nullopt; + if (withPaths) { + outPath.emplace(this->queryOutPath()); + } + this->outputs.emplace("out", outPath); + }; + + // lol. lmao even. + if (this->attrs == nullptr) { + fillDefault(); + return; + } + + Attr * outputs = this->attrs->get(this->state->sOutputs); + if (outputs == nullptr) { + fillDefault(); + return; + } + + // NOTE(Qyriad): I don't think there is any codepath that can cause this to error. + this->state->forceList( + *outputs->value, + outputs->pos, + "while evaluating the 'outputs' attribute of a derivation" + ); + + for (auto [idx, elem] : enumerate(outputs->value->listItems())) { + // NOTE(Qyriad): This error should be *extremely* rare in practice. + // It is impossible to construct with `stdenv.mkDerivation`, + // `builtins.derivation`, or even `derivationStrict`. As far as we can tell, + // it is only possible by overriding a derivation attrset already created by + // one of those with `//` to introduce the failing `outputs` entry. + auto errMsg = fmt("while evaluating output %d of a derivation", idx); + std::string_view outputName = state->forceStringNoCtx( + *elem, + outputs->pos, + errMsg + ); + + if (withPaths) { + // Find the attr with this output's name... + Attr * out = this->attrs->get(this->state->symbols.create(outputName)); + if (out == nullptr) { + // FIXME: throw error? + continue; + } + + // Meanwhile we couldn't figure out any circumstances + // that cause this to error. + state->forceAttrs(*out->value, outputs->pos, errMsg); + + // ...and evaluate its `outPath` attribute. + Attr * outPath = out->value->attrs->get(this->state->sOutPath); + if (outPath == nullptr) { + continue; + // FIXME: throw error? + } + + NixStringContext context; + // And idk what could possibly cause this one to error + // that wouldn't error before here. + auto storePath = state->coerceToStorePath( + outPath->pos, + *outPath->value, + context, + errMsg + ); + this->outputs.emplace(outputName, storePath); + } else { + this->outputs.emplace(outputName, std::nullopt); + } + } +} DrvInfo::Outputs DrvInfo::queryOutputs(bool withPaths, bool onlyOutputsToInstall) { + // If we haven't already cached the outputs set, then do so now. if (outputs.empty()) { - /* Get the ‘outputs’ list. */ - Bindings::iterator i; - if (attrs && (i = attrs->find(state->sOutputs)) != attrs->end()) { - state->forceList(*i->value, i->pos, "while evaluating the 'outputs' attribute of a derivation"); - - /* For each output... */ - for (auto elem : i->value->listItems()) { - std::string output(state->forceStringNoCtx(*elem, i->pos, "while evaluating the name of an output of a derivation")); - - if (withPaths) { - /* Evaluate the corresponding set. */ - Bindings::iterator out = attrs->find(state->symbols.create(output)); - if (out == attrs->end()) continue; // FIXME: throw error? - state->forceAttrs(*out->value, i->pos, "while evaluating an output of a derivation"); - - /* And evaluate its ‘outPath’ attribute. */ - Bindings::iterator outPath = out->value->attrs->find(state->sOutPath); - if (outPath == out->value->attrs->end()) continue; // FIXME: throw error? - NixStringContext context; - outputs.emplace(output, state->coerceToStorePath(outPath->pos, *outPath->value, context, "while evaluating an output path of a derivation")); - } else - outputs.emplace(output, std::nullopt); - } - } else - outputs.emplace("out", withPaths ? std::optional{queryOutPath()} : std::nullopt); + // FIXME: this behavior seems kind of busted, since whether or not this + // DrvInfo will have paths is forever determined by the *first* call to + // this function?? + fillOutputs(withPaths); } + // Things that operate on derivations like packages, like `nix-env` and `nix build`, + // allow derivations to specify which outputs should be used in those user-facing + // cases if the user didn't specify an output explicitly. + // If the caller just wanted all the outputs for this derivation, though, + // then we're done here. if (!onlyOutputsToInstall || !attrs) return outputs; - Bindings::iterator i; - if (attrs && (i = attrs->find(state->sOutputSpecified)) != attrs->end() && state->forceBool(*i->value, i->pos, "while evaluating the 'outputSpecified' attribute of a derivation")) { - Outputs result; - auto out = outputs.find(queryOutputName()); - if (out == outputs.end()) - throw Error("derivation does not have output '%s'", queryOutputName()); - result.insert(*out); - return result; + // Regardless of `meta.outputsToInstall`, though, you can select into a derivation + // output by its attribute, e.g. `pkgs.lix.dev`, which (lol?) sets the magic + // attribute `outputSpecified = true`, and changes the `outputName` attr to the + // explicitly selected-into output. + if (Attr * outSpecAttr = attrs->get(state->sOutputSpecified)) { + bool outputSpecified = this->state->forceBool( + *outSpecAttr->value, + outSpecAttr->pos, + "while evaluating the 'outputSpecified' attribute of a derivation" + ); + if (outputSpecified) { + auto maybeOut = outputs.find(queryOutputName()); + if (maybeOut == outputs.end()) { + throw Error("derivation does not have output '%s'", queryOutputName()); + } + return Outputs{*maybeOut}; + } } - else { - /* Check for `meta.outputsToInstall` and return `outputs` reduced to that. */ - const Value * outTI = queryMeta("outputsToInstall"); - if (!outTI) return outputs; - auto errMsg = Error("this derivation has bad 'meta.outputsToInstall'"); - /* ^ this shows during `nix-env -i` right under the bad derivation */ - if (!outTI->isList()) throw errMsg; - Outputs result; - for (auto elem : outTI->listItems()) { - if (elem->type() != nString) throw errMsg; - auto out = outputs.find(elem->string.s); - if (out == outputs.end()) throw errMsg; - result.insert(*out); - } - return result; + /* Check for `meta.outputsToInstall` and return `outputs` reduced to that. */ + const Value * outTI = queryMeta("outputsToInstall"); + if (!outTI) return outputs; + auto errMsg = Error("this derivation has bad 'meta.outputsToInstall'"); + /* ^ this shows during `nix-env -i` right under the bad derivation */ + if (!outTI->isList()) throw errMsg; + Outputs result; + for (auto elem : outTI->listItems()) { + if (elem->type() != nString) throw errMsg; + auto out = outputs.find(elem->string.s); + if (out == outputs.end()) throw errMsg; + result.insert(*out); } + return result; } @@ -350,56 +419,95 @@ static void getDerivations(EvalState & state, Value & vIn, Value v; state.autoCallFunction(autoArgs, vIn, v); - /* Process the expression. */ - if (!getDerivation(state, v, pathPrefix, drvs, ignoreAssertionFailures)) ; - - else if (v.type() == nAttrs) { - - /* Dont consider sets we've already seen, e.g. y in - `rec { x.d = derivation {...}; y = x; }`. */ - if (!done.insert(v.attrs).second) return; - - /* !!! undocumented hackery to support combining channels in - nix-env.cc. */ - bool combineChannels = v.attrs->find(state.symbols.create("_combineChannels")) != v.attrs->end(); - - /* Consider the attributes in sorted order to get more - deterministic behaviour in nix-env operations (e.g. when - there are names clashes between derivations, the derivation - bound to the attribute with the "lower" name should take - precedence). */ - for (auto & i : v.attrs->lexicographicOrder(state.symbols)) { - debug("evaluating attribute '%1%'", state.symbols[i->name]); - if (!std::regex_match(std::string(state.symbols[i->name]), attrRegex)) - continue; - std::string pathPrefix2 = addToPath(pathPrefix, state.symbols[i->name]); - if (combineChannels) - getDerivations(state, *i->value, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); - else if (getDerivation(state, *i->value, pathPrefix2, drvs, ignoreAssertionFailures)) { - /* If the value of this attribute is itself a set, - should we recurse into it? => Only if it has a - `recurseForDerivations = true' attribute. */ - if (i->value->type() == nAttrs) { - Bindings::iterator j = i->value->attrs->find(state.sRecurseForDerivations); - if (j != i->value->attrs->end() && state.forceBool(*j->value, j->pos, "while evaluating the attribute `recurseForDerivations`")) - getDerivations(state, *i->value, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); - } - } - } + bool shouldRecurse = getDerivation(state, v, pathPrefix, drvs, ignoreAssertionFailures); + if (!shouldRecurse) { + // We're done here. + return; } - else if (v.type() == nList) { + if (v.type() == nList) { // NOTE we can't really deduplicate here because small lists don't have stable addresses // and can cause spurious duplicate detections due to v being on the stack. for (auto [n, elem] : enumerate(v.listItems())) { - std::string pathPrefix2 = addToPath(pathPrefix, fmt("%d", n)); - if (getDerivation(state, *elem, pathPrefix2, drvs, ignoreAssertionFailures)) - getDerivations(state, *elem, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); + std::string joinedAttrPath = addToPath(pathPrefix, fmt("%d", n)); + bool shouldRecurse = getDerivation(state, *elem, joinedAttrPath, drvs, ignoreAssertionFailures); + if (shouldRecurse) { + getDerivations( + state, + *elem, + joinedAttrPath, + autoArgs, + drvs, + done, + ignoreAssertionFailures + ); + } } + + return; + } else if (v.type() != nAttrs) { + state.error<TypeError>( + "expression was expected to be a derivation or collection of derivations, but instead was %s", + showType(v.type(), true) + ).debugThrow(); + } + + /* Dont consider sets we've already seen, e.g. y in + `rec { x.d = derivation {...}; y = x; }`. */ + auto const &[_, didInsert] = done.insert(v.attrs); + if (!didInsert) { + return; } - else - state.error<TypeError>("expression does not evaluate to a derivation (or a set or list of those)").debugThrow(); + // FIXME: what the fuck??? + /* !!! undocumented hackery to support combining channels in + nix-env.cc. */ + bool combineChannels = v.attrs->find(state.symbols.create("_combineChannels")) != v.attrs->end(); + + /* Consider the attributes in sorted order to get more + deterministic behaviour in nix-env operations (e.g. when + there are names clashes between derivations, the derivation + bound to the attribute with the "lower" name should take + precedence). */ + for (auto & attr : v.attrs->lexicographicOrder(state.symbols)) { + debug("evaluating attribute '%1%'", state.symbols[attr->name]); + // FIXME: only consider attrs with identifier-like names?? Why??? + if (!std::regex_match(std::string(state.symbols[attr->name]), attrRegex)) { + continue; + } + std::string joinedAttrPath = addToPath(pathPrefix, state.symbols[attr->name]); + if (combineChannels) { + getDerivations(state, *attr->value, joinedAttrPath, autoArgs, drvs, done, ignoreAssertionFailures); + } else if (getDerivation(state, *attr->value, joinedAttrPath, drvs, ignoreAssertionFailures)) { + /* If the value of this attribute is itself a set, + should we recurse into it? => Only if it has a + `recurseForDerivations = true' attribute. */ + if (attr->value->type() == nAttrs) { + Attr * recurseForDrvs = attr->value->attrs->get(state.sRecurseForDerivations); + if (recurseForDrvs == nullptr) { + continue; + } + bool shouldRecurse = state.forceBool( + *recurseForDrvs->value, + attr->pos, + fmt("while evaluating the '%s' attribute", Magenta("recurseForDerivations")) + ); + if (!shouldRecurse) { + continue; + } + + getDerivations( + state, + *attr->value, + joinedAttrPath, + autoArgs, + drvs, + done, + ignoreAssertionFailures + ); + } + } + } } diff --git a/src/libexpr/get-drvs.hh b/src/libexpr/get-drvs.hh index 44aac3015..fd927b9e5 100644 --- a/src/libexpr/get-drvs.hh +++ b/src/libexpr/get-drvs.hh @@ -37,13 +37,14 @@ private: bool checkMeta(Value & v); + void fillOutputs(bool withPaths = true); + public: /** * path towards the derivation */ std::string attrPath; - DrvInfo(EvalState & state) : state(&state) { }; DrvInfo(EvalState & state, std::string attrPath, Bindings * attrs); DrvInfo(EvalState & state, ref<Store> store, const std::string & drvPathWithOutputs); diff --git a/src/libexpr/json-to-value.cc b/src/libexpr/json-to-value.cc index b69216a48..2309b6c97 100644 --- a/src/libexpr/json-to-value.cc +++ b/src/libexpr/json-to-value.cc @@ -82,28 +82,28 @@ class JSONSax : nlohmann::json_sax<json> { public: JSONSax(EvalState & state, Value & v) : state(state), rs(new JSONState(&v)) {}; - bool null() + bool null() override { rs->value(state).mkNull(); rs->add(); return true; } - bool boolean(bool val) + bool boolean(bool val) override { rs->value(state).mkBool(val); rs->add(); return true; } - bool number_integer(number_integer_t val) + bool number_integer(number_integer_t val) override { rs->value(state).mkInt(val); rs->add(); return true; } - bool number_unsigned(number_unsigned_t val_) + bool number_unsigned(number_unsigned_t val_) override { if (val_ > std::numeric_limits<NixInt::Inner>::max()) { throw Error("unsigned json number %1% outside of Nix integer range", val_); @@ -114,14 +114,14 @@ public: return true; } - bool number_float(number_float_t val, const string_t & s) + bool number_float(number_float_t val, const string_t & s) override { rs->value(state).mkFloat(val); rs->add(); return true; } - bool string(string_t & val) + bool string(string_t & val) override { rs->value(state).mkString(val); rs->add(); @@ -129,7 +129,7 @@ public: } #if NLOHMANN_JSON_VERSION_MAJOR >= 3 && NLOHMANN_JSON_VERSION_MINOR >= 8 - bool binary(binary_t&) + bool binary(binary_t&) override { // This function ought to be unreachable assert(false); @@ -137,35 +137,37 @@ public: } #endif - bool start_object(std::size_t len) + bool start_object(std::size_t len) override { rs = std::make_unique<JSONObjectState>(std::move(rs)); return true; } - bool key(string_t & name) + bool key(string_t & name) override { dynamic_cast<JSONObjectState*>(rs.get())->key(name, state); return true; } - bool end_object() { + bool end_object() override { rs = rs->resolve(state); rs->add(); return true; } - bool end_array() { + bool end_array() override { return end_object(); } - bool start_array(size_t len) { + bool start_array(size_t len) override { rs = std::make_unique<JSONListState>(std::move(rs), len != std::numeric_limits<size_t>::max() ? len : 128); return true; } - bool parse_error(std::size_t, const std::string&, const nlohmann::detail::exception& ex) { + bool + parse_error(std::size_t, const std::string &, const nlohmann::detail::exception & ex) override + { throw JSONParseError("%s", ex.what()); } }; diff --git a/src/libexpr/parser/parser.cc b/src/libexpr/parser/parser.cc index e60cf967f..a00586c36 100644 --- a/src/libexpr/parser/parser.cc +++ b/src/libexpr/parser/parser.cc @@ -43,7 +43,6 @@ error_message_for(p::one<'}'>) = "expecting '}'"; error_message_for(p::one<'"'>) = "expecting '\"'"; error_message_for(p::one<';'>) = "expecting ';'"; error_message_for(p::one<')'>) = "expecting ')'"; -error_message_for(p::one<'='>) = "expecting '='"; error_message_for(p::one<']'>) = "expecting ']'"; error_message_for(p::one<':'>) = "expecting ':'"; error_message_for(p::string<'\'', '\''>) = "expecting \"''\""; diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh index c35f88f8d..57485aa0a 100644 --- a/src/libexpr/value.hh +++ b/src/libexpr/value.hh @@ -3,6 +3,9 @@ #include <cassert> #include <climits> +#include <functional> +#include <ranges> +#include <span> #include "gc-alloc.hh" #include "symbol-table.hh" @@ -11,6 +14,7 @@ #include "source-path.hh" #include "print-options.hh" #include "checked-arithmetic.hh" +#include "concepts.hh" #include <nlohmann/json_fwd.hpp> @@ -132,6 +136,55 @@ class ExternalValueBase std::ostream & operator << (std::ostream & str, const ExternalValueBase & v); +extern ExprBlackHole eBlackHole; + +struct NewValueAs +{ + struct integer_t { }; + constexpr static integer_t integer{}; + + struct floating_t { }; + constexpr static floating_t floating{}; + + struct boolean_t { }; + constexpr static boolean_t boolean{}; + + struct string_t { }; + constexpr static string_t string{}; + + struct path_t { }; + constexpr static path_t path{}; + + struct list_t { }; + constexpr static list_t list{}; + + struct attrs_t { }; + constexpr static attrs_t attrs{}; + + struct thunk_t { }; + constexpr static thunk_t thunk{}; + + struct null_t { }; + constexpr static null_t null{}; + + struct app_t { }; + constexpr static app_t app{}; + + struct primop_t { }; + constexpr static primop_t primop{}; + + struct primOpApp_t { }; + constexpr static primOpApp_t primOpApp{}; + + struct lambda_t { }; + constexpr static lambda_t lambda{}; + + struct external_t { }; + constexpr static external_t external{}; + + struct blackhole_t { }; + constexpr static blackhole_t blackhole{}; +}; struct Value { @@ -142,6 +195,315 @@ private: public: + // Discount `using NewValueAs::*;` +#define USING_VALUETYPE(name) using name = NewValueAs::name + USING_VALUETYPE(integer_t); + USING_VALUETYPE(floating_t); + USING_VALUETYPE(boolean_t); + USING_VALUETYPE(string_t); + USING_VALUETYPE(path_t); + USING_VALUETYPE(list_t); + USING_VALUETYPE(attrs_t); + USING_VALUETYPE(thunk_t); + USING_VALUETYPE(primop_t); + USING_VALUETYPE(app_t); + USING_VALUETYPE(null_t); + USING_VALUETYPE(primOpApp_t); + USING_VALUETYPE(lambda_t); + USING_VALUETYPE(external_t); + USING_VALUETYPE(blackhole_t); +#undef USING_VALUETYPE + + /// Default constructor which is still used in the codebase but should not + /// be used in new code. Zero initializes its members. + [[deprecated]] Value() + : internalType(static_cast<InternalType>(0)) + , _empty{ 0, 0 } + { } + + /// Constructs a nix language value of type "int", with the integral value + /// of @ref i. + Value(integer_t, NixInt i) + : internalType(tInt) + , _empty{ 0, 0 } + { + // the NixInt ctor here is is special because NixInt has a ctor too, so + // we're not allowed to have it as an anonymous aggreagte member. we do + // however still have the option to clear the data members using _empty + // and leaving the second word of data cleared by setting only integer. + integer = i; + } + + /// Constructs a nix language value of type "float", with the floating + /// point value of @ref f. + Value(floating_t, NixFloat f) + : internalType(tFloat) + , fpoint(f) + , _float_pad(0) + { } + + /// Constructs a nix language value of type "bool", with the boolean + /// value of @ref b. + Value(boolean_t, bool b) + : internalType(tBool) + , boolean(b) + , _bool_pad(0) + { } + + /// Constructs a nix language value of type "string", with the value of the + /// C-string pointed to by @ref strPtr, and optionally with an array of + /// string context pointed to by @ref contextPtr. + /// + /// Neither the C-string nor the context array are copied; this constructor + /// assumes suitable memory has already been allocated (with the GC if + /// enabled), and string and context data copied into that memory. + Value(string_t, char const * strPtr, char const ** contextPtr = nullptr) + : internalType(tString) + , string({ .s = strPtr, .context = contextPtr }) + { } + + /// Constructx a nix language value of type "string", with a copy of the + /// string data viewed by @ref copyFrom. + /// + /// The string data *is* copied from @ref copyFrom, and this constructor + /// performs a dynamic (GC) allocation to do so. + Value(string_t, std::string_view copyFrom, NixStringContext const & context = {}) + : internalType(tString) + , string({ .s = gcCopyStringIfNeeded(copyFrom), .context = nullptr }) + { + if (context.empty()) { + // It stays nullptr. + return; + } + + // Copy the context. + this->string.context = gcAllocType<char const *>(context.size() + 1); + + size_t n = 0; + for (NixStringContextElem const & contextElem : context) { + this->string.context[n] = gcCopyStringIfNeeded(contextElem.to_string()); + n += 1; + } + + // Terminator sentinel. + this->string.context[n] = nullptr; + } + + /// Constructx a nix language value of type "string", with the value of the + /// C-string pointed to by @ref strPtr, and optionally with a set of string + /// context @ref context. + /// + /// The C-string is not copied; this constructor assumes suitable memory + /// has already been allocated (with the GC if enabled), and string data + /// has been copied into that memory. The context data *is* copied from + /// @ref context, and this constructor performs a dynamic (GC) allocation + /// to do so. + Value(string_t, char const * strPtr, NixStringContext const & context) + : internalType(tString) + , string({ .s = strPtr, .context = nullptr }) + { + if (context.empty()) { + // It stays nullptr + return; + } + + // Copy the context. + this->string.context = gcAllocType<char const *>(context.size() + 1); + + size_t n = 0; + for (NixStringContextElem const & contextElem : context) { + this->string.context[n] = gcCopyStringIfNeeded(contextElem.to_string()); + n += 1; + } + + // Terminator sentinel. + this->string.context[n] = nullptr; + } + + /// Constructs a nix language value of type "path", with the value of the + /// C-string pointed to by @ref strPtr. + /// + /// The C-string is not copied; this constructor assumes suitable memory + /// has already been allocated (with the GC if enabled), and string data + /// has been copied into that memory. + Value(path_t, char const * strPtr) + : internalType(tPath) + , _path(strPtr) + , _path_pad(0) + { } + + /// Constructs a nix language value of type "path", with the path + /// @ref path. + /// + /// The data from @ref path *is* copied, and this constructor performs a + /// dynamic (GC) allocation to do so. + Value(path_t, SourcePath const & path) + : internalType(tPath) + , _path(gcCopyStringIfNeeded(path.path.abs())) + , _path_pad(0) + { } + + /// Constructs a nix language value of type "list", with element array + /// @ref items. + /// + /// Generally, the data in @ref items is neither deep copied nor shallow + /// copied. This construct assumes the std::span @ref items is a region of + /// memory that has already been allocated (with the GC if enabled), and + /// an array of valid Value pointers has been copied into that memory. + /// + /// Howver, as an implementation detail, if @ref items is only 2 items or + /// smaller, the list is stored inline, and the Value pointers in + /// @ref items are shallow copied into this structure, without dynamically + /// allocating memory. + Value(list_t, std::span<Value *> items) + { + if (items.size() == 1) { + this->internalType = tList1; + this->smallList[0] = items[0]; + this->smallList[1] = nullptr; + } else if (items.size() == 2) { + this->internalType = tList2; + this->smallList[0] = items[0]; + this->smallList[1] = items[1]; + } else { + this->internalType = tListN; + this->bigList.size = items.size(); + this->bigList.elems = items.data(); + } + } + + /// Constructs a nix language value of type "list", with an element array + /// initialized by applying @ref transformer to each element in @ref items. + /// + /// This allows "in-place" construction of a nix list when some logic is + /// needed to get each Value pointer. This constructor dynamically (GC) + /// allocates memory for the size of @ref items, and the Value pointers + /// returned by @ref transformer are shallow copied into it. + template< + std::ranges::sized_range SizedIterableT, + InvocableR<Value *, typename SizedIterableT::value_type const &> TransformerT + > + Value(list_t, SizedIterableT & items, TransformerT const & transformer) + { + if (items.size() == 1) { + this->internalType = tList1; + this->smallList[0] = transformer(*items.begin()); + this->smallList[1] = nullptr; + } else if (items.size() == 2) { + this->internalType = tList2; + auto it = items.begin(); + this->smallList[0] = transformer(*it); + it++; + this->smallList[1] = transformer(*it); + } else { + this->internalType = tListN; + this->bigList.size = items.size(); + this->bigList.elems = gcAllocType<Value *>(items.size()); + auto it = items.begin(); + for (size_t i = 0; i < items.size(); i++, it++) { + this->bigList.elems[i] = transformer(*it); + } + } + } + + /// Constructs a nix language value of the singleton type "null". + Value(null_t) + : internalType(tNull) + , _empty{0, 0} + { } + + /// Constructs a nix language value of type "set", with the attribute + /// bindings pointed to by @ref bindings. + /// + /// The bindings are not not copied; this constructor assumes @ref bindings + /// has already been suitably allocated by something like nix::buildBindings. + Value(attrs_t, Bindings * bindings) + : internalType(tAttrs) + , attrs(bindings) + , _attrs_pad(0) + { } + + /// Constructs a nix language lazy delayed computation, or "thunk". + /// + /// The thunk stores the environment it will be computed in @ref env, and + /// the expression that will need to be evaluated @ref expr. + Value(thunk_t, Env & env, Expr & expr) + : internalType(tThunk) + , thunk({ .env = &env, .expr = &expr }) + { } + + /// Constructs a nix language value of type "lambda", which represents + /// a builtin, primitive operation ("primop"), from the primop + /// implemented by @ref primop. + Value(primop_t, PrimOp & primop); + + /// Constructs a nix language value of type "lambda", which represents a + /// partially applied primop. + Value(primOpApp_t, Value & lhs, Value & rhs) + : internalType(tPrimOpApp) + , primOpApp({ .left = &lhs, .right = &rhs }) + { } + + /// Constructs a nix language value of type "lambda", which represents a + /// lazy partial application of another lambda. + Value(app_t, Value & lhs, Value & rhs) + : internalType(tApp) + , app({ .left = &lhs, .right = &rhs }) + { } + + /// Constructs a nix language value of type "external", which is only used + /// by plugins. Do any existing plugins even use this mechanism? + Value(external_t, ExternalValueBase & external) + : internalType(tExternal) + , external(&external) + , _external_pad(0) + { } + + /// Constructs a nix language value of type "lambda", which represents a + /// run of the mill lambda defined in nix code. + /// + /// This takes the environment the lambda is closed over @ref env, and + /// the lambda expression itself @ref lambda, which will not be evaluated + /// until it is applied. + Value(lambda_t, Env & env, ExprLambda & lambda) + : internalType(tLambda) + , lambda({ .env = &env, .fun = &lambda }) + { } + + /// Constructs an evil thunk, whose evaluation represents infinite recursion. + explicit Value(blackhole_t) + : internalType(tThunk) + , thunk({ .env = nullptr, .expr = reinterpret_cast<Expr *>(&eBlackHole) }) + { } + + Value(Value const & rhs) = default; + + /// Move constructor. Does the same thing as the copy constructor, but + /// also zeroes out the other Value. + Value(Value && rhs) + : internalType(rhs.internalType) + , _empty{ 0, 0 } + { + *this = std::move(rhs); + } + + Value & operator=(Value const & rhs) = default; + + /// Move assignment operator. + /// Does the same thing as the copy assignment operator, but also zeroes out + /// the rhs. + inline Value & operator=(Value && rhs) + { + *this = static_cast<const Value &>(rhs); + if (this != &rhs) { + // Kill `rhs`, because non-destructive move lol. + rhs.internalType = static_cast<InternalType>(0); + rhs._empty[0] = 0; + rhs._empty[1] = 0; + } + return *this; + } + void print(EvalState &state, std::ostream &str, PrintOptions options = PrintOptions {}); // Functions needed to distinguish the type @@ -160,8 +522,15 @@ public: union { + /// Dummy field, which takes up as much space as the largest union variants + /// to set the union's memory to zeroed memory. + uintptr_t _empty[2]; + NixInt integer; - bool boolean; + struct { + bool boolean; + uintptr_t _bool_pad; + }; /** * Strings in the evaluator carry a so-called `context` which @@ -190,8 +559,14 @@ public: const char * * context; // must be in sorted order } string; - const char * _path; - Bindings * attrs; + struct { + const char * _path; + uintptr_t _path_pad; + }; + struct { + Bindings * attrs; + uintptr_t _attrs_pad; + }; struct { size_t size; Value * * elems; @@ -208,12 +583,21 @@ public: Env * env; ExprLambda * fun; } lambda; - PrimOp * primOp; + struct { + PrimOp * primOp; + uintptr_t _primop_pad; + }; struct { Value * left, * right; } primOpApp; - ExternalValueBase * external; - NixFloat fpoint; + struct { + ExternalValueBase * external; + uintptr_t _external_pad; + }; + struct { + NixFloat fpoint; + uintptr_t _float_pad; + }; }; /** @@ -449,8 +833,6 @@ public: }; -extern ExprBlackHole eBlackHole; - bool Value::isBlackhole() const { return internalType == tThunk && thunk.expr == (Expr*) &eBlackHole; diff --git a/src/libfetchers/fetchers.hh b/src/libfetchers/fetchers.hh index 2bb4248be..40f2b6294 100644 --- a/src/libfetchers/fetchers.hh +++ b/src/libfetchers/fetchers.hh @@ -159,6 +159,37 @@ struct InputScheme std::optional<std::string> commitMsg) const; virtual std::pair<StorePath, Input> fetch(ref<Store> store, const Input & input) = 0; + +protected: + void emplaceURLQueryIntoAttrs( + const ParsedURL & parsedURL, + Attrs & attrs, + const StringSet & numericParams, + const StringSet & booleanParams) const + { + for (auto &[name, value] : parsedURL.query) { + if (name == "url") { + throw BadURL( + "URL '%s' must not override url via query param!", + parsedURL.to_string() + ); + } else if (numericParams.count(name) != 0) { + if (auto n = string2Int<uint64_t>(value)) { + attrs.insert_or_assign(name, *n); + } else { + throw BadURL( + "URL '%s' has non-numeric parameter '%s'", + parsedURL.to_string(), + name + ); + } + } else if (booleanParams.count(name) != 0) { + attrs.emplace(name, Explicit<bool> { value == "1" }); + } else { + attrs.emplace(name, value); + } + } + } }; void registerInputScheme(std::shared_ptr<InputScheme> && fetcher); diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index 81c579e84..8e3165ff6 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -273,18 +273,15 @@ struct GitInputScheme : InputScheme Attrs attrs; attrs.emplace("type", "git"); - - for (auto & [name, value] : url.query) { - if (name == "rev" || name == "ref") - attrs.emplace(name, value); - else if (name == "shallow" || name == "submodules" || name == "allRefs") - attrs.emplace(name, Explicit<bool> { value == "1" }); - else - url2.query.emplace(name, value); - } - attrs.emplace("url", url2.to_string()); + emplaceURLQueryIntoAttrs( + url, + attrs, + {"lastModified", "revCount"}, + {"shallow", "submodules", "allRefs"} + ); + return inputFromAttrs(attrs); } @@ -695,7 +692,7 @@ struct GitInputScheme : InputScheme }); Finally const _wait([&] { proc.wait(); }); - unpackTarfile(*proc.stdout(), tmpDir); + unpackTarfile(*proc.getStdout(), tmpDir); } auto storePath = store->addToStore(name, tmpDir, FileIngestionMethod::Recursive, htSHA256, filter); diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index 60fefd1f3..b971781ae 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -1,3 +1,4 @@ +#include "attrs.hh" #include "filetransfer.hh" #include "cache.hh" #include "globals.hh" @@ -36,18 +37,11 @@ struct GitArchiveInputScheme : InputScheme auto path = tokenizeString<std::vector<std::string>>(url.path, "/"); - std::optional<Hash> rev; - std::optional<std::string> ref; - std::optional<std::string> host_url; + std::optional<std::string> refOrRev; auto size = path.size(); if (size == 3) { - if (std::regex_match(path[2], revRegex)) - rev = Hash::parseAny(path[2], htSHA1); - else if (std::regex_match(path[2], refRegex)) - ref = path[2]; - else - throw BadURL("in URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[2]); + refOrRev = path[2]; } else if (size > 3) { std::string rs; for (auto i = std::next(path.begin(), 2); i != path.end(); i++) { @@ -58,61 +52,91 @@ struct GitArchiveInputScheme : InputScheme } if (std::regex_match(rs, refRegex)) { - ref = rs; + refOrRev = rs; } else { throw BadURL("in URL '%s', '%s' is not a branch/tag name", url.url, rs); } } else if (size < 2) throw BadURL("URL '%s' is invalid", url.url); + Attrs attrs; + attrs.emplace("type", type()); + attrs.emplace("owner", path[0]); + attrs.emplace("repo", path[1]); + for (auto &[name, value] : url.query) { - if (name == "rev") { - if (rev) - throw BadURL("URL '%s' contains multiple commit hashes", url.url); - rev = Hash::parseAny(value, htSHA1); - } - else if (name == "ref") { - if (!std::regex_match(value, refRegex)) - throw BadURL("URL '%s' contains an invalid branch/tag name", url.url); - if (ref) - throw BadURL("URL '%s' contains multiple branch/tag names", url.url); - ref = value; - } - else if (name == "host") { - if (!std::regex_match(value, hostRegex)) - throw BadURL("URL '%s' contains an invalid instance host", url.url); - host_url = value; + if (name == "rev" || name == "ref") { + if (refOrRev) { + throw BadURL("URL '%s' already contains a ref or rev", url.url); + } else { + refOrRev = value; + } + } else if (name == "lastModified") { + if (auto n = string2Int<uint64_t>(value)) { + attrs.emplace(name, *n); + } else { + throw Error( + "Attribute 'lastModified' in URL '%s' must be an integer", + url.to_string() + ); + } + } else { + attrs.emplace(name, value); } - // FIXME: barf on unsupported attributes } - if (ref && rev) - throw BadURL("URL '%s' contains both a commit hash and a branch/tag name %s %s", url.url, *ref, rev->gitRev()); + if (refOrRev) attrs.emplace("refOrRev", *refOrRev); - Input input; - input.attrs.insert_or_assign("type", type()); - input.attrs.insert_or_assign("owner", path[0]); - input.attrs.insert_or_assign("repo", path[1]); - if (rev) input.attrs.insert_or_assign("rev", rev->gitRev()); - if (ref) input.attrs.insert_or_assign("ref", *ref); - if (host_url) input.attrs.insert_or_assign("host", *host_url); - - return input; + return inputFromAttrs(attrs); } std::optional<Input> inputFromAttrs(const Attrs & attrs) const override { - if (maybeGetStrAttr(attrs, "type") != type()) return {}; + // Attributes can contain refOrRev and it needs to be figured out + // which one it is (see inputFromURL for when that may happen). + // The correct one (ref or rev) will be written into finalAttrs and + // it needs to be mutable for that. + Attrs finalAttrs(attrs); + auto type_ = maybeGetStrAttr(finalAttrs, "type"); + if (type_ != type()) return {}; + + auto owner = getStrAttr(finalAttrs, "owner"); + auto repo = getStrAttr(finalAttrs, "repo"); + + auto url = fmt("%s:%s/%s", *type_, owner, repo); + if (auto host = maybeGetStrAttr(finalAttrs, "host")) { + if (!std::regex_match(*host, hostRegex)) { + throw BadURL("URL '%s' contains an invalid instance host", url); + } + } - for (auto & [name, value] : attrs) - if (name != "type" && name != "owner" && name != "repo" && name != "ref" && name != "rev" && name != "narHash" && name != "lastModified" && name != "host") - throw Error("unsupported input attribute '%s'", name); + if (auto refOrRev = maybeGetStrAttr(finalAttrs, "refOrRev")) { + finalAttrs.erase("refOrRev"); + if (std::regex_match(*refOrRev, revRegex)) { + finalAttrs.emplace("rev", *refOrRev); + } else if (std::regex_match(*refOrRev, refRegex)) { + finalAttrs.emplace("ref", *refOrRev); + } else { + throw Error( + "in URL '%s', '%s' is not a commit hash or a branch/tag name", + url, + *refOrRev + ); + } + } else if (auto ref = maybeGetStrAttr(finalAttrs, "ref")) { + if (!std::regex_match(*ref, refRegex)) { + throw BadURL("URL '%s' contains an invalid branch/tag name", url); + } + } - getStrAttr(attrs, "owner"); - getStrAttr(attrs, "repo"); + for (auto & [name, value] : finalAttrs) { + if (name != "type" && name != "owner" && name != "repo" && name != "ref" && name != "rev" && name != "narHash" && name != "lastModified" && name != "host") { + throw Error("unsupported input attribute '%s'", name); + } + } Input input; - input.attrs = attrs; + input.attrs = finalAttrs; return input; } diff --git a/src/libfetchers/indirect.cc b/src/libfetchers/indirect.cc index c73505b31..8c0176e84 100644 --- a/src/libfetchers/indirect.cc +++ b/src/libfetchers/indirect.cc @@ -17,6 +17,8 @@ struct IndirectInputScheme : InputScheme std::optional<Hash> rev; std::optional<std::string> ref; + Attrs attrs; + if (path.size() == 1) { } else if (path.size() == 2) { if (std::regex_match(path[1], revRegex)) @@ -26,29 +28,21 @@ struct IndirectInputScheme : InputScheme else throw BadURL("in flake URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[1]); } else if (path.size() == 3) { - if (!std::regex_match(path[1], refRegex)) - throw BadURL("in flake URL '%s', '%s' is not a branch/tag name", url.url, path[1]); ref = path[1]; - if (!std::regex_match(path[2], revRegex)) - throw BadURL("in flake URL '%s', '%s' is not a commit hash", url.url, path[2]); rev = Hash::parseAny(path[2], htSHA1); } else throw BadURL("GitHub URL '%s' is invalid", url.url); std::string id = path[0]; - if (!std::regex_match(id, flakeRegex)) - throw BadURL("'%s' is not a valid flake ID", id); - // FIXME: forbid query params? + attrs.emplace("type", "indirect"); + attrs.emplace("id", id); + if (rev) attrs.emplace("rev", rev->gitRev()); + if (ref) attrs.emplace("ref", *ref); - Input input; - input.direct = false; - input.attrs.insert_or_assign("type", "indirect"); - input.attrs.insert_or_assign("id", id); - if (rev) input.attrs.insert_or_assign("rev", rev->gitRev()); - if (ref) input.attrs.insert_or_assign("ref", *ref); + emplaceURLQueryIntoAttrs(url, attrs, {}, {}); - return input; + return inputFromAttrs(attrs); } std::optional<Input> inputFromAttrs(const Attrs & attrs) const override @@ -63,6 +57,18 @@ struct IndirectInputScheme : InputScheme if (!std::regex_match(id, flakeRegex)) throw BadURL("'%s' is not a valid flake ID", id); + // TODO come up with a nicer error message for those two. + if (auto rev = maybeGetStrAttr(attrs, "rev")) { + if (!std::regex_match(*rev, revRegex)) { + throw BadURL("in flake '%s', '%s' is not a commit hash", id, *rev); + } + } + if (auto ref = maybeGetStrAttr(attrs, "ref")) { + if (!std::regex_match(*ref, refRegex)) { + throw BadURL("in flake '%s', '%s' is not a valid branch/tag name", id, *ref); + } + } + Input input; input.direct = false; input.attrs = attrs; diff --git a/src/libfetchers/mercurial.cc b/src/libfetchers/mercurial.cc index 23cf7b51d..b4150e9df 100644 --- a/src/libfetchers/mercurial.cc +++ b/src/libfetchers/mercurial.cc @@ -56,12 +56,7 @@ struct MercurialInputScheme : InputScheme Attrs attrs; attrs.emplace("type", "hg"); - for (auto &[name, value] : url.query) { - if (name == "rev" || name == "ref") - attrs.emplace(name, value); - else - url2.query.emplace(name, value); - } + emplaceURLQueryIntoAttrs(url, attrs, {"revCount"}, {}); attrs.emplace("url", url2.to_string()); diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index 6ce35aeb2..b11665805 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -201,29 +201,17 @@ struct CurlInputScheme : InputScheme if (!isValidURL(_url, requireTree)) return std::nullopt; - Input input; - auto url = _url; - url.scheme = parseUrlScheme(url.scheme).transport; - - auto narHash = url.query.find("narHash"); - if (narHash != url.query.end()) - input.attrs.insert_or_assign("narHash", narHash->second); + Attrs attrs; + attrs.emplace("type", inputType()); - if (auto i = get(url.query, "rev")) - input.attrs.insert_or_assign("rev", *i); - - if (auto i = get(url.query, "revCount")) - if (auto n = string2Int<uint64_t>(*i)) - input.attrs.insert_or_assign("revCount", *n); + url.scheme = parseUrlScheme(url.scheme).transport; - url.query.erase("rev"); - url.query.erase("revCount"); + emplaceURLQueryIntoAttrs(url, attrs, {"revCount"}, {}); - input.attrs.insert_or_assign("type", inputType()); - input.attrs.insert_or_assign("url", url.to_string()); - return input; + attrs.emplace("url", url.to_string()); + return inputFromAttrs(attrs); } std::optional<Input> inputFromAttrs(const Attrs & attrs) const override @@ -235,7 +223,7 @@ struct CurlInputScheme : InputScheme std::set<std::string> allowedNames = {"type", "url", "narHash", "name", "unpack", "rev", "revCount", "lastModified"}; for (auto & [name, value] : attrs) if (!allowedNames.count(name)) - throw Error("unsupported %s input attribute '%s'", *type, name); + throw Error("unsupported %s input attribute '%s'. If you wanted to fetch a tarball with a query parameter, please use '{ type = \"tarball\"; url = \"...\"; }'", *type, name); Input input; input.attrs = attrs; diff --git a/src/libstore/build-result.cc b/src/libstore/build-result.cc index 18f519c5c..de280c13f 100644 --- a/src/libstore/build-result.cc +++ b/src/libstore/build-result.cc @@ -15,4 +15,27 @@ GENERATE_CMP_EXT( me->cpuUser, me->cpuSystem); +KeyedBuildResult BuildResult::restrictTo(DerivedPath path) const +{ + KeyedBuildResult res{*this, std::move(path)}; + + if (auto pbp = std::get_if<DerivedPath::Built>(&res.path)) { + auto & bp = *pbp; + + /* Because goals are in general shared between derived paths + that share the same derivation, we need to filter their + results to get back just the results we care about. + */ + + for (auto it = res.builtOutputs.begin(); it != res.builtOutputs.end();) { + if (bp.outputs.contains(it->first)) + ++it; + else + it = res.builtOutputs.erase(it); + } + } + + return res; +} + } diff --git a/src/libstore/build-result.hh b/src/libstore/build-result.hh index 8840fa7e3..9634fb944 100644 --- a/src/libstore/build-result.hh +++ b/src/libstore/build-result.hh @@ -11,6 +11,8 @@ namespace nix { +struct KeyedBuildResult; + struct BuildResult { /** @@ -112,6 +114,18 @@ struct BuildResult { throw Error("%s", errorMsg); } + + /** + * Project a BuildResult with just the information that pertains to + * the given path. + * + * A `BuildResult` may hold information for multiple derived paths; + * this function discards information about outputs not relevant in + * `path`. Build `Goal`s in particular may contain more outputs for + * a single build result than asked for directly, it's necessary to + * remove any such additional result to not leak other build infos. + */ + KeyedBuildResult restrictTo(DerivedPath path) const; }; /** diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 7cf8a55c9..38c54e854 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -79,7 +79,6 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, trace("created"); mcExpectedBuilds = std::make_unique<MaintainCount<uint64_t>>(worker.expectedBuilds); - worker.updateProgress(); } @@ -100,7 +99,6 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, const BasicDerivation trace("created"); mcExpectedBuilds = std::make_unique<MaintainCount<uint64_t>>(worker.expectedBuilds); - worker.updateProgress(); /* Prevent the .chroot directory from being garbage-collected. (See isActiveTempFile() in gc.cc.) */ @@ -670,7 +668,6 @@ void DerivationGoal::started() act = std::make_unique<Activity>(*logger, lvlInfo, actBuild, msg, Logger::Fields{worker.store.printStorePath(drvPath), hook ? machineName : "", 1, 1}); mcRunningBuilds = std::make_unique<MaintainCount<uint64_t>>(worker.runningBuilds); - worker.updateProgress(); } void DerivationGoal::tryToBuild() @@ -932,7 +929,7 @@ void runPostBuildHook( Finally const _wait([&] { proc.wait(); }); // FIXME just process the data, without a wrapper sink class - proc.stdout()->drainInto(sink); + proc.getStdout()->drainInto(sink); } void DerivationGoal::buildDone() @@ -1366,7 +1363,6 @@ void DerivationGoal::handleChildOutput(int fd, std::string_view data) void DerivationGoal::handleEOF(int fd) { if (!currentLogLine.empty()) flushLine(); - worker.wakeUp(shared_from_this()); } @@ -1537,8 +1533,6 @@ void DerivationGoal::done( worker.failedBuilds++; } - worker.updateProgress(); - auto traceBuiltOutputsFile = getEnv("_NIX_TRACE_BUILT_OUTPUTS").value_or(""); if (traceBuiltOutputsFile != "") { std::fstream fs; @@ -1566,7 +1560,7 @@ void DerivationGoal::waiteeDone(GoalPtr waitee, ExitCode result) auto & outputs = nodeP->value; for (auto & outputName : outputs) { - auto buildResult = dg->getBuildResult(DerivedPath::Built { + auto buildResult = dg->buildResult.restrictTo(DerivedPath::Built { .drvPath = makeConstantStorePathRef(dg->drvPath), .outputs = OutputsSpec::Names { outputName }, }); diff --git a/src/libstore/build/drv-output-substitution-goal.cc b/src/libstore/build/drv-output-substitution-goal.cc index 735a07f96..94c9206a3 100644 --- a/src/libstore/build/drv-output-substitution-goal.cc +++ b/src/libstore/build/drv-output-substitution-goal.cc @@ -41,14 +41,13 @@ void DrvOutputSubstitutionGoal::tryNext() /* Make sure that we are allowed to start a substitution. Note that even if maxSubstitutionJobs == 0, we still allow a substituter to run. This prevents infinite waiting. */ - if (worker.runningCASubstitutions >= std::max(1U, settings.maxSubstitutionJobs.get())) { + if (worker.runningSubstitutions >= std::max(1U, settings.maxSubstitutionJobs.get())) { worker.waitForBuildSlot(shared_from_this()); return; } maintainRunningSubstitutions = - std::make_unique<MaintainCount<uint64_t>>(worker.runningCASubstitutions); - worker.updateProgress(); + std::make_unique<MaintainCount<uint64_t>>(worker.runningSubstitutions); if (subs.size() == 0) { /* None left. Terminate this goal and let someone else deal @@ -62,7 +61,6 @@ void DrvOutputSubstitutionGoal::tryNext() if (substituterFailed) { worker.failedSubstitutions++; - worker.updateProgress(); } return; @@ -164,10 +162,5 @@ void DrvOutputSubstitutionGoal::work() (this->*state)(); } -void DrvOutputSubstitutionGoal::handleEOF(int fd) -{ - if (fd == downloadState->outPipe.readSide.get()) worker.wakeUp(shared_from_this()); -} - } diff --git a/src/libstore/build/drv-output-substitution-goal.hh b/src/libstore/build/drv-output-substitution-goal.hh index 008b211a1..598b119dc 100644 --- a/src/libstore/build/drv-output-substitution-goal.hh +++ b/src/libstore/build/drv-output-substitution-goal.hh @@ -4,7 +4,6 @@ #include "store-api.hh" #include "goal.hh" #include "realisation.hh" -#include <thread> #include <future> namespace nix { @@ -41,11 +40,6 @@ class DrvOutputSubstitutionGoal : public Goal { */ std::shared_ptr<Store> sub; - /** - * The substituter thread. - */ - std::thread thr; - std::unique_ptr<MaintainCount<uint64_t>> maintainRunningSubstitutions; struct DownloadState @@ -78,7 +72,6 @@ public: std::string key() override; void work() override; - void handleEOF(int fd) override; JobCategory jobCategory() const override { return JobCategory::Substitution; diff --git a/src/libstore/build/entry-points.cc b/src/libstore/build/entry-points.cc index d4bead28e..c6955600e 100644 --- a/src/libstore/build/entry-points.cc +++ b/src/libstore/build/entry-points.cc @@ -22,7 +22,7 @@ void Store::buildPaths(const std::vector<DerivedPath> & reqs, BuildMode buildMod if (ex) logError(i->ex->info()); else - ex = std::move(i->ex); + ex = std::move(*i->ex); } if (i->exitCode != Goal::ecSuccess) { if (auto i2 = dynamic_cast<DerivationGoal *>(i.get())) @@ -62,10 +62,7 @@ std::vector<KeyedBuildResult> Store::buildPathsWithResults( std::vector<KeyedBuildResult> results; for (auto & [req, goalPtr] : state) - results.emplace_back(KeyedBuildResult { - goalPtr->getBuildResult(req), - /* .path = */ req, - }); + results.emplace_back(goalPtr->buildResult.restrictTo(req)); return results; } @@ -78,7 +75,7 @@ BuildResult Store::buildDerivation(const StorePath & drvPath, const BasicDerivat try { worker.run(Goals{goal}); - return goal->getBuildResult(DerivedPath::Built { + return goal->buildResult.restrictTo(DerivedPath::Built { .drvPath = makeConstantStorePathRef(drvPath), .outputs = OutputsSpec::All {}, }); diff --git a/src/libstore/build/goal.cc b/src/libstore/build/goal.cc index f8db98280..4db6af6e6 100644 --- a/src/libstore/build/goal.cc +++ b/src/libstore/build/goal.cc @@ -11,41 +11,10 @@ bool CompareGoalPtrs::operator() (const GoalPtr & a, const GoalPtr & b) const { } -BuildResult Goal::getBuildResult(const DerivedPath & req) const { - BuildResult res { buildResult }; - - if (auto pbp = std::get_if<DerivedPath::Built>(&req)) { - auto & bp = *pbp; - - /* Because goals are in general shared between derived paths - that share the same derivation, we need to filter their - results to get back just the results we care about. - */ - - for (auto it = res.builtOutputs.begin(); it != res.builtOutputs.end();) { - if (bp.outputs.contains(it->first)) - ++it; - else - it = res.builtOutputs.erase(it); - } - } - - return res; -} - - -void addToWeakGoals(WeakGoals & goals, GoalPtr p) -{ - if (goals.find(p) != goals.end()) - return; - goals.insert(p); -} - - void Goal::addWaitee(GoalPtr waitee) { waitees.insert(waitee); - addToWeakGoals(waitee->waiters, shared_from_this()); + waitee->waiters.insert(shared_from_this()); } @@ -79,15 +48,14 @@ void Goal::waiteeDone(GoalPtr waitee, ExitCode result) void Goal::amDone(ExitCode result, std::optional<Error> ex) { trace("done"); - assert(exitCode == ecBusy); - assert(result == ecSuccess || result == ecFailed || result == ecNoSubstituters || result == ecIncompleteClosure); + assert(!exitCode.has_value()); exitCode = result; if (ex) { if (!waiters.empty()) logError(ex->info()); else - this->ex = std::move(*ex); + this->ex = std::make_unique<Error>(std::move(*ex)); } for (auto & i : waiters) { diff --git a/src/libstore/build/goal.hh b/src/libstore/build/goal.hh index 5b755c275..575621037 100644 --- a/src/libstore/build/goal.hh +++ b/src/libstore/build/goal.hh @@ -53,7 +53,7 @@ enum struct JobCategory { struct Goal : public std::enable_shared_from_this<Goal> { - typedef enum {ecBusy, ecSuccess, ecFailed, ecNoSubstituters, ecIncompleteClosure} ExitCode; + typedef enum {ecSuccess, ecFailed, ecNoSubstituters, ecIncompleteClosure} ExitCode; /** * Backlink to the worker. @@ -96,9 +96,8 @@ struct Goal : public std::enable_shared_from_this<Goal> /** * Whether the goal is finished. */ - ExitCode exitCode = ecBusy; + std::optional<ExitCode> exitCode; -protected: /** * Build result. */ @@ -107,21 +106,9 @@ protected: public: /** - * Project a `BuildResult` with just the information that pertains - * to the given request. - * - * In general, goals may be aliased between multiple requests, and - * the stored `BuildResult` has information for the union of all - * requests. We don't want to leak what the other request are for - * sake of both privacy and determinism, and this "safe accessor" - * ensures we don't. - */ - BuildResult getBuildResult(const DerivedPath &) const; - - /** * Exception containing an error message, if any. */ - std::optional<Error> ex; + std::unique_ptr<Error> ex; Goal(Worker & worker, DerivedPath path) : worker(worker) @@ -145,7 +132,6 @@ public: virtual void handleEOF(int fd) { - abort(); } void trace(std::string_view s); @@ -175,6 +161,4 @@ public: virtual JobCategory jobCategory() const = 0; }; -void addToWeakGoals(WeakGoals & goals, GoalPtr p); - } diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 660512e49..2f1f338c1 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -45,7 +45,6 @@ #include <sys/prctl.h> #include <sys/syscall.h> #if HAVE_SECCOMP -#include "linux/fchmodat2-compat.hh" #include <seccomp.h> #endif #define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) @@ -976,7 +975,7 @@ bool LocalDerivationGoal::isAllowed(const DerivedPath & req) struct RestrictedStoreConfig : virtual LocalFSStoreConfig { using LocalFSStoreConfig::LocalFSStoreConfig; - const std::string name() { return "Restricted Store"; } + const std::string name() override { return "Restricted Store"; } }; /* A wrapper around LocalStore that only allows building/querying of @@ -1363,6 +1362,20 @@ void LocalDerivationGoal::chownToBuilder(const Path & path) throw SysError("cannot change ownership of '%1%'", path); } +#if HAVE_SECCOMP + +static void allowSyscall(scmp_filter_ctx ctx, int syscall) { + if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscall, 0) != 0) + throw SysError("unable to add seccomp rule"); +} + +#define ALLOW_CHMOD_IF_SAFE(ctx, syscall, modePos) \ + if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscall, 1, SCMP_A##modePos(SCMP_CMP_MASKED_EQ, S_ISUID | S_ISGID, 0)) != 0 || \ + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), syscall, 1, SCMP_A##modePos(SCMP_CMP_MASKED_EQ, S_ISUID, S_ISUID)) != 0 || \ + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), syscall, 1, SCMP_A##modePos(SCMP_CMP_MASKED_EQ, S_ISGID, S_ISGID)) != 0) \ + throw SysError("unable to add seccomp rule"); + +#endif void setupSeccomp() { @@ -1370,7 +1383,9 @@ void setupSeccomp() #if HAVE_SECCOMP scmp_filter_ctx ctx; - if (!(ctx = seccomp_init(SCMP_ACT_ALLOW))) + // Pretend that syscalls we don't yet know about don't exist. + // This is the best option for compatibility: after all, they did in fact not exist not too long ago. + if (!(ctx = seccomp_init(SCMP_ACT_ERRNO(ENOSYS)))) throw SysError("unable to initialize seccomp mode 2"); Finally cleanup([&]() { @@ -1405,28 +1420,514 @@ void setupSeccomp() seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL64N32) != 0) printError("unable to add mips64el-*abin32 seccomp architecture"); - /* Prevent builders from creating setuid/setgid binaries. */ - for (int perm : { S_ISUID, S_ISGID }) { - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(chmod), 1, - SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmod), 1, - SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmodat), 1, - SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), NIX_SYSCALL_FCHMODAT2, 1, - SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - } - - /* Prevent builders from creating EAs or ACLs. Not all filesystems - support these, and they're not allowed in the Nix store because - they're not representable in the NAR serialisation. */ + // This list is intended for machine consumption. + // Please keep its format, order and BEGIN/END markers. + // + // Currently, it is up to date with libseccomp 2.5.5 and glibc 2.39. + // Run check-syscalls to determine which new syscalls should be added. + // New syscalls must be audited and handled in a way that blocks the following dangerous operations: + // * Creation of non-empty setuid/setgid files + // * Creation of extended attributes (including ACLs) + // + // BEGIN extract-syscalls + allowSyscall(ctx, SCMP_SYS(accept)); + allowSyscall(ctx, SCMP_SYS(accept4)); + allowSyscall(ctx, SCMP_SYS(access)); + allowSyscall(ctx, SCMP_SYS(acct)); + allowSyscall(ctx, SCMP_SYS(add_key)); + allowSyscall(ctx, SCMP_SYS(adjtimex)); + allowSyscall(ctx, SCMP_SYS(afs_syscall)); + allowSyscall(ctx, SCMP_SYS(alarm)); + allowSyscall(ctx, SCMP_SYS(arch_prctl)); + allowSyscall(ctx, SCMP_SYS(arm_fadvise64_64)); + allowSyscall(ctx, SCMP_SYS(arm_sync_file_range)); + allowSyscall(ctx, SCMP_SYS(bdflush)); + allowSyscall(ctx, SCMP_SYS(bind)); + allowSyscall(ctx, SCMP_SYS(bpf)); + allowSyscall(ctx, SCMP_SYS(break)); + allowSyscall(ctx, SCMP_SYS(breakpoint)); + allowSyscall(ctx, SCMP_SYS(brk)); + allowSyscall(ctx, SCMP_SYS(cachectl)); + allowSyscall(ctx, SCMP_SYS(cacheflush)); + allowSyscall(ctx, SCMP_SYS(cachestat)); + allowSyscall(ctx, SCMP_SYS(capget)); + allowSyscall(ctx, SCMP_SYS(capset)); + allowSyscall(ctx, SCMP_SYS(chdir)); + // skip chmod (dangerous) + allowSyscall(ctx, SCMP_SYS(chown)); + allowSyscall(ctx, SCMP_SYS(chown32)); + allowSyscall(ctx, SCMP_SYS(chroot)); + allowSyscall(ctx, SCMP_SYS(clock_adjtime)); + allowSyscall(ctx, SCMP_SYS(clock_adjtime64)); + allowSyscall(ctx, SCMP_SYS(clock_getres)); + allowSyscall(ctx, SCMP_SYS(clock_getres_time64)); + allowSyscall(ctx, SCMP_SYS(clock_gettime)); + allowSyscall(ctx, SCMP_SYS(clock_gettime64)); + allowSyscall(ctx, SCMP_SYS(clock_nanosleep)); + allowSyscall(ctx, SCMP_SYS(clock_nanosleep_time64)); + allowSyscall(ctx, SCMP_SYS(clock_settime)); + allowSyscall(ctx, SCMP_SYS(clock_settime64)); + allowSyscall(ctx, SCMP_SYS(clone)); + allowSyscall(ctx, SCMP_SYS(clone3)); + allowSyscall(ctx, SCMP_SYS(close)); + allowSyscall(ctx, SCMP_SYS(close_range)); + allowSyscall(ctx, SCMP_SYS(connect)); + allowSyscall(ctx, SCMP_SYS(copy_file_range)); + allowSyscall(ctx, SCMP_SYS(creat)); + allowSyscall(ctx, SCMP_SYS(create_module)); + allowSyscall(ctx, SCMP_SYS(delete_module)); + allowSyscall(ctx, SCMP_SYS(dup)); + allowSyscall(ctx, SCMP_SYS(dup2)); + allowSyscall(ctx, SCMP_SYS(dup3)); + allowSyscall(ctx, SCMP_SYS(epoll_create)); + allowSyscall(ctx, SCMP_SYS(epoll_create1)); + allowSyscall(ctx, SCMP_SYS(epoll_ctl)); + allowSyscall(ctx, SCMP_SYS(epoll_ctl_old)); + allowSyscall(ctx, SCMP_SYS(epoll_pwait)); + allowSyscall(ctx, SCMP_SYS(epoll_pwait2)); + allowSyscall(ctx, SCMP_SYS(epoll_wait)); + allowSyscall(ctx, SCMP_SYS(epoll_wait_old)); + allowSyscall(ctx, SCMP_SYS(eventfd)); + allowSyscall(ctx, SCMP_SYS(eventfd2)); + allowSyscall(ctx, SCMP_SYS(execve)); + allowSyscall(ctx, SCMP_SYS(execveat)); + allowSyscall(ctx, SCMP_SYS(exit)); + allowSyscall(ctx, SCMP_SYS(exit_group)); + allowSyscall(ctx, SCMP_SYS(faccessat)); + allowSyscall(ctx, SCMP_SYS(faccessat2)); + allowSyscall(ctx, SCMP_SYS(fadvise64)); + allowSyscall(ctx, SCMP_SYS(fadvise64_64)); + allowSyscall(ctx, SCMP_SYS(fallocate)); + allowSyscall(ctx, SCMP_SYS(fanotify_init)); + allowSyscall(ctx, SCMP_SYS(fanotify_mark)); + allowSyscall(ctx, SCMP_SYS(fchdir)); + // skip fchmod (dangerous) + // skip fchmodat (dangerous) + // skip fchmodat2 (dangerous) + allowSyscall(ctx, SCMP_SYS(fchown)); + allowSyscall(ctx, SCMP_SYS(fchown32)); + allowSyscall(ctx, SCMP_SYS(fchownat)); + allowSyscall(ctx, SCMP_SYS(fcntl)); + allowSyscall(ctx, SCMP_SYS(fcntl64)); + allowSyscall(ctx, SCMP_SYS(fdatasync)); + allowSyscall(ctx, SCMP_SYS(fgetxattr)); + allowSyscall(ctx, SCMP_SYS(finit_module)); + allowSyscall(ctx, SCMP_SYS(flistxattr)); + allowSyscall(ctx, SCMP_SYS(flock)); + allowSyscall(ctx, SCMP_SYS(fork)); + allowSyscall(ctx, SCMP_SYS(fremovexattr)); + allowSyscall(ctx, SCMP_SYS(fsconfig)); + // skip fsetxattr (dangerous) + allowSyscall(ctx, SCMP_SYS(fsmount)); + allowSyscall(ctx, SCMP_SYS(fsopen)); + allowSyscall(ctx, SCMP_SYS(fspick)); + allowSyscall(ctx, SCMP_SYS(fstat)); + allowSyscall(ctx, SCMP_SYS(fstat64)); + allowSyscall(ctx, SCMP_SYS(fstatat64)); + allowSyscall(ctx, SCMP_SYS(fstatfs)); + allowSyscall(ctx, SCMP_SYS(fstatfs64)); + allowSyscall(ctx, SCMP_SYS(fsync)); + allowSyscall(ctx, SCMP_SYS(ftime)); + allowSyscall(ctx, SCMP_SYS(ftruncate)); + allowSyscall(ctx, SCMP_SYS(ftruncate64)); + allowSyscall(ctx, SCMP_SYS(futex)); + allowSyscall(ctx, SCMP_SYS(futex_requeue)); + allowSyscall(ctx, SCMP_SYS(futex_time64)); + allowSyscall(ctx, SCMP_SYS(futex_wait)); + allowSyscall(ctx, SCMP_SYS(futex_waitv)); + allowSyscall(ctx, SCMP_SYS(futex_wake)); + allowSyscall(ctx, SCMP_SYS(futimesat)); + allowSyscall(ctx, SCMP_SYS(getcpu)); + allowSyscall(ctx, SCMP_SYS(getcwd)); + allowSyscall(ctx, SCMP_SYS(getdents)); + allowSyscall(ctx, SCMP_SYS(getdents64)); + allowSyscall(ctx, SCMP_SYS(getegid)); + allowSyscall(ctx, SCMP_SYS(getegid32)); + allowSyscall(ctx, SCMP_SYS(geteuid)); + allowSyscall(ctx, SCMP_SYS(geteuid32)); + allowSyscall(ctx, SCMP_SYS(getgid)); + allowSyscall(ctx, SCMP_SYS(getgid32)); + allowSyscall(ctx, SCMP_SYS(getgroups)); + allowSyscall(ctx, SCMP_SYS(getgroups32)); + allowSyscall(ctx, SCMP_SYS(getitimer)); + allowSyscall(ctx, SCMP_SYS(get_kernel_syms)); + allowSyscall(ctx, SCMP_SYS(get_mempolicy)); + allowSyscall(ctx, SCMP_SYS(getpeername)); + allowSyscall(ctx, SCMP_SYS(getpgid)); + allowSyscall(ctx, SCMP_SYS(getpgrp)); + allowSyscall(ctx, SCMP_SYS(getpid)); + allowSyscall(ctx, SCMP_SYS(getpmsg)); + allowSyscall(ctx, SCMP_SYS(getppid)); + allowSyscall(ctx, SCMP_SYS(getpriority)); + allowSyscall(ctx, SCMP_SYS(getrandom)); + allowSyscall(ctx, SCMP_SYS(getresgid)); + allowSyscall(ctx, SCMP_SYS(getresgid32)); + allowSyscall(ctx, SCMP_SYS(getresuid)); + allowSyscall(ctx, SCMP_SYS(getresuid32)); + allowSyscall(ctx, SCMP_SYS(getrlimit)); + allowSyscall(ctx, SCMP_SYS(get_robust_list)); + allowSyscall(ctx, SCMP_SYS(getrusage)); + allowSyscall(ctx, SCMP_SYS(getsid)); + allowSyscall(ctx, SCMP_SYS(getsockname)); + allowSyscall(ctx, SCMP_SYS(getsockopt)); + allowSyscall(ctx, SCMP_SYS(get_thread_area)); + allowSyscall(ctx, SCMP_SYS(gettid)); + allowSyscall(ctx, SCMP_SYS(gettimeofday)); + allowSyscall(ctx, SCMP_SYS(get_tls)); + allowSyscall(ctx, SCMP_SYS(getuid)); + allowSyscall(ctx, SCMP_SYS(getuid32)); + allowSyscall(ctx, SCMP_SYS(getxattr)); + allowSyscall(ctx, SCMP_SYS(gtty)); + allowSyscall(ctx, SCMP_SYS(idle)); + allowSyscall(ctx, SCMP_SYS(init_module)); + allowSyscall(ctx, SCMP_SYS(inotify_add_watch)); + allowSyscall(ctx, SCMP_SYS(inotify_init)); + allowSyscall(ctx, SCMP_SYS(inotify_init1)); + allowSyscall(ctx, SCMP_SYS(inotify_rm_watch)); + allowSyscall(ctx, SCMP_SYS(io_cancel)); + allowSyscall(ctx, SCMP_SYS(ioctl)); + allowSyscall(ctx, SCMP_SYS(io_destroy)); + allowSyscall(ctx, SCMP_SYS(io_getevents)); + allowSyscall(ctx, SCMP_SYS(ioperm)); + allowSyscall(ctx, SCMP_SYS(io_pgetevents)); + allowSyscall(ctx, SCMP_SYS(io_pgetevents_time64)); + allowSyscall(ctx, SCMP_SYS(iopl)); + allowSyscall(ctx, SCMP_SYS(ioprio_get)); + allowSyscall(ctx, SCMP_SYS(ioprio_set)); + allowSyscall(ctx, SCMP_SYS(io_setup)); + allowSyscall(ctx, SCMP_SYS(io_submit)); + // skip io_uring_enter (may become dangerous) + // skip io_uring_register (may become dangerous) + // skip io_uring_setup (may become dangerous) + allowSyscall(ctx, SCMP_SYS(ipc)); + allowSyscall(ctx, SCMP_SYS(kcmp)); + allowSyscall(ctx, SCMP_SYS(kexec_file_load)); + allowSyscall(ctx, SCMP_SYS(kexec_load)); + allowSyscall(ctx, SCMP_SYS(keyctl)); + allowSyscall(ctx, SCMP_SYS(kill)); + allowSyscall(ctx, SCMP_SYS(landlock_add_rule)); + allowSyscall(ctx, SCMP_SYS(landlock_create_ruleset)); + allowSyscall(ctx, SCMP_SYS(landlock_restrict_self)); + allowSyscall(ctx, SCMP_SYS(lchown)); + allowSyscall(ctx, SCMP_SYS(lchown32)); + allowSyscall(ctx, SCMP_SYS(lgetxattr)); + allowSyscall(ctx, SCMP_SYS(link)); + allowSyscall(ctx, SCMP_SYS(linkat)); + allowSyscall(ctx, SCMP_SYS(listen)); + allowSyscall(ctx, SCMP_SYS(listxattr)); + allowSyscall(ctx, SCMP_SYS(llistxattr)); + allowSyscall(ctx, SCMP_SYS(_llseek)); + allowSyscall(ctx, SCMP_SYS(lock)); + allowSyscall(ctx, SCMP_SYS(lookup_dcookie)); + allowSyscall(ctx, SCMP_SYS(lremovexattr)); + allowSyscall(ctx, SCMP_SYS(lseek)); + // skip lsetxattr (dangerous) + allowSyscall(ctx, SCMP_SYS(lstat)); + allowSyscall(ctx, SCMP_SYS(lstat64)); + allowSyscall(ctx, SCMP_SYS(madvise)); + allowSyscall(ctx, SCMP_SYS(map_shadow_stack)); + allowSyscall(ctx, SCMP_SYS(mbind)); + allowSyscall(ctx, SCMP_SYS(membarrier)); + allowSyscall(ctx, SCMP_SYS(memfd_create)); + allowSyscall(ctx, SCMP_SYS(memfd_secret)); + allowSyscall(ctx, SCMP_SYS(migrate_pages)); + allowSyscall(ctx, SCMP_SYS(mincore)); + allowSyscall(ctx, SCMP_SYS(mkdir)); + allowSyscall(ctx, SCMP_SYS(mkdirat)); + allowSyscall(ctx, SCMP_SYS(mknod)); + allowSyscall(ctx, SCMP_SYS(mknodat)); + allowSyscall(ctx, SCMP_SYS(mlock)); + allowSyscall(ctx, SCMP_SYS(mlock2)); + allowSyscall(ctx, SCMP_SYS(mlockall)); + allowSyscall(ctx, SCMP_SYS(mmap)); + allowSyscall(ctx, SCMP_SYS(mmap2)); + allowSyscall(ctx, SCMP_SYS(modify_ldt)); + allowSyscall(ctx, SCMP_SYS(mount)); + allowSyscall(ctx, SCMP_SYS(mount_setattr)); + allowSyscall(ctx, SCMP_SYS(move_mount)); + allowSyscall(ctx, SCMP_SYS(move_pages)); + allowSyscall(ctx, SCMP_SYS(mprotect)); + allowSyscall(ctx, SCMP_SYS(mpx)); + allowSyscall(ctx, SCMP_SYS(mq_getsetattr)); + allowSyscall(ctx, SCMP_SYS(mq_notify)); + allowSyscall(ctx, SCMP_SYS(mq_open)); + allowSyscall(ctx, SCMP_SYS(mq_timedreceive)); + allowSyscall(ctx, SCMP_SYS(mq_timedreceive_time64)); + allowSyscall(ctx, SCMP_SYS(mq_timedsend)); + allowSyscall(ctx, SCMP_SYS(mq_timedsend_time64)); + allowSyscall(ctx, SCMP_SYS(mq_unlink)); + allowSyscall(ctx, SCMP_SYS(mremap)); + allowSyscall(ctx, SCMP_SYS(msgctl)); + allowSyscall(ctx, SCMP_SYS(msgget)); + allowSyscall(ctx, SCMP_SYS(msgrcv)); + allowSyscall(ctx, SCMP_SYS(msgsnd)); + allowSyscall(ctx, SCMP_SYS(msync)); + allowSyscall(ctx, SCMP_SYS(multiplexer)); + allowSyscall(ctx, SCMP_SYS(munlock)); + allowSyscall(ctx, SCMP_SYS(munlockall)); + allowSyscall(ctx, SCMP_SYS(munmap)); + allowSyscall(ctx, SCMP_SYS(name_to_handle_at)); + allowSyscall(ctx, SCMP_SYS(nanosleep)); + allowSyscall(ctx, SCMP_SYS(newfstatat)); + allowSyscall(ctx, SCMP_SYS(_newselect)); + allowSyscall(ctx, SCMP_SYS(nfsservctl)); + allowSyscall(ctx, SCMP_SYS(nice)); + allowSyscall(ctx, SCMP_SYS(oldfstat)); + allowSyscall(ctx, SCMP_SYS(oldlstat)); + allowSyscall(ctx, SCMP_SYS(oldolduname)); + allowSyscall(ctx, SCMP_SYS(oldstat)); + allowSyscall(ctx, SCMP_SYS(olduname)); + allowSyscall(ctx, SCMP_SYS(open)); + allowSyscall(ctx, SCMP_SYS(openat)); + allowSyscall(ctx, SCMP_SYS(openat2)); + allowSyscall(ctx, SCMP_SYS(open_by_handle_at)); + allowSyscall(ctx, SCMP_SYS(open_tree)); + allowSyscall(ctx, SCMP_SYS(pause)); + allowSyscall(ctx, SCMP_SYS(pciconfig_iobase)); + allowSyscall(ctx, SCMP_SYS(pciconfig_read)); + allowSyscall(ctx, SCMP_SYS(pciconfig_write)); + allowSyscall(ctx, SCMP_SYS(perf_event_open)); + allowSyscall(ctx, SCMP_SYS(personality)); + allowSyscall(ctx, SCMP_SYS(pidfd_getfd)); + allowSyscall(ctx, SCMP_SYS(pidfd_open)); + allowSyscall(ctx, SCMP_SYS(pidfd_send_signal)); + allowSyscall(ctx, SCMP_SYS(pipe)); + allowSyscall(ctx, SCMP_SYS(pipe2)); + allowSyscall(ctx, SCMP_SYS(pivot_root)); + allowSyscall(ctx, SCMP_SYS(pkey_alloc)); + allowSyscall(ctx, SCMP_SYS(pkey_free)); + allowSyscall(ctx, SCMP_SYS(pkey_mprotect)); + allowSyscall(ctx, SCMP_SYS(poll)); + allowSyscall(ctx, SCMP_SYS(ppoll)); + allowSyscall(ctx, SCMP_SYS(ppoll_time64)); + allowSyscall(ctx, SCMP_SYS(prctl)); + allowSyscall(ctx, SCMP_SYS(pread64)); + allowSyscall(ctx, SCMP_SYS(preadv)); + allowSyscall(ctx, SCMP_SYS(preadv2)); + allowSyscall(ctx, SCMP_SYS(prlimit64)); + allowSyscall(ctx, SCMP_SYS(process_madvise)); + allowSyscall(ctx, SCMP_SYS(process_mrelease)); + allowSyscall(ctx, SCMP_SYS(process_vm_readv)); + allowSyscall(ctx, SCMP_SYS(process_vm_writev)); + allowSyscall(ctx, SCMP_SYS(prof)); + allowSyscall(ctx, SCMP_SYS(profil)); + allowSyscall(ctx, SCMP_SYS(pselect6)); + allowSyscall(ctx, SCMP_SYS(pselect6_time64)); + allowSyscall(ctx, SCMP_SYS(ptrace)); + allowSyscall(ctx, SCMP_SYS(putpmsg)); + allowSyscall(ctx, SCMP_SYS(pwrite64)); + allowSyscall(ctx, SCMP_SYS(pwritev)); + allowSyscall(ctx, SCMP_SYS(pwritev2)); + allowSyscall(ctx, SCMP_SYS(query_module)); + allowSyscall(ctx, SCMP_SYS(quotactl)); + allowSyscall(ctx, SCMP_SYS(quotactl_fd)); + allowSyscall(ctx, SCMP_SYS(read)); + allowSyscall(ctx, SCMP_SYS(readahead)); + allowSyscall(ctx, SCMP_SYS(readdir)); + allowSyscall(ctx, SCMP_SYS(readlink)); + allowSyscall(ctx, SCMP_SYS(readlinkat)); + allowSyscall(ctx, SCMP_SYS(readv)); + allowSyscall(ctx, SCMP_SYS(reboot)); + allowSyscall(ctx, SCMP_SYS(recv)); + allowSyscall(ctx, SCMP_SYS(recvfrom)); + allowSyscall(ctx, SCMP_SYS(recvmmsg)); + allowSyscall(ctx, SCMP_SYS(recvmmsg_time64)); + allowSyscall(ctx, SCMP_SYS(recvmsg)); + allowSyscall(ctx, SCMP_SYS(remap_file_pages)); + allowSyscall(ctx, SCMP_SYS(removexattr)); + allowSyscall(ctx, SCMP_SYS(rename)); + allowSyscall(ctx, SCMP_SYS(renameat)); + allowSyscall(ctx, SCMP_SYS(renameat2)); + allowSyscall(ctx, SCMP_SYS(request_key)); + allowSyscall(ctx, SCMP_SYS(restart_syscall)); + allowSyscall(ctx, SCMP_SYS(riscv_flush_icache)); + allowSyscall(ctx, SCMP_SYS(rmdir)); + allowSyscall(ctx, SCMP_SYS(rseq)); + allowSyscall(ctx, SCMP_SYS(rtas)); + allowSyscall(ctx, SCMP_SYS(rt_sigaction)); + allowSyscall(ctx, SCMP_SYS(rt_sigpending)); + allowSyscall(ctx, SCMP_SYS(rt_sigprocmask)); + allowSyscall(ctx, SCMP_SYS(rt_sigqueueinfo)); + allowSyscall(ctx, SCMP_SYS(rt_sigreturn)); + allowSyscall(ctx, SCMP_SYS(rt_sigsuspend)); + allowSyscall(ctx, SCMP_SYS(rt_sigtimedwait)); + allowSyscall(ctx, SCMP_SYS(rt_sigtimedwait_time64)); + allowSyscall(ctx, SCMP_SYS(rt_tgsigqueueinfo)); + allowSyscall(ctx, SCMP_SYS(s390_guarded_storage)); + allowSyscall(ctx, SCMP_SYS(s390_pci_mmio_read)); + allowSyscall(ctx, SCMP_SYS(s390_pci_mmio_write)); + allowSyscall(ctx, SCMP_SYS(s390_runtime_instr)); + allowSyscall(ctx, SCMP_SYS(s390_sthyi)); + allowSyscall(ctx, SCMP_SYS(sched_getaffinity)); + allowSyscall(ctx, SCMP_SYS(sched_getattr)); + allowSyscall(ctx, SCMP_SYS(sched_getparam)); + allowSyscall(ctx, SCMP_SYS(sched_get_priority_max)); + allowSyscall(ctx, SCMP_SYS(sched_get_priority_min)); + allowSyscall(ctx, SCMP_SYS(sched_getscheduler)); + allowSyscall(ctx, SCMP_SYS(sched_rr_get_interval)); + allowSyscall(ctx, SCMP_SYS(sched_rr_get_interval_time64)); + allowSyscall(ctx, SCMP_SYS(sched_setaffinity)); + allowSyscall(ctx, SCMP_SYS(sched_setattr)); + allowSyscall(ctx, SCMP_SYS(sched_setparam)); + allowSyscall(ctx, SCMP_SYS(sched_setscheduler)); + allowSyscall(ctx, SCMP_SYS(sched_yield)); + allowSyscall(ctx, SCMP_SYS(seccomp)); + allowSyscall(ctx, SCMP_SYS(security)); + allowSyscall(ctx, SCMP_SYS(select)); + allowSyscall(ctx, SCMP_SYS(semctl)); + allowSyscall(ctx, SCMP_SYS(semget)); + allowSyscall(ctx, SCMP_SYS(semop)); + allowSyscall(ctx, SCMP_SYS(semtimedop)); + allowSyscall(ctx, SCMP_SYS(semtimedop_time64)); + allowSyscall(ctx, SCMP_SYS(send)); + allowSyscall(ctx, SCMP_SYS(sendfile)); + allowSyscall(ctx, SCMP_SYS(sendfile64)); + allowSyscall(ctx, SCMP_SYS(sendmmsg)); + allowSyscall(ctx, SCMP_SYS(sendmsg)); + allowSyscall(ctx, SCMP_SYS(sendto)); + allowSyscall(ctx, SCMP_SYS(setdomainname)); + allowSyscall(ctx, SCMP_SYS(setfsgid)); + allowSyscall(ctx, SCMP_SYS(setfsgid32)); + allowSyscall(ctx, SCMP_SYS(setfsuid)); + allowSyscall(ctx, SCMP_SYS(setfsuid32)); + allowSyscall(ctx, SCMP_SYS(setgid)); + allowSyscall(ctx, SCMP_SYS(setgid32)); + allowSyscall(ctx, SCMP_SYS(setgroups)); + allowSyscall(ctx, SCMP_SYS(setgroups32)); + allowSyscall(ctx, SCMP_SYS(sethostname)); + allowSyscall(ctx, SCMP_SYS(setitimer)); + allowSyscall(ctx, SCMP_SYS(set_mempolicy)); + allowSyscall(ctx, SCMP_SYS(set_mempolicy_home_node)); + allowSyscall(ctx, SCMP_SYS(setns)); + allowSyscall(ctx, SCMP_SYS(setpgid)); + allowSyscall(ctx, SCMP_SYS(setpriority)); + allowSyscall(ctx, SCMP_SYS(setregid)); + allowSyscall(ctx, SCMP_SYS(setregid32)); + allowSyscall(ctx, SCMP_SYS(setresgid)); + allowSyscall(ctx, SCMP_SYS(setresgid32)); + allowSyscall(ctx, SCMP_SYS(setresuid)); + allowSyscall(ctx, SCMP_SYS(setresuid32)); + allowSyscall(ctx, SCMP_SYS(setreuid)); + allowSyscall(ctx, SCMP_SYS(setreuid32)); + allowSyscall(ctx, SCMP_SYS(setrlimit)); + allowSyscall(ctx, SCMP_SYS(set_robust_list)); + allowSyscall(ctx, SCMP_SYS(setsid)); + allowSyscall(ctx, SCMP_SYS(setsockopt)); + allowSyscall(ctx, SCMP_SYS(set_thread_area)); + allowSyscall(ctx, SCMP_SYS(set_tid_address)); + allowSyscall(ctx, SCMP_SYS(settimeofday)); + allowSyscall(ctx, SCMP_SYS(set_tls)); + allowSyscall(ctx, SCMP_SYS(setuid)); + allowSyscall(ctx, SCMP_SYS(setuid32)); + // skip setxattr (dangerous) + allowSyscall(ctx, SCMP_SYS(sgetmask)); + allowSyscall(ctx, SCMP_SYS(shmat)); + allowSyscall(ctx, SCMP_SYS(shmctl)); + allowSyscall(ctx, SCMP_SYS(shmdt)); + allowSyscall(ctx, SCMP_SYS(shmget)); + allowSyscall(ctx, SCMP_SYS(shutdown)); + allowSyscall(ctx, SCMP_SYS(sigaction)); + allowSyscall(ctx, SCMP_SYS(sigaltstack)); + allowSyscall(ctx, SCMP_SYS(signal)); + allowSyscall(ctx, SCMP_SYS(signalfd)); + allowSyscall(ctx, SCMP_SYS(signalfd4)); + allowSyscall(ctx, SCMP_SYS(sigpending)); + allowSyscall(ctx, SCMP_SYS(sigprocmask)); + allowSyscall(ctx, SCMP_SYS(sigreturn)); + allowSyscall(ctx, SCMP_SYS(sigsuspend)); + allowSyscall(ctx, SCMP_SYS(socket)); + allowSyscall(ctx, SCMP_SYS(socketcall)); + allowSyscall(ctx, SCMP_SYS(socketpair)); + allowSyscall(ctx, SCMP_SYS(splice)); + allowSyscall(ctx, SCMP_SYS(spu_create)); + allowSyscall(ctx, SCMP_SYS(spu_run)); + allowSyscall(ctx, SCMP_SYS(ssetmask)); + allowSyscall(ctx, SCMP_SYS(stat)); + allowSyscall(ctx, SCMP_SYS(stat64)); + allowSyscall(ctx, SCMP_SYS(statfs)); + allowSyscall(ctx, SCMP_SYS(statfs64)); + allowSyscall(ctx, SCMP_SYS(statx)); + allowSyscall(ctx, SCMP_SYS(stime)); + allowSyscall(ctx, SCMP_SYS(stty)); + allowSyscall(ctx, SCMP_SYS(subpage_prot)); + allowSyscall(ctx, SCMP_SYS(swapcontext)); + allowSyscall(ctx, SCMP_SYS(swapoff)); + allowSyscall(ctx, SCMP_SYS(swapon)); + allowSyscall(ctx, SCMP_SYS(switch_endian)); + allowSyscall(ctx, SCMP_SYS(symlink)); + allowSyscall(ctx, SCMP_SYS(symlinkat)); + allowSyscall(ctx, SCMP_SYS(sync)); + allowSyscall(ctx, SCMP_SYS(sync_file_range)); + allowSyscall(ctx, SCMP_SYS(sync_file_range2)); + allowSyscall(ctx, SCMP_SYS(syncfs)); + allowSyscall(ctx, SCMP_SYS(syscall)); + allowSyscall(ctx, SCMP_SYS(_sysctl)); + allowSyscall(ctx, SCMP_SYS(sys_debug_setcontext)); + allowSyscall(ctx, SCMP_SYS(sysfs)); + allowSyscall(ctx, SCMP_SYS(sysinfo)); + allowSyscall(ctx, SCMP_SYS(syslog)); + allowSyscall(ctx, SCMP_SYS(sysmips)); + allowSyscall(ctx, SCMP_SYS(tee)); + allowSyscall(ctx, SCMP_SYS(tgkill)); + allowSyscall(ctx, SCMP_SYS(time)); + allowSyscall(ctx, SCMP_SYS(timer_create)); + allowSyscall(ctx, SCMP_SYS(timer_delete)); + allowSyscall(ctx, SCMP_SYS(timerfd)); + allowSyscall(ctx, SCMP_SYS(timerfd_create)); + allowSyscall(ctx, SCMP_SYS(timerfd_gettime)); + allowSyscall(ctx, SCMP_SYS(timerfd_gettime64)); + allowSyscall(ctx, SCMP_SYS(timerfd_settime)); + allowSyscall(ctx, SCMP_SYS(timerfd_settime64)); + allowSyscall(ctx, SCMP_SYS(timer_getoverrun)); + allowSyscall(ctx, SCMP_SYS(timer_gettime)); + allowSyscall(ctx, SCMP_SYS(timer_gettime64)); + allowSyscall(ctx, SCMP_SYS(timer_settime)); + allowSyscall(ctx, SCMP_SYS(timer_settime64)); + allowSyscall(ctx, SCMP_SYS(times)); + allowSyscall(ctx, SCMP_SYS(tkill)); + allowSyscall(ctx, SCMP_SYS(truncate)); + allowSyscall(ctx, SCMP_SYS(truncate64)); + allowSyscall(ctx, SCMP_SYS(tuxcall)); + allowSyscall(ctx, SCMP_SYS(ugetrlimit)); + allowSyscall(ctx, SCMP_SYS(ulimit)); + allowSyscall(ctx, SCMP_SYS(umask)); + allowSyscall(ctx, SCMP_SYS(umount)); + allowSyscall(ctx, SCMP_SYS(umount2)); + allowSyscall(ctx, SCMP_SYS(uname)); + allowSyscall(ctx, SCMP_SYS(unlink)); + allowSyscall(ctx, SCMP_SYS(unlinkat)); + allowSyscall(ctx, SCMP_SYS(unshare)); + allowSyscall(ctx, SCMP_SYS(uselib)); + allowSyscall(ctx, SCMP_SYS(userfaultfd)); + allowSyscall(ctx, SCMP_SYS(usr26)); + allowSyscall(ctx, SCMP_SYS(usr32)); + allowSyscall(ctx, SCMP_SYS(ustat)); + allowSyscall(ctx, SCMP_SYS(utime)); + allowSyscall(ctx, SCMP_SYS(utimensat)); + allowSyscall(ctx, SCMP_SYS(utimensat_time64)); + allowSyscall(ctx, SCMP_SYS(utimes)); + allowSyscall(ctx, SCMP_SYS(vfork)); + allowSyscall(ctx, SCMP_SYS(vhangup)); + allowSyscall(ctx, SCMP_SYS(vm86)); + allowSyscall(ctx, SCMP_SYS(vm86old)); + allowSyscall(ctx, SCMP_SYS(vmsplice)); + allowSyscall(ctx, SCMP_SYS(vserver)); + allowSyscall(ctx, SCMP_SYS(wait4)); + allowSyscall(ctx, SCMP_SYS(waitid)); + allowSyscall(ctx, SCMP_SYS(waitpid)); + allowSyscall(ctx, SCMP_SYS(write)); + allowSyscall(ctx, SCMP_SYS(writev)); + // END extract-syscalls + + // chmod family: prevent adding setuid/setgid bits to existing files. + // The Nix store does not support setuid/setgid, and even their temporary creation can weaken the security of the sandbox. + ALLOW_CHMOD_IF_SAFE(ctx, SCMP_SYS(chmod), 1); + ALLOW_CHMOD_IF_SAFE(ctx, SCMP_SYS(fchmod), 1); + ALLOW_CHMOD_IF_SAFE(ctx, SCMP_SYS(fchmodat), 2); + ALLOW_CHMOD_IF_SAFE(ctx, SCMP_SYS(fchmodat2), 2); + + // setxattr family: prevent creation of extended attributes or ACLs. + // Not all filesystems support them, and they're incompatible with the NAR format. if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(setxattr), 0) != 0 || seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lsetxattr), 0) != 0 || seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fsetxattr), 0) != 0) @@ -1460,11 +1961,7 @@ void LocalDerivationGoal::runChild() commonChildInit(); - try { - setupSeccomp(); - } catch (...) { - if (buildUser) throw; - } + setupSeccomp(); bool setUser = true; @@ -1882,7 +2379,7 @@ void LocalDerivationGoal::runChild() sandboxArgs.push_back("_ALLOW_LOCAL_NETWORKING"); sandboxArgs.push_back("1"); } - if (sandbox_init_with_parameters(sandboxProfile.c_str(), 0, stringsToCharPtrs(sandboxArgs).data(), NULL)) { + if (sandbox_init_with_parameters(sandboxProfile.c_str(), 0, stringsToCharPtrs(sandboxArgs).data(), nullptr)) { writeFull(STDERR_FILENO, "failed to configure sandbox\n"); _exit(1); } diff --git a/src/libstore/build/substitution-goal.cc b/src/libstore/build/substitution-goal.cc index cc4cb3c8c..027a7e161 100644 --- a/src/libstore/build/substitution-goal.cc +++ b/src/libstore/build/substitution-goal.cc @@ -86,7 +86,6 @@ void PathSubstitutionGoal::tryNext() if (substituterFailed) { worker.failedSubstitutions++; - worker.updateProgress(); } return; @@ -150,8 +149,6 @@ void PathSubstitutionGoal::tryNext() ? std::make_unique<MaintainCount<uint64_t>>(worker.expectedDownloadSize, narInfo->fileSize) : nullptr; - worker.updateProgress(); - /* Bail out early if this substituter lacks a valid signature. LocalStore::addToStore() also checks for this, but only after we've downloaded the path. */ @@ -210,13 +207,10 @@ void PathSubstitutionGoal::tryToRun() } maintainRunningSubstitutions = std::make_unique<MaintainCount<uint64_t>>(worker.runningSubstitutions); - worker.updateProgress(); outPipe.create(); - promise = std::promise<void>(); - - thr = std::thread([this]() { + thr = std::async(std::launch::async, [this]() { auto & fetchPath = subPath ? *subPath : storePath; try { ReceiveInterrupts receiveInterrupts; @@ -230,16 +224,12 @@ void PathSubstitutionGoal::tryToRun() copyStorePath( *sub, worker.store, fetchPath, repair, sub->isTrusted ? NoCheckSigs : CheckSigs ); - - promise.set_value(); } catch (const EndOfFile &) { - promise.set_exception(std::make_exception_ptr(EndOfFile( + throw EndOfFile( "NAR for '%s' fetched from '%s' is incomplete", sub->printStorePath(fetchPath), sub->getUri() - ))); - } catch (...) { - promise.set_exception(std::current_exception()); + ); } }); @@ -253,11 +243,10 @@ void PathSubstitutionGoal::finished() { trace("substitute finished"); - thr.join(); worker.childTerminated(this); try { - promise.get_future().get(); + thr.get(); } catch (std::exception & e) { printError(e.what()); @@ -296,8 +285,6 @@ void PathSubstitutionGoal::finished() worker.doneNarSize += maintainExpectedNar->delta; maintainExpectedNar.reset(); - worker.updateProgress(); - done(ecSuccess, BuildResult::Substituted); } @@ -307,18 +294,12 @@ void PathSubstitutionGoal::handleChildOutput(int fd, std::string_view data) } -void PathSubstitutionGoal::handleEOF(int fd) -{ - if (fd == outPipe.readSide.get()) worker.wakeUp(shared_from_this()); -} - - void PathSubstitutionGoal::cleanup() { try { - if (thr.joinable()) { + if (thr.valid()) { // FIXME: signal worker thread to quit. - thr.join(); + thr.get(); worker.childTerminated(this); } diff --git a/src/libstore/build/substitution-goal.hh b/src/libstore/build/substitution-goal.hh index 1d389d328..d85b3beb3 100644 --- a/src/libstore/build/substitution-goal.hh +++ b/src/libstore/build/substitution-goal.hh @@ -50,9 +50,7 @@ struct PathSubstitutionGoal : public Goal /** * The substituter thread. */ - std::thread thr; - - std::promise<void> promise; + std::future<void> thr; /** * Whether to try to repair a valid path. @@ -112,7 +110,6 @@ public: * Callback used by the worker to write to the log. */ void handleChildOutput(int fd, std::string_view data) override; - void handleEOF(int fd) override; /* Called by destructor, can't be overridden */ void cleanup() override final; diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index 39bcd5d92..5b2e36acb 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -157,21 +157,13 @@ void Worker::removeGoal(GoalPtr goal) if (goal->exitCode == Goal::ecFailed && !settings.keepGoing) topGoals.clear(); } - - /* Wake up goals waiting for any goal to finish. */ - for (auto & i : waitingForAnyGoal) { - GoalPtr goal = i.lock(); - if (goal) wakeUp(goal); - } - - waitingForAnyGoal.clear(); } void Worker::wakeUp(GoalPtr goal) { goal->trace("woken up"); - addToWeakGoals(awake, goal); + awake.insert(goal); } @@ -213,7 +205,7 @@ void Worker::childStarted(GoalPtr goal, const std::set<int> & fds, } -void Worker::childTerminated(Goal * goal, bool wakeSleepers) +void Worker::childTerminated(Goal * goal) { auto i = std::find_if(children.begin(), children.end(), [&](const Child & child) { return child.goal2 == goal; }); @@ -236,16 +228,13 @@ void Worker::childTerminated(Goal * goal, bool wakeSleepers) children.erase(i); - if (wakeSleepers) { - - /* Wake up goals waiting for a build slot. */ - for (auto & j : wantingToBuild) { - GoalPtr goal = j.lock(); - if (goal) wakeUp(goal); - } - - wantingToBuild.clear(); + /* Wake up goals waiting for a build slot. */ + for (auto & j : wantingToBuild) { + GoalPtr goal = j.lock(); + if (goal) wakeUp(goal); } + + wantingToBuild.clear(); } @@ -257,21 +246,14 @@ void Worker::waitForBuildSlot(GoalPtr goal) (isSubstitutionGoal && getNrSubstitutions() < settings.maxSubstitutionJobs)) wakeUp(goal); /* we can do it right away */ else - addToWeakGoals(wantingToBuild, goal); -} - - -void Worker::waitForAnyGoal(GoalPtr goal) -{ - debug("wait for any goal"); - addToWeakGoals(waitingForAnyGoal, goal); + wantingToBuild.insert(goal); } void Worker::waitForAWhile(GoalPtr goal) { debug("wait for a while"); - addToWeakGoals(waitingForAWhile, goal); + waitingForAWhile.insert(goal); } @@ -318,6 +300,19 @@ void Worker::run(const Goals & _topGoals) for (auto & goal : awake2) { checkInterrupt(); goal->work(); + + actDerivations.progress( + doneBuilds, expectedBuilds + doneBuilds, runningBuilds, failedBuilds + ); + actSubstitutions.progress( + doneSubstitutions, + expectedSubstitutions + doneSubstitutions, + runningSubstitutions, + failedSubstitutions + ); + act.setExpected(actFileTransfer, expectedDownloadSize + doneDownloadSize); + act.setExpected(actCopyPath, expectedNarSize + doneNarSize); + if (topGoals.empty()) break; // stuff may have been cancelled } } @@ -429,7 +424,7 @@ void Worker::waitForInput() GoalPtr goal = j->goal.lock(); assert(goal); - if (goal->exitCode == Goal::ecBusy && + if (!goal->exitCode.has_value() && 0 != settings.maxSilentTime && j->respectTimeouts && after - j->lastOutput >= std::chrono::seconds(settings.maxSilentTime)) @@ -440,7 +435,7 @@ void Worker::waitForInput() continue; } - else if (goal->exitCode == Goal::ecBusy && + else if (!goal->exitCode.has_value() && 0 != settings.buildTimeout && j->respectTimeouts && after - j->timeStarted >= std::chrono::seconds(settings.buildTimeout)) @@ -464,6 +459,7 @@ void Worker::waitForInput() if (rd == 0 || (rd == -1 && errno == EIO)) { debug("%1%: got EOF", goal->getName()); goal->handleEOF(k); + wakeUp(goal); j->fds.erase(k); } else if (rd == -1) { if (errno != EINTR) diff --git a/src/libstore/build/worker.hh b/src/libstore/build/worker.hh index e1d8e5031..3984c9c1c 100644 --- a/src/libstore/build/worker.hh +++ b/src/libstore/build/worker.hh @@ -91,11 +91,6 @@ private: std::map<DrvOutput, std::weak_ptr<DrvOutputSubstitutionGoal>> drvOutputSubstitutionGoals; /** - * Goals waiting for busy paths to be unlocked. - */ - WeakGoals waitingForAnyGoal; - - /** * Goals sleeping for a few seconds (polling a lock). */ WeakGoals waitingForAWhile; @@ -151,7 +146,6 @@ public: uint64_t doneSubstitutions = 0; uint64_t failedSubstitutions = 0; uint64_t runningSubstitutions = 0; - uint64_t runningCASubstitutions = 0; uint64_t expectedDownloadSize = 0; uint64_t doneDownloadSize = 0; uint64_t expectedNarSize = 0; @@ -228,12 +222,9 @@ public: bool inBuildSlot, bool respectTimeouts); /** - * Unregisters a running child process. `wakeSleepers` should be - * false if there is no sense in waking up goals that are sleeping - * because they can't run yet (e.g., there is no free build slot, - * or the hook would still say `postpone`). + * Unregisters a running child process. */ - void childTerminated(Goal * goal, bool wakeSleepers = true); + void childTerminated(Goal * goal); /** * Put `goal` to sleep until a build slot becomes available (which @@ -242,12 +233,6 @@ public: void waitForBuildSlot(GoalPtr goal); /** - * Wait for any goal to finish. Pretty indiscriminate way to - * wait for some resource that some other goal is holding. - */ - void waitForAnyGoal(GoalPtr goal); - - /** * Wait for a few seconds and then retry this goal. Used when * waiting for a lock held by another process. This kind of * polling is inefficient, but POSIX doesn't really provide a way @@ -295,14 +280,6 @@ public: bool pathContentsGood(const StorePath & path); void markContentsGood(const StorePath & path); - - void updateProgress() - { - actDerivations.progress(doneBuilds, expectedBuilds + doneBuilds, runningBuilds, failedBuilds); - actSubstitutions.progress(doneSubstitutions, expectedSubstitutions + doneSubstitutions, runningSubstitutions, failedSubstitutions); - act.setExpected(actFileTransfer, expectedDownloadSize + doneDownloadSize); - act.setExpected(actCopyPath, expectedNarSize + doneNarSize); - } }; } diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 4162015d6..b09645f46 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -158,7 +158,7 @@ struct TunnelSink : Sink { Sink & to; TunnelSink(Sink & to) : to(to) { } - void operator () (std::string_view data) + void operator () (std::string_view data) override { to << STDERR_WRITE << data; } diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index 02d26e034..6cfa3ffac 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -177,14 +177,14 @@ static bool hasVirt() { size_t size; size = sizeof(hasVMM); - if (sysctlbyname("kern.hv_vmm_present", &hasVMM, &size, NULL, 0) == 0) { + if (sysctlbyname("kern.hv_vmm_present", &hasVMM, &size, nullptr, 0) == 0) { if (hasVMM) return false; } // whether the kernel and hardware supports virt size = sizeof(hvSupport); - if (sysctlbyname("kern.hv_support", &hvSupport, &size, NULL, 0) == 0) { + if (sysctlbyname("kern.hv_support", &hvSupport, &size, nullptr, 0) == 0) { return hvSupport == 1; } else { return false; diff --git a/src/libstore/linux/fchmodat2-compat.hh b/src/libstore/linux/fchmodat2-compat.hh deleted file mode 100644 index d5ef81e22..000000000 --- a/src/libstore/linux/fchmodat2-compat.hh +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Determine the syscall number for `fchmodat2`. - * - * On most platforms this is 452. Exceptions can be found on - * a glibc git checkout via `rg --pcre2 'define __NR_fchmodat2 (?!452)'`. - * - * The problem is that glibc 2.39 and libseccomp 2.5.5 are needed to - * get the syscall number. However, a Lix built against nixpkgs 23.11 - * (glibc 2.38) should still have the issue fixed without depending - * on the build environment. - * - * To achieve that, the macros below try to determine the platform and - * set the syscall number which is platform-specific, but - * in most cases 452. - * - * TODO: remove this when 23.11 is EOL and the entire (supported) ecosystem - * is on glibc 2.39. - */ - -#pragma once -///@file - -#if defined(__alpha__) -# define NIX_SYSCALL_FCHMODAT2 562 -#elif defined(__x86_64__) && SIZE_MAX == 0xFFFFFFFF // x32 -# define NIX_SYSCALL_FCHMODAT2 1073742276 -#elif defined(__mips__) && defined(__mips64) && defined(_ABIN64) // mips64/n64 -# define NIX_SYSCALL_FCHMODAT2 5452 -#elif defined(__mips__) && defined(__mips64) && defined(_ABIN32) // mips64/n32 -# define NIX_SYSCALL_FCHMODAT2 6452 -#elif defined(__mips__) && defined(_ABIO32) // mips32 -# define NIX_SYSCALL_FCHMODAT2 4452 -#else -# define NIX_SYSCALL_FCHMODAT2 452 -#endif diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 10d43ee3e..f09d1bdab 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -557,7 +557,7 @@ void LocalStore::openDB(State & state, bool create) if (sqlite3_exec(db, "pragma main.journal_size_limit = 1099511627776;", 0, 0, 0) != SQLITE_OK) SQLiteError::throw_(db, "setting journal_size_limit"); int enable = 1; - if (sqlite3_file_control(db, NULL, SQLITE_FCNTL_PERSIST_WAL, &enable) != SQLITE_OK) + if (sqlite3_file_control(db, nullptr, SQLITE_FCNTL_PERSIST_WAL, &enable) != SQLITE_OK) SQLiteError::throw_(db, "setting persistent WAL mode"); } diff --git a/src/libstore/meson.build b/src/libstore/meson.build index fa363bd19..5416bd2b5 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -167,6 +167,9 @@ if host_machine.system() == 'linux' elif host_machine.system() == 'darwin' libstore_sources += files('platform/darwin.cc') libstore_headers += files('platform/darwin.hh') +elif host_machine.system() == 'freebsd' + libstore_sources += files('platform/freebsd.cc') + libstore_headers += files('platform/freebsd.hh') else libstore_sources += files('platform/fallback.cc') libstore_headers += files('platform/fallback.hh') @@ -202,23 +205,29 @@ foreach name, value : cpp_str_defines ] endforeach +dependencies = [ + libarchive, + liblixutil, # Internal. + seccomp, + sqlite, + sodium, + curl, + openssl, + aws_sdk, + aws_s3, + aws_sdk_transfer, + nlohmann_json, +] + +if host_machine.system() == 'freebsd' + dependencies += [ libprocstat ] +endif + libstore = library( 'lixstore', libstore_generated_headers, libstore_sources, - dependencies : [ - libarchive, - liblixutil, # Internal. - seccomp, - sqlite, - sodium, - curl, - openssl, - aws_sdk, - aws_s3, - aws_sdk_transfer, - nlohmann_json, - ], + dependencies : dependencies, cpp_args : cpp_args, cpp_pch : cpp_pch, install : true, diff --git a/src/libstore/platform.cc b/src/libstore/platform.cc index d10d33f0e..72757e39b 100644 --- a/src/libstore/platform.cc +++ b/src/libstore/platform.cc @@ -5,6 +5,8 @@ #include "platform/linux.hh" #elif __APPLE__ #include "platform/darwin.hh" +#elif __FreeBSD__ +#include "platform/freebsd.hh" #else #include "platform/fallback.hh" #endif @@ -16,6 +18,8 @@ std::shared_ptr<LocalStore> LocalStore::makeLocalStore(const Params & params) return std::shared_ptr<LocalStore>(new LinuxLocalStore(params)); #elif __APPLE__ return std::shared_ptr<LocalStore>(new DarwinLocalStore(params)); +#elif __FreeBSD__ + return std::shared_ptr<LocalStore>(new FreeBSDLocalStore(params)); #else return std::shared_ptr<LocalStore>(new FallbackLocalStore(params)); #endif @@ -32,6 +36,8 @@ std::shared_ptr<LocalDerivationGoal> LocalDerivationGoal::makeLocalDerivationGoa return std::make_shared<LinuxLocalDerivationGoal>(drvPath, wantedOutputs, worker, buildMode); #elif __APPLE__ return std::make_shared<DarwinLocalDerivationGoal>(drvPath, wantedOutputs, worker, buildMode); +#elif __FreeBSD__ + return std::make_shared<FreeBSDLocalDerivationGoal>(drvPath, wantedOutputs, worker, buildMode); #else return std::make_shared<FallbackLocalDerivationGoal>(drvPath, wantedOutputs, worker, buildMode); #endif @@ -53,6 +59,10 @@ std::shared_ptr<LocalDerivationGoal> LocalDerivationGoal::makeLocalDerivationGoa return std::make_shared<DarwinLocalDerivationGoal>( drvPath, drv, wantedOutputs, worker, buildMode ); +#elif __FreeBSD__ + return std::make_shared<FreeBSDLocalDerivationGoal>( + drvPath, drv, wantedOutputs, worker, buildMode + ); #else return std::make_shared<FallbackLocalDerivationGoal>( drvPath, drv, wantedOutputs, worker, buildMode diff --git a/src/libstore/platform/darwin.cc b/src/libstore/platform/darwin.cc index 83b4b4183..1b591fde3 100644 --- a/src/libstore/platform/darwin.cc +++ b/src/libstore/platform/darwin.cc @@ -235,15 +235,15 @@ void DarwinLocalDerivationGoal::execBuilder(std::string builder, Strings args, S if (drv->platform == "aarch64-darwin") { // Unset kern.curproc_arch_affinity so we can escape Rosetta int affinity = 0; - sysctlbyname("kern.curproc_arch_affinity", NULL, NULL, &affinity, sizeof(affinity)); + sysctlbyname("kern.curproc_arch_affinity", nullptr, nullptr, &affinity, sizeof(affinity)); cpu_type_t cpu = CPU_TYPE_ARM64; - posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); + posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, nullptr); } else if (drv->platform == "x86_64-darwin") { cpu_type_t cpu = CPU_TYPE_X86_64; - posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); + posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, nullptr); } - posix_spawn(NULL, builder.c_str(), NULL, &attrp, stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); + posix_spawn(nullptr, builder.c_str(), nullptr, &attrp, stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); } } diff --git a/src/libstore/platform/freebsd.cc b/src/libstore/platform/freebsd.cc new file mode 100644 index 000000000..bdba1abf5 --- /dev/null +++ b/src/libstore/platform/freebsd.cc @@ -0,0 +1,142 @@ +#include "platform/freebsd.hh" +#include "regex.hh" +#include <sys/param.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/sysctl.h> +#include <sys/user.h> +#include <libprocstat.h> + +namespace nix { + +static void readSysctlRoots(const char * name, UncheckedRoots & unchecked) +{ + size_t len = 0; + std::string value; + if (int err = sysctlbyname(name, nullptr, &len, nullptr, 0) < 0) { + if (err == ENOENT || err == EACCES) { + return; + } else { + throw SysError(err, "sysctlbyname %1%", name); + } + } + + value.resize(len, ' '); + if (int err = sysctlbyname(name, value.data(), &len, nullptr, 0) < 0) { + if (err == ENOENT || err == EACCES) { + return; + } else { + throw SysError(err, "sysctlbyname %1%", name); + } + } + + for (auto & path : tokenizeString<Strings>(value, ";")) { + unchecked[path].emplace(fmt("{{sysctl:%1%}}", name)); + } +} + +struct ProcstatDeleter +{ + void operator()(struct procstat * ps) + { + procstat_close(ps); + } +}; + +template<auto del> +struct ProcstatReferredDeleter +{ + struct procstat * ps; + + ProcstatReferredDeleter(struct procstat * ps) : ps(ps) {} + + template<typename T> + void operator()(T * p) + { + del(ps, p); + } +}; + +void FreeBSDLocalStore::findPlatformRoots(UncheckedRoots & unchecked) +{ + readSysctlRoots("kern.module_path", unchecked); + + auto storePathRegex = regex::storePathRegex(storeDir); + + auto ps = std::unique_ptr<struct procstat, ProcstatDeleter>(procstat_open_sysctl()); + if (!ps) { + throw SysError("procstat_open_sysctl"); + } + + auto procs = std::unique_ptr<struct kinfo_proc[], ProcstatReferredDeleter<procstat_freeprocs>>( + nullptr, ps.get() + ); + auto files = std::unique_ptr<struct filestat_list, ProcstatReferredDeleter<procstat_freefiles>>( + nullptr, ps.get() + ); + + unsigned int numprocs = 0; + procs.reset(procstat_getprocs(ps.get(), KERN_PROC_PROC, 0, &numprocs)); + if (!procs || numprocs == 0) { + throw SysError("procstat_getprocs"); + }; + + for (unsigned int procidx = 0; procidx < numprocs; procidx++) { + // Includes file descriptors, executable, cwd, + // and mmapped files (including dynamic libraries) + files.reset(procstat_getfiles(ps.get(), &procs[procidx], 1)); + // We only have permission if we're root so just skip it if we fail + if (!files) { + continue; + } + + for (struct filestat * file = files->stqh_first; file; file = file->next.stqe_next) { + if (!file->fs_path) { + continue; + } + + std::string role; + if (file->fs_uflags & PS_FST_UFLAG_CTTY) { + role = "ctty"; + } else if (file->fs_uflags & PS_FST_UFLAG_CDIR) { + role = "cwd"; + } else if (file->fs_uflags & PS_FST_UFLAG_JAIL) { + role = "jail"; + } else if (file->fs_uflags & PS_FST_UFLAG_RDIR) { + role = "root"; + } else if (file->fs_uflags & PS_FST_UFLAG_TEXT) { + role = "text"; + } else if (file->fs_uflags & PS_FST_UFLAG_TRACE) { + role = "trace"; + } else if (file->fs_uflags & PS_FST_UFLAG_MMAP) { + role = "mmap"; + } else { + role = fmt("fd/%1%", file->fs_fd); + } + + unchecked[file->fs_path].emplace(fmt("{procstat:%1%/%2%}", procs[procidx].ki_pid, role) + ); + } + + auto env_name = fmt("{procstat:%1%/env}", procs[procidx].ki_pid); + // No need to free, the buffer is reused on next call and deallocated in procstat_close + char ** env = procstat_getenvv(ps.get(), &procs[procidx], 0); + if (env == nullptr) { + continue; + } + + for (size_t i = 0; env[i]; i++) { + auto envString = std::string(env[i]); + + auto envEnd = std::sregex_iterator{}; + for (auto match = + std::sregex_iterator{envString.begin(), envString.end(), storePathRegex}; + match != envEnd; + match++) + { + unchecked[match->str()].emplace(env_name); + } + } + } +} +} diff --git a/src/libstore/platform/freebsd.hh b/src/libstore/platform/freebsd.hh new file mode 100644 index 000000000..99aff3c10 --- /dev/null +++ b/src/libstore/platform/freebsd.hh @@ -0,0 +1,47 @@ +#pragma once +///@file + +#include "build/local-derivation-goal.hh" +#include "gc-store.hh" +#include "local-store.hh" + +namespace nix { + +/** + * FreeBSD-specific implementation of LocalStore + */ +class FreeBSDLocalStore : public LocalStore +{ +public: + FreeBSDLocalStore(const Params & params) + : StoreConfig(params) + , LocalFSStoreConfig(params) + , LocalStoreConfig(params) + , Store(params) + , LocalFSStore(params) + , LocalStore(params) + { + } + FreeBSDLocalStore(const std::string scheme, std::string path, const Params & params) + : FreeBSDLocalStore(params) + { + throw UnimplementedError("FreeBSDLocalStore"); + } + +private: + + void findPlatformRoots(UncheckedRoots & unchecked) override; +}; + +/** + * FreeBSD-specific implementation of LocalDerivationGoal + */ +class FreeBSDLocalDerivationGoal : public LocalDerivationGoal +{ +public: + using LocalDerivationGoal::LocalDerivationGoal; + +private: +}; + +} diff --git a/src/libutil/concepts.hh b/src/libutil/concepts.hh new file mode 100644 index 000000000..48bd1dbe1 --- /dev/null +++ b/src/libutil/concepts.hh @@ -0,0 +1,22 @@ +#pragma once +/// @file Defines C++ 20 concepts that std doesn't have. + +#include <type_traits> + +namespace nix +{ + +/// Like std::invocable<>, but also constrains the return type as well. +/// +/// Somehow, there is no std concept to do this, even though there is a type trait +/// for it. +/// +/// @tparam CallableT The type you want to constrain to be callable, and to return +/// @p ReturnT when called with @p Args as arguments. +/// +/// @tparam ReturnT The type the callable should return when called. +/// @tparam Args The arguments the callable should accept to return @p ReturnT. +template<typename CallableT, typename ReturnT, typename ...Args> +concept InvocableR = std::is_invocable_r_v<ReturnT, CallableT, Args...>; + +} diff --git a/src/libutil/current-process.cc b/src/libutil/current-process.cc index 3c037c33f..33cda211b 100644 --- a/src/libutil/current-process.cc +++ b/src/libutil/current-process.cc @@ -15,6 +15,11 @@ # include <sys/resource.h> #endif +#if __FreeBSD__ +# include <sys/param.h> +# include <sys/sysctl.h> +#endif + #include <sys/mount.h> #include <cgroup.hh> @@ -102,6 +107,24 @@ std::optional<Path> getSelfExe() return buf; else return std::nullopt; + #elif __FreeBSD__ + int sysctlName[] = { + CTL_KERN, + KERN_PROC, + KERN_PROC_PATHNAME, + -1, + }; + size_t pathLen = 0; + if (sysctl(sysctlName, sizeof(sysctlName) / sizeof(sysctlName[0]), nullptr, &pathLen, nullptr, 0) < 0) { + return std::nullopt; + } + + std::vector<char> path(pathLen); + if (sysctl(sysctlName, sizeof(sysctlName) / sizeof(sysctlName[0]), path.data(), &pathLen, nullptr, 0) < 0) { + return std::nullopt; + } + + return Path(path.begin(), path.end()); #else return std::nullopt; #endif diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index f0199d36f..e2319ec59 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -21,22 +21,14 @@ Path absPath(Path path, std::optional<PathView> dir, bool resolveSymlinks) { if (path.empty() || path[0] != '/') { if (!dir) { -#ifdef __GNU__ - /* GNU (aka. GNU/Hurd) doesn't have any limitation on path - lengths and doesn't define `PATH_MAX'. */ - char *buf = getcwd(NULL, 0); - if (buf == NULL) -#else char buf[PATH_MAX]; - if (!getcwd(buf, sizeof(buf))) -#endif + if (!getcwd(buf, sizeof(buf))) { throw SysError("cannot get cwd"); + } path = concatStrings(buf, "/", path); -#ifdef __GNU__ - free(buf); -#endif - } else + } else { path = concatStrings(*dir, "/", path); + } } return canonPath(path, resolveSymlinks); } diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc index d674c9651..53460f729 100644 --- a/src/libutil/logging.cc +++ b/src/libutil/logging.cc @@ -7,6 +7,7 @@ #include <algorithm> #include <atomic> +#include <mutex> #include <sstream> #include <nlohmann/json.hpp> diff --git a/src/libutil/meson.build b/src/libutil/meson.build index c860e7e00..01fe65207 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -58,6 +58,7 @@ libutil_headers = files( 'comparator.hh', 'compression.hh', 'compute-levels.hh', + 'concepts.hh', 'config-impl.hh', 'config.hh', 'current-process.hh', diff --git a/src/libutil/processes.cc b/src/libutil/processes.cc index 4fe3dcfb4..05b1321e9 100644 --- a/src/libutil/processes.cc +++ b/src/libutil/processes.cc @@ -251,7 +251,7 @@ std::pair<int, std::string> runProgram(RunOptions && options) try { auto proc = runProgram2(options); Finally const _wait([&] { proc.wait(); }); - stdout = proc.stdout()->drain(); + stdout = proc.getStdout()->drain(); } catch (ExecError & e) { status = e.status; } diff --git a/src/libutil/processes.hh b/src/libutil/processes.hh index f61b9696d..dc09a9ba4 100644 --- a/src/libutil/processes.hh +++ b/src/libutil/processes.hh @@ -106,7 +106,7 @@ public: void wait(); - Source * stdout() const { return stdoutSource.get(); } + Source * getStdout() const { return stdoutSource.get(); }; }; std::pair<int, std::string> runProgram(RunOptions && options); diff --git a/src/libutil/serialise.hh b/src/libutil/serialise.hh index 9b1892bbb..6c637bd35 100644 --- a/src/libutil/serialise.hh +++ b/src/libutil/serialise.hh @@ -211,7 +211,7 @@ struct TeeSink : Sink { Sink & sink1, & sink2; TeeSink(Sink & sink1, Sink & sink2) : sink1(sink1), sink2(sink2) { } - virtual void operator () (std::string_view data) + virtual void operator () (std::string_view data) override { sink1(data); sink2(data); @@ -228,7 +228,7 @@ struct TeeSource : Source Sink & sink; TeeSource(Source & orig, Sink & sink) : orig(orig), sink(sink) { } - size_t read(char * data, size_t len) + size_t read(char * data, size_t len) override { size_t n = orig.read(data, len); sink({data, n}); @@ -245,7 +245,7 @@ struct SizedSource : Source size_t remain; SizedSource(Source & orig, size_t size) : orig(orig), remain(size) { } - size_t read(char * data, size_t len) + size_t read(char * data, size_t len) override { if (this->remain <= 0) { throw EndOfFile("sized: unexpected end-of-file"); @@ -338,7 +338,7 @@ struct GeneratorSource : Source { GeneratorSource(Generator<Bytes> && g) : g(std::move(g)) {} - virtual size_t read(char * data, size_t len) + virtual size_t read(char * data, size_t len) override { // we explicitly do not poll the generator multiple times to fill the // buffer, only to produce some output at all. this is allowed by the diff --git a/src/libutil/tarfile.cc b/src/libutil/tarfile.cc index 760a5a65a..c7f9499fd 100644 --- a/src/libutil/tarfile.cc +++ b/src/libutil/tarfile.cc @@ -54,7 +54,7 @@ TarArchive::TarArchive(Source & source, bool raw) : buffer(65536) archive_read_support_format_raw(archive); archive_read_support_format_empty(archive); } - archive_read_set_option(archive, NULL, "mac-ext", NULL); + archive_read_set_option(archive, nullptr, "mac-ext", nullptr); check(archive_read_open(archive, (void *)this, callback_open, callback_read, callback_close), "Failed to open archive (%s)"); } @@ -65,7 +65,7 @@ TarArchive::TarArchive(const Path & path) archive_read_support_filter_all(archive); archive_read_support_format_all(archive); - archive_read_set_option(archive, NULL, "mac-ext", NULL); + archive_read_set_option(archive, nullptr, "mac-ext", nullptr); check(archive_read_open_filename(archive, path.c_str(), 16384), "failed to open archive: %s"); } diff --git a/src/meson.build b/src/meson.build index 3fc5595b8..e918ae392 100644 --- a/src/meson.build +++ b/src/meson.build @@ -12,10 +12,19 @@ subdir('libmain') # libcmd depends on everything subdir('libcmd') - # The rest of the subdirectories aren't separate components, # just source files in another directory, so we process them here. +# Static library that just sets default ASan options. It needs to be included +# in every executable. +asanoptions = static_library( + 'libasanoptions', + files('asan-options/asan-options.cc'), +) +libasanoptions = declare_dependency( + link_whole: asanoptions +) + build_remote_sources = files( 'build-remote/build-remote.cc', ) diff --git a/src/nix/diff-closures.cc b/src/nix/diff-closures.cc index 5eda309d5..dac4625e6 100644 --- a/src/nix/diff-closures.cc +++ b/src/nix/diff-closures.cc @@ -24,24 +24,17 @@ GroupedPaths getClosureInfo(ref<Store> store, const StorePath & toplevel) GroupedPaths groupedPaths; - for (auto & path : closure) { + for (auto const & path : closure) { /* Strip the output name. Unfortunately this is ambiguous (we can't distinguish between output names like "bin" and version suffixes like "unstable"). */ static std::regex regex("(.*)-([a-z]+|lib32|lib64)"); - std::smatch match; + std::cmatch match; std::string name{path.name()}; - // Used to keep name alive through being potentially overwritten below - // (to not invalidate the references from the regex result) - // - // n.b. cannot be just path.name().{begin,end}() since that returns const - // char *, which does not, for some reason, convert as required on - // libstdc++. Seems like a libstdc++ bug or standard bug to me... we - // can afford the allocation in any case. - const std::string origName{path.name()}; + std::string_view const origName = path.name(); std::string outputName; - if (std::regex_match(origName, match, regex)) { + if (std::regex_match(origName.begin(), origName.end(), match, regex)) { name = match[1]; outputName = match[2]; } diff --git a/src/nix/meson.build b/src/nix/meson.build index 22f148fcb..97387e402 100644 --- a/src/nix/meson.build +++ b/src/nix/meson.build @@ -80,6 +80,7 @@ nix = executable( profiles_md_gen, nix2_commands_sources, dependencies : [ + libasanoptions, liblixcmd, liblixutil_mstatic, liblixstore_mstatic, diff --git a/src/resolve-system-dependencies/resolve-system-dependencies.cc b/src/resolve-system-dependencies/resolve-system-dependencies.cc index 2c4b06791..319f7108e 100644 --- a/src/resolve-system-dependencies/resolve-system-dependencies.cc +++ b/src/resolve-system-dependencies/resolve-system-dependencies.cc @@ -48,7 +48,7 @@ std::set<std::string> runResolver(const Path & filename) return {}; } - char* obj = (char*) mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd.get(), 0); + char* obj = (char*) mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd.get(), 0); if (!obj) throw SysError("mmapping '%s'", filename); diff --git a/tests/functional/fetchers.sh b/tests/functional/fetchers.sh new file mode 100644 index 000000000..0f888dc33 --- /dev/null +++ b/tests/functional/fetchers.sh @@ -0,0 +1,91 @@ +source common.sh + +requireGit + +clearStore + +testFetchTreeError() { + rawFetchTreeArg="${1?fetchTree arg missing}" + messageSubstring="${2?messageSubstring missing}" + + output="$(nix eval --impure --raw --expr "(builtins.fetchTree $rawFetchTreeArg).outPath" 2>&1)" && status=0 || status=$? + grepQuiet "$messageSubstring" <<<"$output" + test "$status" -ne 0 +} + +# github/gitlab/sourcehut fetcher input validation +for provider in github gitlab sourcehut; do + # ref/rev validation + testFetchTreeError \ + "{ type = \"$provider\"; owner = \"foo\"; repo = \"bar\"; ref = \",\"; }" \ + "URL '$provider:foo/bar' contains an invalid branch/tag name" + + testFetchTreeError \ + "\"$provider://host/foo/bar/,\"" \ + "URL '$provider:foo/bar', ',' is not a commit hash or a branch/tag name" + + testFetchTreeError \ + "\"$provider://host/foo/bar/f16d8f43dd0998cdb315a2cccf2e4d10027e7ca4?rev=abc\"" \ + "URL '$provider://host/foo/bar/f16d8f43dd0998cdb315a2cccf2e4d10027e7ca4?rev=abc' already contains a ref or rev" + + testFetchTreeError \ + "\"$provider://host/foo/bar/ref?ref=ref2\"" \ + "URL '$provider://host/foo/bar/ref?ref=ref2' already contains a ref or rev" + + # host validation + testFetchTreeError \ + "{ type = \"$provider\"; owner = \"foo\"; repo = \"bar\"; host = \"git_hub.com\"; }" \ + "URL '$provider:foo/bar' contains an invalid instance host" + + testFetchTreeError \ + "\"$provider://host/foo/bar/ref?host=git_hub.com\"" \ + "URL '$provider:foo/bar' contains an invalid instance host" + + # invalid attributes + testFetchTreeError \ + "{ type = \"$provider\"; owner = \"foo\"; repo = \"bar\"; wrong = true; }" \ + "unsupported input attribute 'wrong'" + + testFetchTreeError \ + "\"$provider://host/foo/bar/ref?wrong=1\"" \ + "unsupported input attribute 'wrong'" +done + +# unsupported attributes w/ tarball fetcher +testFetchTreeError \ + "\"https://host/foo?wrong=1\"" \ + "unsupported tarball input attribute 'wrong'. If you wanted to fetch a tarball with a query parameter, please use '{ type = \"tarball\"; url = \"...\"; }" + +# test for unsupported attributes / validation in git fetcher +testFetchTreeError \ + "\"git+https://github.com/owner/repo?invalid=1\"" \ + "unsupported Git input attribute 'invalid'" + +testFetchTreeError \ + "\"git+https://github.com/owner/repo?url=foo\"" \ + "URL 'git+https://github.com/owner/repo?url=foo' must not override url via query param!" + +testFetchTreeError \ + "\"git+https://github.com/owner/repo?ref=foo.lock\"" \ + "invalid Git branch/tag name 'foo.lock'" + +testFetchTreeError \ + "{ type = \"git\"; url =\"https://github.com/owner/repo\"; ref = \"foo.lock\"; }" \ + "invalid Git branch/tag name 'foo.lock'" + +# same for mercurial +testFetchTreeError \ + "\"hg+https://forge.tld/owner/repo?invalid=1\"" \ + "unsupported Mercurial input attribute 'invalid'" + +testFetchTreeError \ + "{ type = \"hg\"; url = \"https://forge.tld/owner/repo\"; invalid = 1; }" \ + "unsupported Mercurial input attribute 'invalid'" + +testFetchTreeError \ + "\"hg+https://forge.tld/owner/repo?ref=,\"" \ + "invalid Mercurial branch/tag name ','" + +testFetchTreeError \ + "{ type = \"hg\"; url = \"https://forge.tld/owner/repo\"; ref = \",\"; }" \ + "invalid Mercurial branch/tag name ','" diff --git a/tests/functional/flakes/subdir-flake.sh b/tests/functional/flakes/subdir-flake.sh new file mode 100644 index 000000000..399518502 --- /dev/null +++ b/tests/functional/flakes/subdir-flake.sh @@ -0,0 +1,20 @@ +source common.sh +requireGit +clearStore + +container="$TEST_HOME"/flake-container +flake_dir="$container"/flake-dir + +createGitRepo "$container" +mkdir -p "$flake_dir" +writeSimpleFlake "$flake_dir" +git -C "$container" add flake-dir + +pushd "$flake_dir" &>/dev/null + info="$(nix flake info --json)" + [[ "$(jq -r '.resolvedUrl' <<<"$info")" == git+file://*/flake-container?dir=flake-dir ]] + [[ "$(jq -r '.url' <<<"$info")" == git+file://*/flake-container?dir=flake-dir ]] + + # Make sure we can actually access & build stuff in this flake. + nix build "path:$flake_dir#foo" -L +popd &>/dev/null diff --git a/tests/functional/meson.build b/tests/functional/meson.build index cbf6a1563..7a9c7182f 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -71,6 +71,7 @@ functional_tests_scripts = [ 'flakes/build-paths.sh', 'flakes/flake-registry.sh', 'flakes/flake-in-submodule.sh', + 'flakes/subdir-flake.sh', 'gc.sh', 'nix-collect-garbage-d.sh', 'nix-collect-garbage-dry-run.sh', @@ -94,6 +95,7 @@ functional_tests_scripts = [ 'fetchGitRefs.sh', 'gc-runtime.sh', 'tarball.sh', + 'fetchers.sh', 'fetchGit.sh', 'fetchurl.sh', 'fetchPath.sh', diff --git a/tests/functional/repl.sh b/tests/functional/repl.sh index cd56b4d92..22c69e20b 100644 --- a/tests/functional/repl.sh +++ b/tests/functional/repl.sh @@ -244,3 +244,30 @@ testReplResponseNoRegex ' y = { a = 1; }; } ' + +# Test that editing a store path does not reload... +echo '{ identity = a: a; }' > repl-test.nix +repl_test_store="$(nix-store --add repl-test.nix)" +EDITOR=true testReplResponseNoRegex " +a = ''test string that we'll grep later'' +:l $repl_test_store +:e identity +a +" "test string that we'll grep later" + +# ...even through symlinks +ln -s "$repl_test_store" repl-test-link.nix +EDITOR=true testReplResponseNoRegex " +a = ''test string that we'll grep later'' +:l repl-test-link.nix +:e identity +a +" "test string that we'll grep later" + +# Test that editing a local file does reload +EDITOR=true testReplResponseNoRegex " +a = ''test string that we'll grep later'' +:l repl-test.nix +:e identity +a +" "undefined variable" diff --git a/tests/functional/repl_characterization/meson.build b/tests/functional/repl_characterization/meson.build index 56410cfd2..79de9a5f5 100644 --- a/tests/functional/repl_characterization/meson.build +++ b/tests/functional/repl_characterization/meson.build @@ -7,6 +7,7 @@ repl_characterization_tester = executable( 'test-repl-characterization', repl_characterization_tester_sources, dependencies : [ + libasanoptions, liblixutil, liblixutil_test_support, sodium, diff --git a/tests/functional/test-libstoreconsumer/meson.build b/tests/functional/test-libstoreconsumer/meson.build index ad96aac12..63d0c97ac 100644 --- a/tests/functional/test-libstoreconsumer/meson.build +++ b/tests/functional/test-libstoreconsumer/meson.build @@ -2,6 +2,7 @@ libstoreconsumer_tester = executable( 'test-libstoreconsumer', 'main.cc', dependencies : [ + libasanoptions, liblixutil, liblixstore, sodium, diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix index 301eede46..20e66f6c1 100644 --- a/tests/nixos/default.nix +++ b/tests/nixos/default.nix @@ -155,4 +155,6 @@ in broken-userns = runNixOSTestFor "x86_64-linux" ./broken-userns.nix; coredumps = runNixOSTestFor "x86_64-linux" ./coredumps; + + io_uring = runNixOSTestFor "x86_64-linux" ./io_uring; } diff --git a/tests/nixos/io_uring/default.nix b/tests/nixos/io_uring/default.nix new file mode 100644 index 000000000..9cd445d6a --- /dev/null +++ b/tests/nixos/io_uring/default.nix @@ -0,0 +1,7 @@ +let + inherit (import ../util.nix) mkNixBuildTest; +in +mkNixBuildTest { + name = "io_uring"; + expressionFile = ./package.nix; +} diff --git a/tests/nixos/io_uring/package.nix b/tests/nixos/io_uring/package.nix new file mode 100644 index 000000000..8f980183a --- /dev/null +++ b/tests/nixos/io_uring/package.nix @@ -0,0 +1,19 @@ +{ runCommandCC }: +runCommandCC "io_uring-is-blocked" { } '' + cat > test.c <<EOF + #include <errno.h> + #include <sys/syscall.h> + #include <unistd.h> + + int main() { + int res = syscall(SYS_io_uring_setup, 0, NULL); + return res == -1 && errno == ENOSYS ? 0 : 1; + } + EOF + "$CC" -o test test.c + if ! ./test; then + echo "Oh no! io_uring is available!" + exit 1 + fi + touch "$out" +'' diff --git a/tests/nixos/setuid/fchmodat2-suid.c b/tests/nixos/setuid/fchmodat2-suid.c index 931489ad7..7280331d5 100644 --- a/tests/nixos/setuid/fchmodat2-suid.c +++ b/tests/nixos/setuid/fchmodat2-suid.c @@ -12,10 +12,7 @@ int main(void) { fprintf(fd, "henlo :3"); fclose(fd); - // FIXME use something nicer here that's less - // platform-dependent as soon as we go to 24.05 - // and the glibc is new enough to support fchmodat2 - long rs = syscall(452, NULL, name, S_ISUID, 0); + long rs = syscall(SYS_fchmodat2, NULL, name, S_ISUID, 0); assert(rs == -1); assert(errno == EPERM); } diff --git a/tests/nixos/tarball-flakes.nix b/tests/nixos/tarball-flakes.nix index ca7627bd1..5deba4a12 100644 --- a/tests/nixos/tarball-flakes.nix +++ b/tests/nixos/tarball-flakes.nix @@ -69,7 +69,7 @@ in # Check that we got redirected to the immutable URL. locked_url = info["locked"]["url"] - assert locked_url == "http://localhost/stable/${nixpkgs.rev}.tar.gz", f"{locked_url=} != http://localhost/stable/${nixpkgs.rev}.tar.gz" + assert locked_url == "http://localhost/stable/${nixpkgs.rev}.tar.gz?rev=${nixpkgs.rev}&revCount=1234", f"{locked_url=} != http://localhost/stable/${nixpkgs.rev}.tar.gz" # Check that we got the rev and revCount attributes. revision = info["revision"] diff --git a/tests/unit/meson.build b/tests/unit/meson.build index c449b2276..55c7566bd 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -11,6 +11,10 @@ # functions, the result would be way less readable than just a bit of copypasta. # It's only ~200 lines; better to just refactor the tests themselves which we'll want to do anyway. +default_test_env = { + 'ASAN_OPTIONS': 'detect_leaks=0:halt_on_error=1:abort_on_error=1:print_summary=1:dump_instruction_bytes=1' +} + libutil_test_support_sources = files( 'libutil-support/tests/cli-literate-parser.cc', 'libutil-support/tests/hash.cc', @@ -63,6 +67,7 @@ libutil_tester = executable( 'liblixutil-tests', libutil_tests_sources, dependencies : [ + libasanoptions, rapidcheck, gtest, boehm, @@ -78,7 +83,7 @@ test( 'libutil-unit-tests', libutil_tester, args : tests_args, - env : { + env : default_test_env + { '_NIX_TEST_UNIT_DATA': meson.project_source_root() / 'tests/unit/libutil/data', }, suite : 'check', @@ -132,6 +137,7 @@ libstore_tester = executable( 'liblixstore-tests', libstore_tests_sources, dependencies : [ + libasanoptions, liblixstore_test_support, liblixutil_test_support, liblixstore_mstatic, @@ -147,7 +153,7 @@ test( 'libstore-unit-tests', libstore_tester, args : tests_args, - env : { + env : default_test_env + { '_NIX_TEST_UNIT_DATA': meson.project_source_root() / 'tests/unit/libstore/data', }, suite : 'check', @@ -196,6 +202,7 @@ libexpr_tester = executable( 'liblixexpr-tests', libexpr_tests_sources, dependencies : [ + libasanoptions, liblixexpr_test_support, liblixstore_test_support, liblixstore_mstatic, @@ -214,7 +221,7 @@ test( 'libexpr-unit-tests', libexpr_tester, args : tests_args, - env : { + env : default_test_env + { '_NIX_TEST_UNIT_DATA': meson.project_source_root() / 'tests/unit/libexpr/data', }, suite : 'check', @@ -226,6 +233,7 @@ libcmd_tester = executable( 'liblixcmd-tests', files('libcmd/args.cc'), dependencies : [ + libasanoptions, liblixcmd, liblixutil, liblixmain, @@ -241,7 +249,7 @@ test( 'libcmd-unit-tests', libcmd_tester, args : tests_args, - env : { + env : default_test_env + { # No special meaning here, it's just a file laying around that is unlikely to go anywhere # any time soon. '_NIX_TEST_UNIT_DATA': meson.project_source_root() / 'src/nix-env/buildenv.nix', @@ -272,6 +280,7 @@ test( 'libmain-unit-tests', libmain_tester, args : tests_args, + env : default_test_env, suite : 'check', protocol : 'gtest', ) diff --git a/version.json b/version.json index 48db2994f..809358e6d 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,5 @@ { "version": "2.91.0-dev", + "official_release": false, "release_name": "TBA" } |