├── .gitignore ├── .github └── workflows │ ├── update-flake-lock.yml │ ├── nix.yml │ └── flakehub-publish-rolling.yml ├── default.nix ├── src ├── bubblewrap-insecure.patch ├── test │ ├── pinned-snap-versions.toml │ └── default.nix ├── nixos-module.nix ├── package.nix └── nixify.patch ├── flake.nix ├── LICENSE ├── README.md └── flake.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /result 2 | /.nixos-test-history 3 | -------------------------------------------------------------------------------- /.github/workflows/update-flake-lock.yml: -------------------------------------------------------------------------------- 1 | name: Update flake.lock 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 0 * * 0' 6 | 7 | jobs: 8 | lockfile: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: DeterminateSystems/nix-installer-action@main 13 | - uses: DeterminateSystems/update-flake-lock@main 14 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import ( 2 | let 3 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 4 | in 5 | fetchTarball { 6 | url = 7 | lock.nodes.flake-compat.locked.url 8 | or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 9 | sha256 = lock.nodes.flake-compat.locked.narHash; 10 | } 11 | ) { src = ./.; }).defaultNix 12 | -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: Build and test flake 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: DeterminateSystems/nix-installer-action@main 17 | - uses: DeterminateSystems/magic-nix-cache-action@main 18 | - uses: DeterminateSystems/flake-checker-action@main 19 | - run: nix build --print-build-logs 20 | - run: nix flake check --print-build-logs 21 | -------------------------------------------------------------------------------- /src/bubblewrap-insecure.patch: -------------------------------------------------------------------------------- 1 | diff --git a/bubblewrap.c b/bubblewrap.c 2 | index f8728c7..964c595 100644 3 | --- a/bubblewrap.c 4 | +++ b/bubblewrap.c 5 | @@ -2904,10 +2904,6 @@ main (int argc, 6 | /* Get the (optional) privileges we need */ 7 | acquire_privs (); 8 | 9 | - /* Never gain any more privs during exec */ 10 | - if (prctl (PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) 11 | - die_with_error ("prctl(PR_SET_NO_NEW_PRIVS) failed"); 12 | - 13 | /* The initial code is run with high permissions 14 | (i.e. CAP_SYS_ADMIN), so take lots of care. */ 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/flakehub-publish-rolling.yml: -------------------------------------------------------------------------------- 1 | name: "Publish every Git push to main to FlakeHub" 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | jobs: 7 | flakehub-publish: 8 | runs-on: "ubuntu-latest" 9 | permissions: 10 | id-token: "write" 11 | contents: "read" 12 | steps: 13 | - uses: "actions/checkout@v3" 14 | - uses: "DeterminateSystems/nix-installer-action@main" 15 | - uses: "DeterminateSystems/flakehub-push@main" 16 | with: 17 | name: "nix-community/nix-snapd" 18 | rolling: true 19 | visibility: "public" 20 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Snap package for Nix and NixOS"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs"; 6 | flake-parts.url = "github:hercules-ci/flake-parts"; 7 | flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; 8 | }; 9 | 10 | outputs = 11 | inputs@{ self, flake-parts, ... }: 12 | flake-parts.lib.mkFlake { inherit inputs; } { 13 | flake.nixosModules.default = import ./src/nixos-module.nix self; 14 | systems = [ 15 | "x86_64-linux" 16 | "aarch64-linux" 17 | ]; 18 | perSystem = 19 | { pkgs, ... }: 20 | { 21 | packages.default = pkgs.callPackage ./src/package.nix { }; 22 | checks.test = import ./src/test { inherit self pkgs; }; 23 | }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/test/pinned-snap-versions.toml: -------------------------------------------------------------------------------- 1 | [x86_64-linux] 2 | hash = "sha256-B/iV42aWorzqU27LGDCCorR/JLw3yz9Xi9P3fw/CdMo=" 3 | snaps = [ 4 | { name = "snapd", rev = 23258 }, 5 | { name = "bare", rev = 5 }, 6 | { name = "core", rev = 16928 }, 7 | { name = "core20", rev = 2318 }, 8 | { name = "core22", rev = 1380}, 9 | { name = "gnome-42-2204", rev = 176 }, 10 | { name = "gtk-common-themes", rev = 1535 }, 11 | { name = "gnome-calculator", rev = 955 }, 12 | { name = "hello-world", rev = 29 }, 13 | ] 14 | 15 | [aarch64-linux] 16 | hash = "sha256-mrw+15QmGGC4JphzZijB942ef6j47wZiicWx0RUecro=" 17 | snaps = [ 18 | { name = "snapd", rev = 23259 }, 19 | { name = "bare", rev = 5 }, 20 | { name = "core", rev = 16931 }, 21 | { name = "core20", rev = 2321 }, 22 | { name = "core22", rev = 1383 }, 23 | { name = "gnome-42-2204", rev = 178 }, 24 | { name = "gtk-common-themes", rev = 1535 }, 25 | { name = "gnome-calculator", rev = 956 }, 26 | { name = "hello-world", rev = 29 }, 27 | ] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Benjamin Levy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nix-snapd 2 | 3 | Snap package for Nix and NixOS 4 | 5 | ## Installation 6 | 7 | ### Flakes 8 | 9 | Example minimal `/etc/nixos/flake.nix`: 10 | 11 | ``` nix 12 | { 13 | description = "NixOS configuration"; 14 | 15 | inputs = { 16 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 17 | nix-snapd.url = "github:nix-community/nix-snapd"; 18 | nix-snapd.inputs.nixpkgs.follows = "nixpkgs"; 19 | }; 20 | 21 | outputs = { nixpkgs, nix-snapd }: { 22 | nixosConfigurations.my-hostname = nixpkgs.lib.nixosSystem { 23 | system = "x86_64-linux"; 24 | modules = [ 25 | nix-snapd.nixosModules.default 26 | { 27 | services.snap.enable = true; 28 | } 29 | ]; 30 | }; 31 | }; 32 | } 33 | ``` 34 | 35 | ### Channels 36 | 37 | Add a `nix-snapd` channel with 38 | 39 | ``` sh 40 | sudo nix-channel --add https://github.com/nix-community/nix-snapd/archive/main.tar.gz nix-snapd 41 | sudo nix-channel --update 42 | ``` 43 | 44 | Then make the following modification to `/etc/nixos/configuration.nix`: 45 | 46 | ``` nix 47 | { ... }: 48 | 49 | { 50 | imports = [ (import ).nixosModules.default ]; 51 | 52 | services.snap.enable = true; 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /src/nixos-module.nix: -------------------------------------------------------------------------------- 1 | self: 2 | 3 | { 4 | pkgs, 5 | config, 6 | lib, 7 | ... 8 | }: 9 | 10 | let 11 | cfg = config.services.snap; 12 | snap = self.packages.${pkgs.stdenv.system}.default; 13 | in 14 | { 15 | options.services.snap = { 16 | enable = lib.mkEnableOption "snap service"; 17 | 18 | snapBinInPath = lib.mkOption { 19 | default = true; 20 | example = false; 21 | description = "Include /snap/bin in PATH."; 22 | type = lib.types.bool; 23 | }; 24 | 25 | desktopFiles = lib.mkOption { 26 | default = true; 27 | example = false; 28 | description = "Add desktop files for opening snaps in desktop environments."; 29 | type = lib.types.bool; 30 | }; 31 | }; 32 | 33 | config = lib.mkIf cfg.enable { 34 | environment.systemPackages = [ snap ]; 35 | 36 | environment.extraInit = '' 37 | ${lib.optionalString cfg.snapBinInPath '' 38 | export PATH="/snap/bin:$PATH" 39 | ''} 40 | 41 | ${lib.optionalString cfg.desktopFiles '' 42 | export XDG_DATA_DIRS="/var/lib/snapd/desktop:$XDG_DATA_DIRS" 43 | ''} 44 | ''; 45 | 46 | systemd = { 47 | packages = [ snap ]; 48 | sockets.snapd.wantedBy = [ "sockets.target" ]; 49 | services.snapd.wantedBy = [ "multi-user.target" ]; 50 | }; 51 | 52 | security.wrappers.snap-confine-setuid-wrapper = { 53 | setuid = true; 54 | owner = "root"; 55 | group = "root"; 56 | source = "${snap}/libexec/snapd/snap-confine-stage-1"; 57 | }; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "locked": { 5 | "lastModified": 1733328505, 6 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 7 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 8 | "revCount": 69, 9 | "type": "tarball", 10 | "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" 11 | }, 12 | "original": { 13 | "type": "tarball", 14 | "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" 15 | } 16 | }, 17 | "flake-parts": { 18 | "inputs": { 19 | "nixpkgs-lib": "nixpkgs-lib" 20 | }, 21 | "locked": { 22 | "lastModified": 1760948891, 23 | "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", 24 | "owner": "hercules-ci", 25 | "repo": "flake-parts", 26 | "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "owner": "hercules-ci", 31 | "repo": "flake-parts", 32 | "type": "github" 33 | } 34 | }, 35 | "nixpkgs": { 36 | "locked": { 37 | "lastModified": 1761442529, 38 | "narHash": "sha256-8aDps5fCt0Ndw56ZgeBvdT7E5zeUSFi3CJaNR7ZJKnA=", 39 | "owner": "nixos", 40 | "repo": "nixpkgs", 41 | "rev": "75762615e96b1a7f172dcdadf62aa9f3aebedf7b", 42 | "type": "github" 43 | }, 44 | "original": { 45 | "owner": "nixos", 46 | "repo": "nixpkgs", 47 | "type": "github" 48 | } 49 | }, 50 | "nixpkgs-lib": { 51 | "locked": { 52 | "lastModified": 1754788789, 53 | "narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", 54 | "owner": "nix-community", 55 | "repo": "nixpkgs.lib", 56 | "rev": "a73b9c743612e4244d865a2fdee11865283c04e6", 57 | "type": "github" 58 | }, 59 | "original": { 60 | "owner": "nix-community", 61 | "repo": "nixpkgs.lib", 62 | "type": "github" 63 | } 64 | }, 65 | "root": { 66 | "inputs": { 67 | "flake-compat": "flake-compat", 68 | "flake-parts": "flake-parts", 69 | "nixpkgs": "nixpkgs" 70 | } 71 | } 72 | }, 73 | "root": "root", 74 | "version": 7 75 | } 76 | -------------------------------------------------------------------------------- /src/test/default.nix: -------------------------------------------------------------------------------- 1 | { self, pkgs }: 2 | 3 | let 4 | nixos-lib = import "${pkgs.path}/nixos/lib" { }; 5 | 6 | system = pkgs.stdenv.system; 7 | 8 | snap = self.packages.${system}.default; 9 | 10 | pinnedSnapVersions = (pkgs.lib.importTOML ./pinned-snap-versions.toml).${system}; 11 | 12 | # Download tested snaps with a fixed-output derivation because the test runner 13 | # normally doesn't have internet access 14 | downloadedSnaps = 15 | pkgs.runCommand "${system}-downloaded-snaps" 16 | { 17 | buildInputs = [ 18 | snap 19 | pkgs.squashfsTools 20 | ]; 21 | outputHashMode = "recursive"; 22 | outputHashAlgo = "sha256"; 23 | outputHash = pinnedSnapVersions.hash; 24 | } 25 | '' 26 | mkdir $out 27 | cd $out 28 | ${pkgs.lib.concatMapStrings ( 29 | { name, rev, ... }: 30 | '' 31 | snap download ${name} --revision=${toString rev} 32 | '' 33 | ) pinnedSnapVersions.snaps} 34 | ''; 35 | in 36 | nixos-lib.runTest { 37 | name = "snap"; 38 | hostPkgs = pkgs; 39 | 40 | nodes.machine = { 41 | imports = [ 42 | (import "${pkgs.path}/nixos/tests/common/user-account.nix") 43 | (import "${pkgs.path}/nixos/tests/common/x11.nix") 44 | self.nixosModules.default 45 | ]; 46 | virtualisation.diskSize = 2048; 47 | test-support.displayManager.auto.user = "alice"; 48 | services.snap.enable = true; 49 | }; 50 | 51 | enableOCR = true; 52 | 53 | testScript = '' 54 | # Check version 55 | assert "${snap.version}" in machine.succeed("snap --version") 56 | 57 | # Ensure snap programs aren't already installed 58 | machine.fail("hello-world") 59 | machine.fail("gnome-calculator") 60 | 61 | # Install snaps 62 | ${pkgs.lib.concatMapStrings ( 63 | { 64 | name, 65 | rev, 66 | classic ? false, 67 | }: 68 | let 69 | path = "${downloadedSnaps}/${name}_${toString rev}"; 70 | classicFlag = pkgs.lib.optionalString classic "--classic"; 71 | in 72 | '' 73 | machine.succeed("snap ack ${path}.assert") 74 | machine.succeed("snap install ${classicFlag} ${path}.snap") 75 | '' 76 | ) pinnedSnapVersions.snaps} 77 | 78 | def run(): 79 | machine.wait_for_unit("snapd.service") 80 | 81 | assert machine.succeed("hello-world") == "Hello World!\n" 82 | assert machine.succeed("su - alice -c hello-world") == "Hello World!\n" 83 | 84 | # Test gnome-calculator snap 85 | machine.wait_for_x() 86 | machine.succeed("su - alice -c '${pkgs.xorg.xhost}/bin/xhost si:localuser:alice'") 87 | machine.succeed("su - alice -c '${pkgs.xorg.xhost}/bin/xhost si:localuser:root'") 88 | assert "Basic" not in machine.get_screen_text() 89 | machine.execute("su - alice -c gnome-calculator >&2 &") 90 | machine.wait_for_text("Basic") 91 | assert "Basic" in machine.get_screen_text() 92 | machine.screenshot("gnome-calculator") 93 | 94 | # Ensure programs run after a crash or clean reboot 95 | run() 96 | machine.crash() 97 | run() 98 | machine.shutdown() 99 | run() 100 | 101 | # Ensure uninstalling snaps works 102 | machine.succeed("snap remove hello-world") 103 | machine.fail("hello-world") 104 | ''; 105 | } 106 | -------------------------------------------------------------------------------- /src/package.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | lib, 4 | stdenv, 5 | python3, 6 | xdg-utils, 7 | writeTextDir, 8 | fetchFromGitHub, 9 | buildGoModule, 10 | buildFHSEnvBubblewrap, 11 | bubblewrap, 12 | }: 13 | 14 | let 15 | version = "2.67"; 16 | 17 | src = fetchFromGitHub { 18 | owner = "canonical"; 19 | repo = "snapd"; 20 | rev = version; 21 | hash = "sha256-WiUgLV8/Luxb3T9u1nT/rCk8YduzyyjPaCuiJszuEZU="; 22 | }; 23 | 24 | goModules = 25 | (buildGoModule { 26 | pname = "snap-go-mod"; 27 | inherit version src; 28 | vendorHash = "sha256-A/L4Bnx0MIvOUedF8MojXwyE09i0cImrz5fR4zqRWxM="; 29 | }).goModules; 30 | 31 | insecureBubblewrap = bubblewrap.overrideAttrs (o: { 32 | patches = (o.patches or [ ]) ++ [ ./bubblewrap-insecure.patch ]; 33 | }); 34 | 35 | buildFHSEnvInsecureBubblewrap = buildFHSEnvBubblewrap.override { 36 | bubblewrap = insecureBubblewrap; 37 | }; 38 | 39 | env = buildFHSEnvInsecureBubblewrap { 40 | name = "snap-env"; 41 | extraBwrapArgs = [ 42 | "--ro-bind /etc/pam.d /etc/pam.d" 43 | "--ro-bind /etc/pam /etc/pam" 44 | ]; 45 | targetPkgs = 46 | pkgs: 47 | (with pkgs; [ 48 | # Snapd calls 49 | util-linux.mount 50 | squashfsTools 51 | systemd 52 | openssh 53 | gnutar 54 | gzip 55 | # TODO: xdelta 56 | 57 | # Snap hook calls 58 | bash 59 | sudo 60 | gawk 61 | 62 | # Mount wrapper calls 63 | coreutils 64 | ]); 65 | }; 66 | in 67 | stdenv.mkDerivation { 68 | pname = "snap"; 69 | inherit version src; 70 | 71 | nativeBuildInputs = with pkgs; [ 72 | makeWrapper 73 | autoconf 74 | automake 75 | autoconf-archive 76 | ]; 77 | 78 | buildInputs = with pkgs; [ 79 | go 80 | glibc 81 | glibc.static 82 | pkg-config 83 | libseccomp 84 | libxfs 85 | libcap 86 | glib 87 | udev 88 | libapparmor 89 | ]; 90 | 91 | patches = [ ./nixify.patch ]; 92 | 93 | configurePhase = '' 94 | substituteInPlace $(grep -rl '@out@') --subst-var 'out' 95 | 96 | export GOCACHE=$TMPDIR/go-cache 97 | 98 | ln -s ${goModules} vendor 99 | 100 | ./mkversion.sh $version 101 | 102 | ( 103 | cd cmd 104 | autoreconf -i -f 105 | ./configure \ 106 | --prefix=$out \ 107 | --libexecdir=$out/libexec/snapd \ 108 | --with-snap-mount-dir=/snap \ 109 | --enable-apparmor \ 110 | --enable-nvidia-biarch \ 111 | --enable-merged-usr 112 | ) 113 | 114 | mkdir build 115 | cd build 116 | ''; 117 | 118 | makeFlagsPackaging = [ 119 | "--makefile=../packaging/snapd.mk" 120 | "SNAPD_DEFINES_DIR=${writeTextDir "snapd.defines.mk" ""}" 121 | "snap_mount_dir=$(out)/snap" 122 | "bindir=$(out)/bin" 123 | "sbindir=$(out)/sbin" 124 | "libexecdir=$(out)/libexec" 125 | "mandir=$(out)/share/man" 126 | "datadir=$(out)/share" 127 | "localstatedir=$(TMPDIR)/localstatedir" 128 | "sharedstatedir=$(TMPDIR)/sharedstatedir" 129 | "unitdir=$(out)/unitdir" 130 | "builddir=." 131 | "with_testkeys=1" 132 | "with_apparmor=1" 133 | "with_core_bits=0" 134 | "with_alt_snap_mount_dir=0" 135 | ]; 136 | 137 | makeFlagsData = [ 138 | "--directory=../data" 139 | "BINDIR=$(out)/bin" 140 | "LIBEXECDIR=$(out)/libexec" 141 | "DATADIR=$(out)/share" 142 | "SYSTEMDSYSTEMUNITDIR=$(out)/lib/systemd/system" 143 | "SYSTEMDUSERUNITDIR=$(out)/lib/systemd/user" 144 | "ENVD=$(out)/etc/profile.d" 145 | "DBUSDIR=$(out)/share/dbus-1" 146 | "APPLICATIONSDIR=$(out)/share/applications" 147 | "SYSCONFXDGAUTOSTARTDIR=$(out)/etc/xdg/autostart" 148 | "ICON_FOLDER=$(out)/share/snapd" 149 | ]; 150 | 151 | makeFlagsCmd = [ 152 | "--directory=../cmd" 153 | "SYSTEMD_SYSTEM_GENERATOR_DIR=$out/lib/systemd/system-generators" 154 | ]; 155 | 156 | buildPhase = '' 157 | make $makeFlagsPackaging all 158 | make $makeFlagsData all 159 | make $makeFlagsCmd all 160 | ''; 161 | 162 | installPhase = '' 163 | make $makeFlagsPackaging install 164 | make $makeFlagsData install 165 | make $makeFlagsCmd install 166 | rm -rf $out/var 167 | ''; 168 | 169 | postFixup = '' 170 | mv $out/libexec/snapd/snap-confine{,-unwrapped} 171 | 172 | cat > $out/libexec/snapd/snap-confine << EOL 173 | #!${python3}/bin/python3 174 | import sys, os 175 | setuid_wrapper = "/run/wrappers/bin/snap-confine-setuid-wrapper" 176 | path = ( 177 | setuid_wrapper 178 | if os.path.exists(setuid_wrapper) 179 | else "@out@/libexec/snapd/snap-confine-stage-1" 180 | ) 181 | os.execv(path, [path] + sys.argv[1:]) 182 | EOL 183 | substituteInPlace $out/libexec/snapd/snap-confine --subst-var 'out' 184 | chmod +x $out/libexec/snapd/snap-confine 185 | 186 | cat > $out/libexec/snapd/snap-confine-stage-1 << EOL 187 | #!${python3}/bin/python3 188 | import sys, os, json 189 | os.environ["NIX_SNAP_CONFINE_DATA"] = json.dumps(dict( 190 | uid=os.getuid(), 191 | gid=os.getgid(), 192 | args=sys.argv[1:], 193 | )) 194 | try: 195 | os.setuid(0) 196 | os.setgid(0) 197 | except PermissionError: 198 | raise PermissionError(" ".join(( 199 | "Snap-confine wasn't run as root.", 200 | "Either re-run this command as root or use the NixOS module.", 201 | ))) 202 | os.execv( 203 | "${env}/bin/snap-env", 204 | [ 205 | "${env}/bin/snap-env", 206 | "-c", 207 | "exec @out@/libexec/snapd/snap-confine-stage-2", 208 | ], 209 | ) 210 | EOL 211 | substituteInPlace $out/libexec/snapd/snap-confine-stage-1 --subst-var 'out' 212 | chmod +x $out/libexec/snapd/snap-confine-stage-1 213 | 214 | cat > $out/libexec/snapd/snap-confine-stage-2 << EOL 215 | #!${python3}/bin/python3 216 | import sys, os, json 217 | data = json.loads(os.environ.pop("NIX_SNAP_CONFINE_DATA")) 218 | os.setresuid(data["uid"], 0, 0) 219 | os.setresgid(data["gid"], 0, 0) 220 | os.environ["PATH"] += ":@out@/bin" 221 | unwrapped = "@out@/libexec/snapd/snap-confine-unwrapped" 222 | os.execv(unwrapped, [unwrapped] + data["args"]) 223 | EOL 224 | substituteInPlace $out/libexec/snapd/snap-confine-stage-? --subst-var 'out' 225 | chmod +x $out/libexec/snapd/snap-confine-stage-2 226 | 227 | # Make xdg-open wrapper for io.snapcraft.Launcher so it can run xdg-open to 228 | # open with any installed program, even if it isn't a dependency 229 | makeWrapper ${xdg-utils}/bin/xdg-open $out/libexec/xdg-open \ 230 | --suffix PATH : /run/current-system/sw/bin 231 | 232 | wrapProgram $out/libexec/snapd/snapd \ 233 | --set SNAPD_DEBUG 1 \ 234 | --set PATH $out/bin:${ 235 | lib.makeBinPath ( 236 | with pkgs; 237 | [ 238 | # Snapd calls 239 | util-linux.mount 240 | shadow 241 | squashfsTools 242 | systemd 243 | openssh 244 | gnutar 245 | gzip 246 | # TODO: xdelta 247 | 248 | # Snap hook calls 249 | bash 250 | sudo 251 | gawk 252 | 253 | # Mount wrapper calls 254 | coreutils 255 | ] 256 | ) 257 | } \ 258 | --run ${lib.escapeShellArg '' 259 | set -uex 260 | shopt -s nullglob 261 | 262 | # Pre-create directories 263 | install -dm755 /var/lib/snapd/snaps 264 | install -dm111 /var/lib/snapd/void 265 | 266 | # Upstream snapd writes unit files to /etc/systemd/system, which is 267 | # immutable on NixOS. This package works around that by patching snapd 268 | # to write the unit files to /var/lib/snapd/nix-systemd-system 269 | # instead, and enables them as transient runtime units. However, this 270 | # means they won't automatically start on boot, which breaks snapd. 271 | # To solve this, the next block of code starts all the unit files in 272 | # /var/lib/snapd/nix-systemd-system. 273 | 274 | for path in /var/lib/snapd/nix-systemd-system/*; do 275 | name="$(basename "$path")" 276 | if ! systemctl is-active --quiet "$name"; then 277 | rtpath="/run/systemd/system/$name" 278 | ln -fs "$path" "$rtpath" 279 | systemctl start "$name" 280 | rm -f "$rtpath" 281 | fi 282 | done 283 | 284 | # Make /snap/bin symlinks not point inside /nix/store, 285 | # so they don't point to an old version of snap 286 | for f in /snap/bin/*; do 287 | if [[ "$(readlink "$f")" = /nix/store/* ]]; then 288 | rm -f "$f" 289 | ln -s /run/current-system/sw/bin/snap "$f" 290 | fi 291 | done 292 | ''} 293 | ''; 294 | } 295 | -------------------------------------------------------------------------------- /src/nixify.patch: -------------------------------------------------------------------------------- 1 | diff --git a/cmd/Makefile.am b/cmd/Makefile.am 2 | index b1923adf44..f17cce66d8 100644 3 | --- a/cmd/Makefile.am 4 | +++ b/cmd/Makefile.am 5 | @@ -98,7 +98,7 @@ fmt:: $(filter-out $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildc 6 | # installing a fresh copy of snap confine and the appropriate apparmor profile. 7 | .PHONY: hack 8 | hack: snap-confine/snap-confine-debug snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp snap-discard-ns/snap-discard-ns snap-device-helper/snap-device-helper snapd-apparmor/snapd-apparmor 9 | - sudo install -D -m 4755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine 10 | + sudo install -D -m 755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine 11 | if [ -d $(DESTDIR)$(APPARMOR_SYSCONFIG) ]; then sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)$(APPARMOR_SYSCONFIG)/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real; fi 12 | sudo install -d -m 755 $(DESTDIR)$(snapdstatedir)/apparmor/snap-confine/ 13 | if [ "$$(command -v apparmor_parser)" != "" ]; then sudo apparmor_parser -r snap-confine/snap-confine.apparmor; fi 14 | @@ -418,7 +418,7 @@ endif 15 | 16 | install-exec-hook: 17 | # Ensure that snap-confine is u+s (setuid) 18 | - chmod 4755 $(DESTDIR)$(libexecdir)/snap-confine 19 | + chmod 755 $(DESTDIR)$(libexecdir)/snap-confine 20 | 21 | ## 22 | ## snap-mgmt 23 | diff --git a/cmd/configure.ac b/cmd/configure.ac 24 | index 9df43ac860..78c8f428b3 100644 25 | --- a/cmd/configure.ac 26 | +++ b/cmd/configure.ac 27 | @@ -232,9 +232,6 @@ fi 28 | dnl FIXME: get this via something like pkgconf once it is defined there 29 | dnl FIXME: Use PKG_CHECK_VAR when we have dropped Trusty (14.04) 30 | AC_ARG_VAR([SYSTEMD_PREFIX], [value for systemd prefix (overriding pkg-config)]) 31 | -if test -z "${SYSTEMD_PREFIX}"; then 32 | - SYSTEMD_PREFIX="$($PKG_CONFIG --variable=prefix systemd)" 33 | -fi 34 | if test -n "${SYSTEMD_PREFIX}"; then 35 | SYSTEMD_SYSTEM_ENV_GENERATOR_DIR="${SYSTEMD_PREFIX}/lib/systemd/system-environment-generators" 36 | else 37 | diff --git a/cmd/libsnap-confine-private/utils.c b/cmd/libsnap-confine-private/utils.c 38 | index f39e498a65..7288297508 100644 39 | --- a/cmd/libsnap-confine-private/utils.c 40 | +++ b/cmd/libsnap-confine-private/utils.c 41 | @@ -242,7 +242,7 @@ int sc_nonfatal_mkpath(const char *const path, mode_t mode) 42 | bool sc_is_expected_path(const char *path) 43 | { 44 | const char *expected_path_re = 45 | - "^((/var/lib/snapd)?/snap/(snapd|core)/x?[0-9]+/usr/lib|/usr/lib(exec)?)/snapd/snap-confine$"; 46 | + "^((/var/lib/snapd)?/snap/(snapd|core)/x?[0-9]+/usr/lib|(/usr|@out@)/lib(exec)?)/snapd/snap-confine(-unwrapped)?$"; 47 | regex_t re; 48 | if (regcomp(&re, expected_path_re, REG_EXTENDED | REG_NOSUB) != 0) 49 | die("can not compile regex %s", expected_path_re); 50 | diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c 51 | index 513c6794d2..706fedbbe4 100644 52 | --- a/cmd/snap-confine/mount-support.c 53 | +++ b/cmd/snap-confine/mount-support.c 54 | @@ -976,7 +976,7 @@ void sc_populate_mount_ns(struct sc_apparmor *apparmor, int snap_update_ns_fd, 55 | {.path = "/run"}, // to get /run with sockets and what not 56 | {.path = "/lib/modules",.is_optional = true}, // access to the modules of the running kernel 57 | {.path = "/lib/firmware",.is_optional = true}, // access to the firmware of the running kernel 58 | - {.path = "/usr/src"}, // FIXME: move to SecurityMounts in system-trace interface 59 | + {.path = "/usr/src",.is_optional = true}, // FIXME: move to SecurityMounts in system-trace interface 60 | {.path = "/var/log"}, // FIXME: move to SecurityMounts in log-observe interface 61 | #ifdef MERGED_USR 62 | {.path = "/run/media",.is_bidirectional = true,.altpath = "/media"}, // access to the users removable devices 63 | diff --git a/cmd/snap-confine/seccomp-support.c b/cmd/snap-confine/seccomp-support.c 64 | index 5bf3338819..1eea7ea22e 100644 65 | --- a/cmd/snap-confine/seccomp-support.c 66 | +++ b/cmd/snap-confine/seccomp-support.c 67 | @@ -83,10 +83,6 @@ static void validate_path_has_strict_perms(const char *path) 68 | die("%s not root-owned %i:%i", path, stat_buf.st_uid, 69 | stat_buf.st_gid); 70 | } 71 | - 72 | - if (stat_buf.st_mode & S_IWOTH) { 73 | - die("%s has 'other' write %o", path, stat_buf.st_mode); 74 | - } 75 | } 76 | 77 | static void validate_bpfpath_is_safe(const char *path) 78 | diff --git a/cmd/snap-confine/snap-confine.c b/cmd/snap-confine/snap-confine.c 79 | index 6392657054..8dfb4a89e7 100644 80 | --- a/cmd/snap-confine/snap-confine.c 81 | +++ b/cmd/snap-confine/snap-confine.c 82 | @@ -436,7 +436,7 @@ int main(int argc, char **argv) 83 | * one, which definitely doesn't run in a snap-specific namespace, has a 84 | * predictable PID and is long lived. 85 | */ 86 | - sc_reassociate_with_pid1_mount_ns(); 87 | + // NIX PATCH: Don't try to escape the FHS environment 88 | // Do global initialization: 89 | int global_lock_fd = sc_lock_global(); 90 | // Ensure that "/" or "/snap" is mounted with the 91 | diff --git a/dirs/dirs.go b/dirs/dirs.go 92 | index f1d441ee33..4e8bc24513 100644 93 | --- a/dirs/dirs.go 94 | +++ b/dirs/dirs.go 95 | @@ -378,7 +378,7 @@ func SnapSystemdConfDirUnder(rootdir string) string { 96 | // SnapServicesDirUnder returns the path to the systemd services 97 | // conf dir under rootdir. 98 | func SnapServicesDirUnder(rootdir string) string { 99 | - return filepath.Join(rootdir, "/etc/systemd/system") 100 | + return filepath.Join(rootdir, "/var/lib/snapd/nix-systemd-system") 101 | } 102 | 103 | func SnapRuntimeServicesDirUnder(rootdir string) string { 104 | @@ -533,7 +533,7 @@ func SetRootDir(rootdir string) { 105 | SnapSystemdDir = filepath.Join(rootdir, "/etc/systemd") 106 | SnapSystemdRunDir = filepath.Join(rootdir, "/run/systemd") 107 | 108 | - SnapDBusSystemPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/system.d") 109 | + SnapDBusSystemPolicyDir = filepath.Join(rootdir, "/var/lib/snapd/nix-dbus-system") 110 | SnapDBusSessionPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/session.d") 111 | // Use 'dbus-1/services' and `dbus-1/system-services' to mirror 112 | // '/usr/share/dbus-1' hierarchy. 113 | @@ -544,7 +544,7 @@ func SetRootDir(rootdir string) { 114 | 115 | CloudInstanceDataFile = filepath.Join(rootdir, "/run/cloud-init/instance-data.json") 116 | 117 | - SnapUdevRulesDir = filepath.Join(rootdir, "/etc/udev/rules.d") 118 | + SnapUdevRulesDir = filepath.Join(rootdir, "/var/lib/snapd/nix-udev-rules") 119 | 120 | SnapKModModulesDir = filepath.Join(rootdir, "/etc/modules-load.d/") 121 | SnapKModModprobeDir = filepath.Join(rootdir, "/etc/modprobe.d/") 122 | @@ -589,7 +589,7 @@ func SetRootDir(rootdir string) { 123 | // both RHEL and CentOS list "fedora" in ID_LIKE 124 | DistroLibExecDir = filepath.Join(rootdir, "/usr/libexec/snapd") 125 | } else { 126 | - DistroLibExecDir = filepath.Join(rootdir, "/usr/lib/snapd") 127 | + DistroLibExecDir = filepath.Join(rootdir, "@out@/libexec/snapd") 128 | } 129 | 130 | XdgRuntimeDirBase = filepath.Join(rootdir, "/run/user") 131 | diff --git a/interfaces/system_key.go b/interfaces/system_key.go 132 | index d6595154b3..52df93e914 100644 133 | --- a/interfaces/system_key.go 134 | +++ b/interfaces/system_key.go 135 | @@ -107,7 +107,7 @@ func generateSystemKey() (*systemKey, error) { 136 | sk := &systemKey{ 137 | Version: systemKeyVersion, 138 | } 139 | - snapdPath, err := snapdtool.InternalToolPath("snapd") 140 | + snapdPath, err := snapdtool.InternalToolPath("snapd-unwrapped") 141 | if err != nil { 142 | return nil, err 143 | } 144 | @@ -274,7 +274,7 @@ func SystemKeyMismatch(extraData SystemKeyExtraData) (bool, error) { 145 | if mockedSystemKey == nil { 146 | if exe, err := os.Readlink("/proc/self/exe"); err == nil { 147 | // detect running local local builds 148 | - if !strings.HasPrefix(exe, "/usr") && !strings.HasPrefix(exe, dirs.SnapMountDir) { 149 | + if !strings.HasPrefix(exe, "/usr") && !strings.HasPrefix(exe, dirs.SnapMountDir) && !strings.HasPrefix(exe, "@out@") { 150 | logger.Noticef("running from non-installed location %s: ignoring system-key", exe) 151 | return false, ErrSystemKeyVersion 152 | } 153 | diff --git a/snap/info.go b/snap/info.go 154 | index 96b7356e30..f481e94f8e 100644 155 | --- a/snap/info.go 156 | +++ b/snap/info.go 157 | @@ -1501,9 +1501,9 @@ func (app *AppInfo) launcherCommand(command string) string { 158 | command = " " + command 159 | } 160 | if app.Name == app.Snap.SnapName() { 161 | - return fmt.Sprintf("/usr/bin/snap run%s %s", command, app.Snap.InstanceName()) 162 | + return fmt.Sprintf("@out@/bin/snap run%s %s", command, app.Snap.InstanceName()) 163 | } 164 | - return fmt.Sprintf("/usr/bin/snap run%s %s.%s", command, app.Snap.InstanceName(), app.Name) 165 | + return fmt.Sprintf("@out@/bin/snap run%s %s.%s", command, app.Snap.InstanceName(), app.Name) 166 | } 167 | 168 | // LauncherCommand returns the launcher command line to use when invoking the 169 | diff --git a/systemd/systemd.go b/systemd/systemd.go 170 | index a6ad62a771..e5ea856068 100644 171 | --- a/systemd/systemd.go 172 | +++ b/systemd/systemd.go 173 | @@ -616,6 +616,14 @@ func (s *systemd) EnableNoReload(serviceNames []string) error { 174 | if len(serviceNames) == 0 { 175 | return nil 176 | } 177 | + for _, serviceName := range serviceNames { 178 | + servicePath := filepath.Join(dirs.SnapServicesDir, serviceName) 179 | + serviceRuntimePath := filepath.Join(dirs.SnapRuntimeServicesDir, serviceName) 180 | + os.Remove(serviceRuntimePath) 181 | + if err := os.Symlink(servicePath, serviceRuntimePath); err != nil { 182 | + return err 183 | + } 184 | + } 185 | var args []string 186 | if s.rootDir != "" { 187 | // passing root already implies no reload 188 | @@ -623,6 +631,7 @@ func (s *systemd) EnableNoReload(serviceNames []string) error { 189 | } else { 190 | args = append(args, "--no-reload") 191 | } 192 | + args = append(args, "--runtime") 193 | args = append(args, "enable") 194 | args = append(args, serviceNames...) 195 | _, err := s.systemctl(args...) 196 | @@ -643,6 +652,14 @@ func (s *systemd) DisableNoReload(serviceNames []string) error { 197 | if len(serviceNames) == 0 { 198 | return nil 199 | } 200 | + for _, serviceName := range serviceNames { 201 | + servicePath := filepath.Join(dirs.SnapServicesDir, serviceName) 202 | + serviceRuntimePath := filepath.Join(dirs.SnapRuntimeServicesDir, serviceName) 203 | + os.Remove(serviceRuntimePath) 204 | + if err := os.Symlink(servicePath, serviceRuntimePath); err != nil { 205 | + return err 206 | + } 207 | + } 208 | var args []string 209 | if s.rootDir != "" { 210 | // passing root already implies no reload 211 | @@ -650,6 +667,7 @@ func (s *systemd) DisableNoReload(serviceNames []string) error { 212 | } else { 213 | args = append(args, "--no-reload") 214 | } 215 | + args = append(args, "--runtime") 216 | args = append(args, "disable") 217 | args = append(args, serviceNames...) 218 | _, err := s.systemctl(args...) 219 | diff --git a/usersession/userd/launcher.go b/usersession/userd/launcher.go 220 | index 830ed9995c..843f628730 100644 221 | --- a/usersession/userd/launcher.go 222 | +++ b/usersession/userd/launcher.go 223 | @@ -206,7 +206,7 @@ func (s *Launcher) OpenURL(addr string, sender dbus.Sender) *dbus.Error { 224 | // this code must not add directories from the snap 225 | // to XDG_DATA_DIRS and similar, see 226 | // https://ubuntu.com/security/CVE-2020-11934 227 | - if err := exec.Command("xdg-open", addr).Run(); err != nil { 228 | + if err := exec.Command("@out@/libexec/xdg-open", addr).Run(); err != nil { 229 | return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL")) 230 | } 231 | 232 | @@ -292,7 +292,7 @@ func (s *Launcher) OpenFile(parentWindow string, clientFd dbus.UnixFD, sender db 233 | return dbus.MakeFailedError(fmt.Errorf("permission denied")) 234 | } 235 | 236 | - if err = exec.Command("xdg-open", filename).Run(); err != nil { 237 | + if err = exec.Command("@out@/libexec/xdg-open", filename).Run(); err != nil { 238 | return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL")) 239 | } 240 | 241 | diff --git a/wrappers/binaries.go b/wrappers/binaries.go 242 | index 5f88b3aff8..ca2c187b87 100644 243 | --- a/wrappers/binaries.go 244 | +++ b/wrappers/binaries.go 245 | @@ -220,7 +220,7 @@ func EnsureSnapBinaries(s *snap.Info) (err error) { 246 | } 247 | 248 | appBase := filepath.Base(app.WrapperPath()) 249 | - binariesContent[appBase] = &osutil.SymlinkFileState{Target: "/usr/bin/snap"} 250 | + binariesContent[appBase] = &osutil.SymlinkFileState{Target: "@out@/bin/snap"} 251 | 252 | if completionVariant != noCompletion && app.Completer != "" { 253 | completersContent[appBase] = &osutil.SymlinkFileState{Target: completeSh} 254 | diff --git a/wrappers/internal/service_unit_gen.go b/wrappers/internal/service_unit_gen.go 255 | index 7f066942fb..4a014ca981 100644 256 | --- a/wrappers/internal/service_unit_gen.go 257 | +++ b/wrappers/internal/service_unit_gen.go 258 | @@ -157,6 +157,8 @@ TimeoutStopSec={{.StopTimeout.Seconds}} 259 | TimeoutStartSec={{.StartTimeout.Seconds}} 260 | {{- end}} 261 | Type={{.App.Daemon}} 262 | +# NIX PATCH: This is needed because nixpkgs's chrootenv runs the code in a child process 263 | +NotifyAccess=all 264 | {{- if .Remain}} 265 | RemainAfterExit={{.Remain}} 266 | {{- end}} 267 | --------------------------------------------------------------------------------