Skip to content

Commit

Permalink
🔨 ⚡ 🦀 🔒 Make rust sandbox safe with crates
Browse files Browse the repository at this point in the history
This adds a set of rust crates, under base.languages.rust.crates, and
tooling to extend, override or replace that set. This set is built from
fixed output derivations so rust will no longer access crates.io during
build. It also simplifies the logic by treating external and internal
crates the same. It also removes a limitation where users could not
create a custom .cargo/config.toml file.

Removed the test for internal dependencies, because the use of
setupHooks the vendoring happens build time and the eval time test can't
verify anything.
  • Loading branch information
simonrainerson committed Sep 5, 2023
1 parent e5957b1 commit eeb6074
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 132 deletions.
36 changes: 35 additions & 1 deletion docs/src/rust/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,41 @@
- `mkComponent` is made for building extensions where you can create your own
Nedryland component types by passing in the `nedrylandType` string.

## Specifying Dependencies
## Specifying Crate Dependencies

In order to be deterministic crates are downloaded and verified ahead of building, and
these crates need to be made available to the build environment. This is done through
`buildInputs`. Crates can be be defined using
`base.languages.rust.fetchCrate {name = "some-crate-name"; version = "x.y.z"; sha256 = "sha"; deps = [crate]}`
where sha256 is the same as you will find on
[crates index](https://github.com/rust-lang/crates.io-index) and deps is a list of these
kind of derivations.

To ease writing of multiple such expressions there's a CLI tool
`gen-crate-expression` both in each rust shell and as an app in Nedryglot's
flake. Use `gen-crate-expression --help` for usage information.

A set of crates has also been generated and ships with nedryglot under
`base.languages.rust.crates`. These are roughly equivalent to the crates
available in [Rust Playground](https://play.rust-lang.org/). This default crate
set can be overridden by making an extension. This extension overrides the crate
set with a set of one crate:
```nix
{ base }:
{
languages.rust = base.languages.rust.overrideAttr {
crates = {
gimli = base.languages.rust.fetchCrate {
name="gimli";
version="0.27.3";
sha256="b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"; deps=[];
};
};
};
}
```

## Specifying Other Dependencies

Specifying dependencies in a Rust component can be done in two ways. First option is to
assign any of the standard `mkDerivation` arguments a list with the needed inputs.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/rust/override.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Overriding the Rust Version

`base.languages.rust` exposes the function `overrideRust` to change the
`base.languages.rust` exposes the function `mkRustToolset` to change the
build/lint tools. This function takes a number of required arguments to override
the specific tools. The non-overriden tools are the following:

Expand Down
7 changes: 6 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@
(import ./test.nix nedry).all;
};

apps = nedryland.apps.${system};
apps = nedryland.apps.${system} // {
gen-crate-expression = {
type = "app";
program = ''${pkgs.python3}/bin/python ${./rust/gen-crates-expr.py} "$@"}'';
};
};
}
);
}
Expand Down
9 changes: 0 additions & 9 deletions rust/cargo-internal.config.toml

This file was deleted.

18 changes: 18 additions & 0 deletions rust/crate-setup-hook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#! /usr/bin/env bash

