You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/10/08 13:59:50 UTC

(tooling-trusted-releases) 02/02: Fix landlock/rsync issue

This is an automated email from the ASF dual-hosted git repository.

alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit ebba32a4056134e6a35bc282a1fe322eb473ecaf
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Thu Oct 8 14:59:21 2026 +0100

    Fix landlock/rsync issue
---
 atr/sandbox.py              | 20 +++++++++++++++++++-
 atr/ssh.py                  |  4 ++--
 atr/tasks/checks/compare.py |  2 +-
 tests/unit/test_sandbox.py  | 35 +++++++++++++++++++++++++++++++++++
 4 files changed, 57 insertions(+), 4 deletions(-)

diff --git a/atr/sandbox.py b/atr/sandbox.py
index f1f27a1c..8b31f5cf 100644
--- a/atr/sandbox.py
+++ b/atr/sandbox.py
@@ -23,6 +23,7 @@ import subprocess
 import sys
 from typing import Final
 
+LIST_ACCESSES: Final = "read-dir"
 RO_ACCESSES: Final = "read-file,read-dir"
 RW_ACCESSES: Final = (
     "read-file,read-dir,write-file,truncate,refer,"
@@ -33,7 +34,13 @@ SYSTEM_ACCESSES: Final = "execute,read-file,read-dir"
 SYSTEM_PATHS: Final = ("/bin", "/etc", "/lib", "/lib64", "/sbin", "/usr")
 
 
-def command(argv: list[str], *, ro_paths: list[str] | None = None, rw_paths: list[str] | None = None) -> list[str]:
+def command(
+    argv: list[str],
+    *,
+    list_paths: list[str] | None = None,
+    ro_paths: list[str] | None = None,
+    rw_paths: list[str] | None = None,
+) -> list[str]:
     setpriv = landlock_setpriv()
     if setpriv is None:
         return argv
@@ -41,6 +48,8 @@ def command(argv: list[str], *, ro_paths: list[str] | None = None, rw_paths: lis
     for path in SYSTEM_PATHS:
         if os.path.exists(path):
             wrapped += ["--landlock-rule", f"path-beneath:{SYSTEM_ACCESSES}:{path}"]
+    for path in list_paths or []:
+        wrapped += ["--landlock-rule", f"path-beneath:{LIST_ACCESSES}:{path}"]
     for path in ro_paths or []:
         wrapped += ["--landlock-rule", f"path-beneath:{RO_ACCESSES}:{path}"]
     for path in rw_paths or []:
@@ -67,3 +76,12 @@ def landlock_setpriv() -> str | None:
     if b"--landlock-access" in (result.stdout + result.stderr):
         return setpriv
     return None
+
+
+def rsync_command(
+    argv: list[str], *, ro_paths: list[str] | None = None, rw_paths: list[str] | None = None
+) -> list[str]:
+    # Since 3.4, rsync opens each directory from / down to its destination rather than calling chdir,
+    # and Landlock refuses that walk unless every directory on the way can be listed. Listing is all
+    # it gets, so file contents stay confined to ro_paths and rw_paths
+    return command(argv, list_paths=["/"], ro_paths=ro_paths, rw_paths=rw_paths)
diff --git a/atr/ssh.py b/atr/ssh.py
index 0e61d94f..0f0bdc70 100644
--- a/atr/ssh.py
+++ b/atr/ssh.py
@@ -645,7 +645,7 @@ async def _step_07a_process_validated_rsync_read(
                 raise RsyncArgsError(f"No files match the tag for release {release.key}")
             argv[-1:] = files
 
-        argv = sandbox.command(argv, ro_paths=[str(source_dir)])
+        argv = sandbox.rsync_command(argv, ro_paths=[str(source_dir)])
 
         ###################################################
         ### Calls _step_08_execute_rsync_sender_command ###
@@ -694,7 +694,7 @@ async def _step_07b_process_validated_rsync_write(
             if old_rev is not None:
                 log.info(f"Using old revision {old_rev.number} and interim path {path}")
             rsync_argv = _build_rsync_write_argv(argv)
-            rsync_argv = sandbox.command(rsync_argv, rw_paths=[str(path)])
+            rsync_argv = sandbox.rsync_command(rsync_argv, rw_paths=[str(path)])
 
             ###################################################
             ### Calls _step_08_execute_rsync_upload_command ###
diff --git a/atr/tasks/checks/compare.py b/atr/tasks/checks/compare.py
index 6146a9e1..1df9517a 100644
--- a/atr/tasks/checks/compare.py
+++ b/atr/tasks/checks/compare.py
@@ -297,7 +297,7 @@ def _compare_trees_rsync(repo_dir: safe.StatePath, archive_dir: safe.StatePath)
         f"{archive_dir}{os.sep}",
     ]
     # A dry-run diff, so rsync only reads the two trees - confine it to those at the kernel level.
-    command = sandbox.command(command, ro_paths=[str(repo_dir), str(archive_dir)])
+    command = sandbox.rsync_command(command, ro_paths=[str(repo_dir), str(archive_dir)])
     result = subprocess.run(
         command,
         capture_output=True,
diff --git a/tests/unit/test_sandbox.py b/tests/unit/test_sandbox.py
index 73d69168..c56bfa28 100644
--- a/tests/unit/test_sandbox.py
+++ b/tests/unit/test_sandbox.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import pathlib
+import shutil
 import subprocess
 
 import pytest
@@ -73,6 +74,40 @@ def test_executes_command_with_granted_read(tmp_path: pathlib.Path) -> None:
     assert result.stdout == b"inside"
 
 
+@requires_landlock
+@pytest.mark.skipif(shutil.which("rsync") is None, reason="rsync is not available")
+def test_rsync_command_copies_into_granted_path(tmp_path: pathlib.Path) -> None:
+    source = tmp_path / "source"
+    source.mkdir()
+    (source / "file.txt").write_text("content")
+    destination = tmp_path / "destination"
+    destination.mkdir()
+    argv = sandbox.rsync_command(
+        ["rsync", "-a", f"{source}/", f"{destination}/"], ro_paths=[str(source)], rw_paths=[str(destination)]
+    )
+    result = subprocess.run(argv, capture_output=True)
+    assert result.returncode == 0, result.stderr
+    assert (destination / "file.txt").read_text() == "content"
+
+
+@requires_landlock
+def test_rsync_command_denies_read_outside_granted_paths(tmp_path: pathlib.Path) -> None:
+    granted = tmp_path / "granted"
+    granted.mkdir()
+    secret = tmp_path / "secret.txt"
+    secret.write_text("secret")
+    argv = sandbox.rsync_command(["cat", str(secret)], ro_paths=[str(granted)])
+    result = subprocess.run(argv, capture_output=True)
+    assert result.returncode != 0
+
+
+def test_rsync_command_lists_from_root(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(sandbox, "landlock_setpriv", lambda: "/bin/setpriv")
+    wrapped = sandbox.rsync_command(["rsync", "--server"], ro_paths=["/a"])
+    assert f"path-beneath:{sandbox.LIST_ACCESSES}:/" in wrapped
+    assert f"path-beneath:{sandbox.RO_ACCESSES}:/a" in wrapped
+
+
 @requires_landlock
 def test_write_operations_with_granted_rw(tmp_path: pathlib.Path) -> None:
     granted = tmp_path / "granted"


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org