Documentation

menu
HostingIntroductionBranch HostingPull-request DeploymentsSecretsPersistenceDebuggingMultiple ServersCustom DomainsRaw DomainsExample: Typescript & GoExample: JitsiExample: RSS BridgeExample: Tiny Tiny RSSExample: SearxActionsInstalling Nixyaml configNix FlakesWhat Garnix CI doesIncremental BuildsUsing Private InputsCachingEnterprise installationsGitHub Actions IntegrationBadgesFOD ChecksGarnModulesNodejsHaskellRustPostgreSQLUseropen source

FOD Checksshare

TL;DRshare

You can enable fodChecks: true in your garnix yaml config if you have a paid garnix account. This will enable a CI run that checks that, for all FODs in your dependency graph, the builder scripts and the output hashes are in sync. Running FOD checks prevents security vulnerabilities described below, and in more depth in our blog post.

The Longer Explanationshare

When you're using Nix's Fixed-Output Derivations (FODs), as part of the declaration of the FOD, you have to specify an output hash of the derivation's store output. When you build the derivation for the first time, Nix will run the build script and check the contents written to the output against the given hash. If they match, the build is considered successful, and the output is added to the nix store.

If afterwards you build another FOD, with a different build script but with the same output hash, then nix will not run the different build script again. Instead it will decide that it doesn't need to, since it already has the store path with the specified hash and it will happily use that.

Exampleshare

Here's flake file to demonstrate the behavior:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
  };
  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in
    {
      packages.${system} = {
        valid = derivation {
          name = "source";
          outputHash = "sha256-nhfLFd11u71du5hO2mdIY8OxCrcmE8+KOaAMPhGoSSo=";
          outputHashAlgo = "sha256";
          builder = "${pkgs.bash}/bin/bash";
          system = "x86_64-linux";
          args = [
            "-c"
            ''
              ${pkgs.curl}/bin/curl 'http://garnix.io' > $out
            ''
          ];
        };
        lying = derivation {
          name = "source";
          # same hash as above
          outputHash = "sha256-nhfLFd11u71du5hO2mdIY8OxCrcmE8+KOaAMPhGoSSo=";
          outputHashAlgo = "sha256";
          builder = "${pkgs.bash}/bin/bash";
          system = "x86_64-linux";
          args = [
            "-c"
            ''
              ${pkgs.curl}/bin/curl 'http://some-other-site.io' > $out
            ''
          ];
        };
      };
    };
}

In a directory with that example saved as flake.nix, if you first run

nix build -L .#valid

then Nix will build the valid package, which will work, since the hash matches what garnix.io returns. If afterwards you run

nix build -L .#lying

Nix will not run the build script, but instead just re-use the storepath built for the valid package, since the outputHash is the same.

Consequencesshare

  • Sometimes you change a derivation's build script, but forget to update the hash. Nix won't complain, but your changes just don't have any effect. This bites newcomers regularly and can take quite a long time to debug. It is overall pretty annoying, but has mostly fairly benign consequences.

  • A malicious attacker can use the behavior to sneak things passed PR reviewers, since human PR reviewers seldomly verify that the outputHash of FODs is correct. Conceivable attacks are to re-use a completely unrelated, previously introduced package as a different one. Or to pretend using a newer version of some software that includes crucial security fixes while in reality re-using an older version. This can happen not just in your code, but also in your dependecies, including Nixpkgs.

For a more in-depth discussion about possible exploits see our blog post on the topic.

Mitigationshare

garnix has implemented a CI check that will:

  • analyze the transitive dependency graph of the packages that garnix builds,
  • for any FODs in the graph run the build script once,
  • check that the output actually matches the declared outputHash.

It saves all successfully verified FODs in a database, to avoid having to re-check all FODs constantly.

Since the FOD check can be fairly resource-hungry, this is only available on paid accounts. To enable FOD checks for your repo add

fodChecks: true

to your garnix yaml config.

Note that the FOD check produces false positives (e.g. in case an upstream server is temporarily down or has expired SSL certificates) or flags hash mismatches that weren't introduces maliciously, but are honest mistakes. So interpreting any failed checks requires some manual effort.