crates="$NIX_BUILD_TOP"/.nedryglot/.dependency-crates
name="$crates/$(echo "@out@" | sed -E 's|/nix/store/\w{32}-||')"
mkdir -p "$crates"
if [ ! -e "@out@"/Cargo.toml ]; then
for crate in "@out@"/*; do
if [ ! -e "$crates/$(basename "$crate")" ] && [ -f "$crate"/Cargo.toml ]; then
ln -s "$crate" "$crates/$(basename "$crate")"
fi
done
elif [ ! -e "$name" ]; then
ln -s "@out@" "$name"
fi

echo "[source.crates-io]
directory=\"$NIX_BUILD_TOP/.nedryglot/.dependency-crates\"" >"$NIX_BUILD_TOP"/.nedryglot/config.toml
export CARGO_HOME="$NIX_BUILD_TOP"/.nedryglot
140 changes: 140 additions & 0 deletions rust/default-crates.nix

Large diffs are not rendered by default.

43 changes: 28 additions & 15 deletions rust/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ let
, callPackage
, cargo
, clippy
, coreutils
, jq
, lib
, makeSetupHook
, j2cli
, pkgs
, python3
Expand All @@ -17,8 +15,10 @@ let
, rustfmt
, stdenv
, symlinkJoin
, fetchurl
, tonicVersion ? "0.7.2"
, xdg-utils
, crates ? null
, crossTargets ? { }
, extraAttrs ? { }
}:
Expand Down Expand Up @@ -180,15 +180,6 @@ let
package.overrideAttrs (
oldAttrs: {

nativeBuildInputs = oldAttrs.nativeBuildInputs
++ [
(makeSetupHook
{
name = "generate-cargo-checksums";
deps = [ jq coreutils ];
} ./generateCargoChecksums.sh)
];

buildPhase = ''
runHook preBuild
cargo package --no-verify --no-metadata
Expand All @@ -198,13 +189,16 @@ let
installPhase = ''
runHook preInstall
${oldAttrs.installPhase or ""}
mkdir -p $out/src/rust
mkdir -p $out
for crate in target/package/*.crate; do
tar -xzf $crate -C $out/src/rust
tar -xzf $crate -C $out
echo "{\"files\":{},\"package\":\"$(sha256sum $crate | grep -E -o '^(\w*)')\"}" >$out/"$(basename "''${crate//.crate/}")"/.cargo-checksum.json
done
runHook postInstall
'';

setupHook = ./crate-setup-hook.sh;
}
);

Expand All @@ -216,9 +210,29 @@ let
addAttributes = f: inner (args // {
extraAttrs = (extraAttrs // (f extraAttrs));
});

fetchCrate = { name, version, sha256, deps ? [ ] }:
stdenv.mkDerivation rec{
inherit version;
pname = name;
propagatedBuildInputs = deps;
src = fetchurl {
name = "${name}.tar.gz";
inherit sha256;
url = "https://crates.io/api/v1/crates/${name}/${version}/download";
};
installPhase = ''
mkdir $out
cp -r . $out
echo "{\"files\":{},\"package\":\"$(sha256sum ${src} | grep -E -o '^(\w*)')\"}" >$out/.cargo-checksum.json
'';
setupHook = ./crate-setup-hook.sh;
};

in
extraAttrs // {
inherit overrideAttrs mkRustToolset mkCrossTarget overrideCrossTargets toApplication toLibrary mkLibrary addAttributes toRustTarget;
inherit overrideAttrs mkRustToolset mkCrossTarget overrideCrossTargets toApplication toLibrary mkLibrary addAttributes toRustTarget fetchCrate;
crates = if crates == null then import ./default-crates.nix fetchCrate else crates;
crossTargets = crossTargets' // {
override = overrideCrossTargets;
};
Expand All @@ -243,7 +257,6 @@ let

defaultVersion = { inherit rustc cargo; };


mkClient = mkComponentWith base.mkClient toApplication;

mkService = mkComponentWith base.mkService toApplication;
Expand Down
132 changes: 132 additions & 0 deletions rust/gen-crates-expr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from pathlib import Path
import argparse
import io
import json
import sys
import tarfile
import tempfile
import typing
import urllib.request


FETCH_CRATE_EXPR = ' {name} = fetchCrate {{ name="{name}"; version="{version}"; sha256="{sha256}"; deps=[{deps}];}};'


def comparable_version(semver: str) -> str:
return ".".join(map(lambda i: i.zfill(4), semver.split(".")))


def to_nix_expression(content: io.BufferedReader, version: typing.Optional[str], include_optional_deps: bool) -> (str, typing.List[str]):
"""Generate a nix expression from a file inside a tar archive.
Args:
content: A file inside a tar archive.
version: Use a specific version instead of latest.
include_optional_deps: If optional dependencies should be returned in the deps list
Returns:
(str, list): The nix expression to fetch the crate and a list of dependencies.
"""
versions = map(json.loads, content.readlines())
for version_info in sorted(versions, key=lambda v: comparable_version(v.get("vers")), reverse=True):
if version is not None and version_info["vers"] != version:
continue
if not version_info["yanked"]:
deps = set(
map(
lambda dep: dep.get("package", dep["name"]),
filter(
lambda dep: True if dep.get("optional", False) and include_optional_deps else not dep.get("optional", False),
filter(
lambda dep: dep.get("kind") in ["normal", "build"] and dep.get("name") != version_info["name"],
version_info["deps"]
)
)
)
)
return (
FETCH_CRATE_EXPR.format(
name=version_info["name"],
version=version_info["vers"],
sha256=version_info["cksum"],
deps=" ".join(deps),
),
list(deps),
)
return ("", [])

def is_crate(tar_member: tarfile.TarInfo) -> bool:
"""Filter tar member to only include files describing crates.
Exclude everything in the .github folder, everything with an extension and
directories.
Args:
tar_member: The object representing a file in the tar archive.
Returns:
bool: whether a file is a crate description.
"""
return (
tar_member.isreg()
and tar_member.name.split("/")[1] != ".github"
and "." not in tar_member.name.split("/")[-1]
)

def main(github_ref: str, crates: typing.List[str], output: typing.Optional[Path], include_deps: bool, optional_deps: bool, silent: bool) -> None:
"""Generate a nix expression to fetch crates from crates.io index.
Args:
github_ref: The ref to use on creates.io-index.
creates: The list of crate names to look up.
output: An optional file to output to instead of stdout.
include_deps: If nix expressions should be generated for dependencies.
optional_deps: If optional dependencies should be considered.
silent: If progress and messages should be omitted.
"""

visited_crates = []
crates_to_visit = crates
result = []

if not silent:
print(f"Downloading crates index at {github_ref}...", file=sys.stderr)
with urllib.request.urlopen(f"https://github.com/rust-lang/crates.io-index/tarball/{github_ref}") as tarball:
with tempfile.NamedTemporaryFile("w+b") as temp_tar:
temp_tar.write(tarball.read())
with tarfile.open(temp_tar.name) as tar:
for crate in crates_to_visit:
if not silent:
print(f"Looking for {crate}...", file=sys.stderr)
visited_crates.append(crate)
for tar_member in filter(is_crate, tar.getmembers()):
if tar_member.name.split("/")[-1] == crate.split(":")[0]:
version = crate.split(":")[1] if ":" in crate else None
expr, deps = to_nix_expression(tar.extractfile(tar_member.name), version, optional_deps)
if expr:
result.append(expr)
if include_deps and deps:
crates_to_visit.extend([c for c in deps if c not in visited_crates and c not in crates_to_visit])
break

expression = "fetchCrate: rec{\n" + "\n\n".join(result) + "\n}"

if output:
with open(output, "w") as out:
out.write(expression)
else:
print(expression)


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate a nix expression to fetch rust crates when building with Nedryglot's rust language tooling.")
parser.add_argument("--ref", type=str, help="Git ref of https://github.com/rust-lang/crates.io-index to use for crate lookup, if omitted master is used.", default="master")
parser.add_argument("--output", type=Path, help="Write output to a file instead of stdout.")
parser.add_argument("--no-deps", action="store_true", help="Turn off dependency traversal and only download the listed crates.")
parser.add_argument("--optional-deps", action="store_true", help="Include optional dependencies.")
parser.add_argument("--silent", action="store_true", help="Do not print progress and info to stderr.")
parser.add_argument("crates", nargs="*", help="A space separated list of crates to generate expressions for. Use name:version to fetch a specific version.", default=[])
args = parser.parse_args()
main(args.ref, args.crates, args.output, not args.no_deps, args.optional_deps, args.silent)
sys.exit(0)

15 changes: 0 additions & 15 deletions rust/generateCargoChecksums.sh

This file was deleted.

11 changes: 0 additions & 11 deletions rust/internalDepsSetupHook.sh

This file was deleted.

Loading

0 comments on commit eeb6074

Please sign in to comment.