diff options
| author | tv <tv@krebsco.de> | 2026-11-12 11:13:53 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-12 11:13:53 +0000 |
| commit | 1ecf303545d70da9b861b08874b71fd259ac4d3f (patch) | |
| tree | f015eaacebd66df1b54ee806ceb0c02cabb3873e /tests | |
| parent | a0238c05909757ec44015fafc786284d4dbf329e (diff) | |
populate file: add insecureLinks
rsync 3.5.0 opens merge files, including per-directory .rsync-filter
files, walking the path one directory at a time, refusing symlinks that
neither root nor the user running rsync owns, and opening each directory
for reading. It treats the files it can't open that way as missing,
without saying so. That happens, e.g., below a home directory with mode
711, which the user may enter but not read, or behind another user's
symlink. Such a source then got synced without its .rsync-filter rules:
excluded files got transferred, and --delete-excluded deleted nothing.
Before syncing with rsync 3.5.0 or later a source containing .rsync-filter
files, populate asks rsync which of them it reads: a dry run with
--debug=FILTER2. If it reports an existing one as missing, the file
source type's new insecureLinks option decides: with null, the default,
populate fails with an error naming the file; with true, rsync reads such
files anyway (--insecure-links); with false, rsync syncs without them.
Both print a warning.
File scenarios sync a source through a symlink owned by nobody, with each
setting.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/default.nix | 1 | ||||
| -rwxr-xr-x | tests/fixtures.sh | 10 | ||||
| -rw-r--r-- | tests/scenarios.nix | 34 |
3 files changed, 44 insertions, 1 deletions
diff --git a/tests/default.nix b/tests/default.nix index a6b9356..285ed86 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -21,6 +21,7 @@ let remoteHost = "target"; agePath = "${pkgs.age}/bin/age"; inherit gitHead gitFirst; + rsyncVersion = pkgs.rsync.version; }; flakeJq = import ../pkgs/populate/flake-jq.nix; in diff --git a/tests/fixtures.sh b/tests/fixtures.sh index 833df77..b57dcc5 100755 --- a/tests/fixtures.sh +++ b/tests/fixtures.sh @@ -258,6 +258,16 @@ cat > /fixtures/alt2/flake.nix <<'NIX' { outputs = _: { greeting = "alt2"; who = "alt2"; }; } NIX +# filtered, filtered-link: a directory with an .rsync-filter, and a symlink to +# it that neither root nor the user deploying owns, through which rsync 3.5.0 +# and later can't read that file. +mkdir /fixtures/filtered +printf -- '- /garbage\n' > /fixtures/filtered/.rsync-filter +echo keep > /fixtures/filtered/keep +echo garbage > /fixtures/filtered/garbage +ln -s filtered /fixtures/filtered-link +chown -h nobody /fixtures/filtered-link + # noflake: a directory without flake.nix. mkdir /fixtures/noflake echo noflake > /fixtures/noflake/README diff --git a/tests/scenarios.nix b/tests/scenarios.nix index 4838b74..efad151 100644 --- a/tests/scenarios.nix +++ b/tests/scenarios.nix @@ -26,6 +26,8 @@ # HEAD of /fixtures/gitsrc, and its parent. gitHead, gitFirst, + # Version of rsync on the deploying machine. + rsyncVersion, }: let @@ -269,7 +271,37 @@ assert !(krops.isLocalTarget (targets.remote "/")); targetKind = "local"; phases = [ (builtins.head args.phases) ]; })) - ]; + ] + # A source whose .rsync-filter rsync 3.5.0 and later can't read, because + # it's reached through a symlink that neither root nor the user deploying + # owns. (Running as root, the scenarios can't use the other common + # cause, a directory on the way that the user may enter but not read.) + ++ concatMap (targetKind: map (insecureLinks: let + affected = lib.versionAtLeast rsyncVersion "3.5.0"; + excluded = !affected || insecureLinks == true; + in scenario { + name = "file-${targetKind}-insecureLinks" + + (if insecureLinks == null then "Null" else if insecureLinks then "True" else "False"); + inherit targetKind; + prepare = '' + mkdir -p "$T"/x + echo old > "$T"/x/garbage + ''; + phases = twice ({ + sources.x.file = { path = "/fixtures/filtered-link"; inherit insecureLinks; }; + } // (if affected && insecureLinks == null then { + expect.fail = "can't read /fixtures/filtered-link/.rsync-filter, so it would sync /fixtures/filtered-link/ without it."; + } else { + check = '' + expect_eq keep "$(cat "$T"/x/keep)" keep + ${if excluded then '' + test ! -e "$T"/x/garbage || fail "garbage not excluded" + '' else '' + expect_eq garbage "$(cat "$T"/x/garbage)" garbage + ''} + ''; + })); + }) [ null true false ]) targetKinds; flake = matrix "flake" { kind = [ "git" "path" "subdir" "remote" ]; |
