chroot-distro 2.3.1__tar.gz → 2.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/PKG-INFO +35 -17
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/README.md +34 -16
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/pyproject.toml +1 -1
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/help/pages.py +26 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/__init__.py +16 -10
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/bindings.py +10 -1
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/elevate.py +43 -4
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/namespace.py +36 -2
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_elevate.py +101 -4
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_namespace.py +94 -1
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/uv.lock +1 -1
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.editorconfig +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/codeql/codeql-config.yml +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/dependabot.yml +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/workflows/ci.yml +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/workflows/codeql.yml +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/workflows/publish.yml +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.gitignore +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.python-version +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/LICENSE +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/check-before-commit.sh +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/check-config.sh +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/__init__.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/arch.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/atomic.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/cli.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/backup.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/build.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/clear_cache.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/copy.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/diff.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/help/__init__.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/help/render.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/info.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/install.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/install_local.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/kill.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/list_cmd.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/chroot_cmd.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/env.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/passwd.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/ps.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/push.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/remove.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/rename.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/reset.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/restore.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/run.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/search.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/sync.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/unmount.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/completions/_chroot-distro +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/completions/chroot-distro.bash +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/completions/chroot-distro.fish +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/constants.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/exceptions.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/__init__.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/android.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_cache.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/__init__.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/constants.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/copy_step.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/dockerignore.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/engine.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/errors.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/handlers.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/parsing.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/run_step.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/stage.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/users.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/display.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/__init__.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/cache.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/layers.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/media.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/pull.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/push.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/refs.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/transport.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/dockerfile.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/download.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/gpu.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/layer_diff.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/mount_manager.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/nvidia.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/oci_writer.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/rootfs.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/session.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/sound.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/tar_extract.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/wayland.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/x11.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/locking.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/message.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/names.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/parser.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/paths.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/progress.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/py.typed +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/rate_limit.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/conftest.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_android.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_arch.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_backup_restore.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_bind_options.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_cli.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_constants.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_diff_baseline_cache.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_display.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_display_sockets.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_docker_refs.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_dockerfile.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_download_algorithms.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_download_blob_multi.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_download_multi.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_gpu.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_info.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_install.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_install_local.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_kill.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_layer_diff.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_list.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_locking.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_login_helpers.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_message.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_mount_manager_ns.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_names.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_parser.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_paths.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_progress.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_push_chunked.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_remove.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_rootfs.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_sound.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_tar_extract.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_unmount.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_wayland.py +0 -0
- {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_x11.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chroot-distro
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: chroot-distro is a lightweight Linux container management utility built around chroot.
|
|
5
5
|
Project-URL: Homepage, https://github.com/sabamdarif/chroot-distro
|
|
6
6
|
Project-URL: Repository, https://github.com/sabamdarif/chroot-distro
|
|
@@ -468,7 +468,7 @@ chroot-distro login ubuntu --get-chroot-cmd
|
|
|
468
468
|
| Option | Description |
|
|
469
469
|
|---|---|
|
|
470
470
|
| `-u`, `--user USER` | Log in as USER (default: `root`). Accepts `name`, numeric `uid`, `name:group`, or `uid:gid`. |
|
|
471
|
-
| `--isolated` | Reduce host exposure and enable namespace isolation (mount, PID, UTS, IPC via `unshare`/`nsenter`). On Termux: also skip Android system, storage, and `$PREFIX` binds unless you opt in with `--shared-*` or `--bind`. (Fresh `/tmp` and `/run` are the default in every mode now, not just `--isolated`.) Mutually exclusive with `--minimal`. |
|
|
471
|
+
| `--isolated` | Reduce host exposure and enable namespace isolation (mount, PID, UTS, IPC, and cgroup when supported, via `unshare`/`nsenter`). On Termux: also skip Android system, storage, and `$PREFIX` binds unless you opt in with `--shared-*` or `--bind`. (Fresh `/tmp` and `/run` are the default in every mode now, not just `--isolated`.) Mutually exclusive with `--minimal`. To get the same namespace isolation **without** reducing the mount set, set `CD_USE_NS=1` instead (see [Environment variables](#environment-variables)). |
|
|
472
472
|
| `--minimal` | Bare minimum chroot: core pseudo-filesystems only (`/dev`, `/proc`, `/sys`, plus `/run`, `/dev/pts`, `/dev/shm` when present). Stripped guest environment. Mutually exclusive with `--isolated`. |
|
|
473
473
|
| `--shared-home` | Bind the invoking user's host home into the guest home (or `/root` for root). On Termux, binds `TERMUX_HOME`. |
|
|
474
474
|
| `--shared-tmp` | Bind host tmp (`/tmp` on Linux, `$PREFIX/tmp` on Termux) to `/tmp` in the guest. Opt-in only: by default the container gets its own fresh `/tmp`, not the host's. |
|
|
@@ -562,23 +562,38 @@ Termux.)
|
|
|
562
562
|
- Guest `ldconfig` is run inside the chroot to refresh the shared library
|
|
563
563
|
cache after the new libraries are bind-mounted.
|
|
564
564
|
|
|
565
|
-
#### Namespace isolation (`--isolated`)
|
|
565
|
+
#### Namespace isolation (`--isolated` and `CD_USE_NS`)
|
|
566
566
|
|
|
567
567
|
With `--isolated`, chroot-distro creates a per-container namespace holder
|
|
568
568
|
(`unshare`) and runs bind mounts, special mounts, and `chroot` inside that
|
|
569
569
|
environment (`nsenter`). Supported namespaces: **mount**, **PID**, **UTS**,
|
|
570
|
-
and **
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
570
|
+
**IPC**, and **cgroup**. The cgroup namespace is acquired best-effort (used
|
|
571
|
+
when the kernel supports it, skipped otherwise); the **mount/PID/UTS/IPC**
|
|
572
|
+
set is **all-or-nothing**: chroot-distro probes that set first, and if any
|
|
573
|
+
one of them is unsupported on the kernel it acquires none of them and falls
|
|
574
|
+
back fully to a non-isolated login (with a warning naming the missing
|
|
575
|
+
namespace), so a session is never left half-isolated. This is inspired by
|
|
575
576
|
[Ubuntu-Chroot](Ubuntu-Chroot/tools/chroot.sh) and is **not** a full
|
|
576
577
|
container runtime: there is no network namespace, no user namespace
|
|
577
578
|
mapping, and no image layering.
|
|
578
579
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
580
|
+
`--isolated` couples two things: namespace isolation **and** a reduced set
|
|
581
|
+
of host bind mounts (it skips the Android system/storage/`$PREFIX` binds on
|
|
582
|
+
Termux, and `/tmp`/display sharing on Linux, unless re-enabled with
|
|
583
|
+
`--shared-*` / `--bind`). If you want **only** the namespace isolation
|
|
584
|
+
while keeping every default mount, set the `CD_USE_NS=1` environment
|
|
585
|
+
variable instead: every `login`/`run` then runs in the same
|
|
586
|
+
mount/PID/UTS/IPC/cgroup namespaces but with the full default mount set
|
|
587
|
+
intact. `CD_USE_NS` accepts `1`/`true`/`yes`/`on`.
|
|
588
|
+
|
|
589
|
+
> `CD_USE_NS` is set by the invoking user but the tool re-executes itself
|
|
590
|
+
> as root; chroot-distro forwards the variable across `sudo`/`doas`/`pkexec`
|
|
591
|
+
> /`su` automatically, so it keeps working even when the `sudo` line prints
|
|
592
|
+
> `'-E' is ignored`.
|
|
593
|
+
|
|
594
|
+
Do not mix isolated (via `--isolated` or `CD_USE_NS`) and non-isolated
|
|
595
|
+
logins on the same container without running `chroot-distro unmount <name>`
|
|
596
|
+
first. Concurrent isolated sessions share the same holder and mounts.
|
|
582
597
|
|
|
583
598
|
#### Host bindings (Termux, default mode)
|
|
584
599
|
|
|
@@ -635,9 +650,10 @@ entries win):
|
|
|
635
650
|
`LIBGL_ALWAYS_SOFTWARE`. Your `--env` entries override these.
|
|
636
651
|
|
|
637
652
|
`HOSTNAME` is always set to the host system hostname name. The
|
|
638
|
-
`hostname`/`uname` commands report it only
|
|
639
|
-
|
|
640
|
-
report the host's name (no UTS
|
|
653
|
+
`hostname`/`uname` commands report it only when namespace isolation is
|
|
654
|
+
active (via `--isolated` or `CD_USE_NS`), where the UTS namespace is given
|
|
655
|
+
a real hostname; otherwise they still report the host's name (no UTS
|
|
656
|
+
namespace to change).
|
|
641
657
|
|
|
642
658
|
On Termux (unless isolated or minimal), `$PREFIX/bin` is appended to
|
|
643
659
|
`PATH`. A snippet at `/etc/profile.d/termux-profile.sh` re-applies
|
|
@@ -1168,6 +1184,7 @@ paths on Linux are typically under `/root/.local/share/` and
|
|
|
1168
1184
|
| `CD_DOWNLOAD_WORKERS` | Parallel registry layer downloads during `install` (default `4`, maximum `10`). Invalid values use the default; out-of-range values are clamped. |
|
|
1169
1185
|
| `CD_DOWNLOAD_RATE_LIMIT` | Bandwidth limit for downloads (e.g., `5M` for 5 MiB/s, default `0` = unlimited). Supports suffixes `K`, `M`, `G` (case-insensitive). |
|
|
1170
1186
|
| `CD_DOWNLOAD_MAX_RETRIES` | Maximum retry attempts per connection failure (default `3`, clamped between `0` and `20`). |
|
|
1187
|
+
| `CD_USE_NS` | When truthy (`1`/`true`/`yes`/`on`), every `login`/`run` uses full Linux namespace isolation (mount, PID, UTS, IPC, and cgroup when supported) **without** skipping any default bind mounts. Differs from `--isolated`, which also reduces the mount set. Forwarded across privilege elevation automatically. |
|
|
1171
1188
|
| `CD_FORCE_NO_COLORS` | When set, disables ANSI colours in Chroot-Distro output. |
|
|
1172
1189
|
| `COLUMNS` | Fallback terminal width for `--help` rendering. |
|
|
1173
1190
|
| `TERM`, `COLORTERM` | Inherited into the guest (always; even in `--minimal`). `TERM` defaults to `xterm-256color` when unset on the host. |
|
|
@@ -1260,9 +1277,10 @@ cp src/chroot_distro/completions/chroot-distro.fish \
|
|
|
1260
1277
|
- **Kernel features**: FUSE modules, real `iptables`, custom cgroup
|
|
1261
1278
|
hierarchies, and similar kernel-module features may not work inside the
|
|
1262
1279
|
guest.
|
|
1263
|
-
- **Namespaces**: `--isolated`
|
|
1264
|
-
|
|
1265
|
-
|
|
1280
|
+
- **Namespaces**: `--isolated` (or `CD_USE_NS=1`) provides
|
|
1281
|
+
mount/PID/UTS/IPC isolation, plus the cgroup namespace when the kernel
|
|
1282
|
+
supports it, via `unshare`/`nsenter` — but there is no network namespace,
|
|
1283
|
+
no user-namespace mapping, and no parity with Docker or Podman.
|
|
1266
1284
|
- **Bind mount hygiene**: crashed sessions or orphan processes can leave
|
|
1267
1285
|
mounts busy; `unmount` and lazy unmount mitigate this but orphaned
|
|
1268
1286
|
processes should be cleaned up.
|
|
@@ -439,7 +439,7 @@ chroot-distro login ubuntu --get-chroot-cmd
|
|
|
439
439
|
| Option | Description |
|
|
440
440
|
|---|---|
|
|
441
441
|
| `-u`, `--user USER` | Log in as USER (default: `root`). Accepts `name`, numeric `uid`, `name:group`, or `uid:gid`. |
|
|
442
|
-
| `--isolated` | Reduce host exposure and enable namespace isolation (mount, PID, UTS, IPC via `unshare`/`nsenter`). On Termux: also skip Android system, storage, and `$PREFIX` binds unless you opt in with `--shared-*` or `--bind`. (Fresh `/tmp` and `/run` are the default in every mode now, not just `--isolated`.) Mutually exclusive with `--minimal`. |
|
|
442
|
+
| `--isolated` | Reduce host exposure and enable namespace isolation (mount, PID, UTS, IPC, and cgroup when supported, via `unshare`/`nsenter`). On Termux: also skip Android system, storage, and `$PREFIX` binds unless you opt in with `--shared-*` or `--bind`. (Fresh `/tmp` and `/run` are the default in every mode now, not just `--isolated`.) Mutually exclusive with `--minimal`. To get the same namespace isolation **without** reducing the mount set, set `CD_USE_NS=1` instead (see [Environment variables](#environment-variables)). |
|
|
443
443
|
| `--minimal` | Bare minimum chroot: core pseudo-filesystems only (`/dev`, `/proc`, `/sys`, plus `/run`, `/dev/pts`, `/dev/shm` when present). Stripped guest environment. Mutually exclusive with `--isolated`. |
|
|
444
444
|
| `--shared-home` | Bind the invoking user's host home into the guest home (or `/root` for root). On Termux, binds `TERMUX_HOME`. |
|
|
445
445
|
| `--shared-tmp` | Bind host tmp (`/tmp` on Linux, `$PREFIX/tmp` on Termux) to `/tmp` in the guest. Opt-in only: by default the container gets its own fresh `/tmp`, not the host's. |
|
|
@@ -533,23 +533,38 @@ Termux.)
|
|
|
533
533
|
- Guest `ldconfig` is run inside the chroot to refresh the shared library
|
|
534
534
|
cache after the new libraries are bind-mounted.
|
|
535
535
|
|
|
536
|
-
#### Namespace isolation (`--isolated`)
|
|
536
|
+
#### Namespace isolation (`--isolated` and `CD_USE_NS`)
|
|
537
537
|
|
|
538
538
|
With `--isolated`, chroot-distro creates a per-container namespace holder
|
|
539
539
|
(`unshare`) and runs bind mounts, special mounts, and `chroot` inside that
|
|
540
540
|
environment (`nsenter`). Supported namespaces: **mount**, **PID**, **UTS**,
|
|
541
|
-
and **
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
541
|
+
**IPC**, and **cgroup**. The cgroup namespace is acquired best-effort (used
|
|
542
|
+
when the kernel supports it, skipped otherwise); the **mount/PID/UTS/IPC**
|
|
543
|
+
set is **all-or-nothing**: chroot-distro probes that set first, and if any
|
|
544
|
+
one of them is unsupported on the kernel it acquires none of them and falls
|
|
545
|
+
back fully to a non-isolated login (with a warning naming the missing
|
|
546
|
+
namespace), so a session is never left half-isolated. This is inspired by
|
|
546
547
|
[Ubuntu-Chroot](Ubuntu-Chroot/tools/chroot.sh) and is **not** a full
|
|
547
548
|
container runtime: there is no network namespace, no user namespace
|
|
548
549
|
mapping, and no image layering.
|
|
549
550
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
551
|
+
`--isolated` couples two things: namespace isolation **and** a reduced set
|
|
552
|
+
of host bind mounts (it skips the Android system/storage/`$PREFIX` binds on
|
|
553
|
+
Termux, and `/tmp`/display sharing on Linux, unless re-enabled with
|
|
554
|
+
`--shared-*` / `--bind`). If you want **only** the namespace isolation
|
|
555
|
+
while keeping every default mount, set the `CD_USE_NS=1` environment
|
|
556
|
+
variable instead: every `login`/`run` then runs in the same
|
|
557
|
+
mount/PID/UTS/IPC/cgroup namespaces but with the full default mount set
|
|
558
|
+
intact. `CD_USE_NS` accepts `1`/`true`/`yes`/`on`.
|
|
559
|
+
|
|
560
|
+
> `CD_USE_NS` is set by the invoking user but the tool re-executes itself
|
|
561
|
+
> as root; chroot-distro forwards the variable across `sudo`/`doas`/`pkexec`
|
|
562
|
+
> /`su` automatically, so it keeps working even when the `sudo` line prints
|
|
563
|
+
> `'-E' is ignored`.
|
|
564
|
+
|
|
565
|
+
Do not mix isolated (via `--isolated` or `CD_USE_NS`) and non-isolated
|
|
566
|
+
logins on the same container without running `chroot-distro unmount <name>`
|
|
567
|
+
first. Concurrent isolated sessions share the same holder and mounts.
|
|
553
568
|
|
|
554
569
|
#### Host bindings (Termux, default mode)
|
|
555
570
|
|
|
@@ -606,9 +621,10 @@ entries win):
|
|
|
606
621
|
`LIBGL_ALWAYS_SOFTWARE`. Your `--env` entries override these.
|
|
607
622
|
|
|
608
623
|
`HOSTNAME` is always set to the host system hostname name. The
|
|
609
|
-
`hostname`/`uname` commands report it only
|
|
610
|
-
|
|
611
|
-
report the host's name (no UTS
|
|
624
|
+
`hostname`/`uname` commands report it only when namespace isolation is
|
|
625
|
+
active (via `--isolated` or `CD_USE_NS`), where the UTS namespace is given
|
|
626
|
+
a real hostname; otherwise they still report the host's name (no UTS
|
|
627
|
+
namespace to change).
|
|
612
628
|
|
|
613
629
|
On Termux (unless isolated or minimal), `$PREFIX/bin` is appended to
|
|
614
630
|
`PATH`. A snippet at `/etc/profile.d/termux-profile.sh` re-applies
|
|
@@ -1139,6 +1155,7 @@ paths on Linux are typically under `/root/.local/share/` and
|
|
|
1139
1155
|
| `CD_DOWNLOAD_WORKERS` | Parallel registry layer downloads during `install` (default `4`, maximum `10`). Invalid values use the default; out-of-range values are clamped. |
|
|
1140
1156
|
| `CD_DOWNLOAD_RATE_LIMIT` | Bandwidth limit for downloads (e.g., `5M` for 5 MiB/s, default `0` = unlimited). Supports suffixes `K`, `M`, `G` (case-insensitive). |
|
|
1141
1157
|
| `CD_DOWNLOAD_MAX_RETRIES` | Maximum retry attempts per connection failure (default `3`, clamped between `0` and `20`). |
|
|
1158
|
+
| `CD_USE_NS` | When truthy (`1`/`true`/`yes`/`on`), every `login`/`run` uses full Linux namespace isolation (mount, PID, UTS, IPC, and cgroup when supported) **without** skipping any default bind mounts. Differs from `--isolated`, which also reduces the mount set. Forwarded across privilege elevation automatically. |
|
|
1142
1159
|
| `CD_FORCE_NO_COLORS` | When set, disables ANSI colours in Chroot-Distro output. |
|
|
1143
1160
|
| `COLUMNS` | Fallback terminal width for `--help` rendering. |
|
|
1144
1161
|
| `TERM`, `COLORTERM` | Inherited into the guest (always; even in `--minimal`). `TERM` defaults to `xterm-256color` when unset on the host. |
|
|
@@ -1231,9 +1248,10 @@ cp src/chroot_distro/completions/chroot-distro.fish \
|
|
|
1231
1248
|
- **Kernel features**: FUSE modules, real `iptables`, custom cgroup
|
|
1232
1249
|
hierarchies, and similar kernel-module features may not work inside the
|
|
1233
1250
|
guest.
|
|
1234
|
-
- **Namespaces**: `--isolated`
|
|
1235
|
-
|
|
1236
|
-
|
|
1251
|
+
- **Namespaces**: `--isolated` (or `CD_USE_NS=1`) provides
|
|
1252
|
+
mount/PID/UTS/IPC isolation, plus the cgroup namespace when the kernel
|
|
1253
|
+
supports it, via `unshare`/`nsenter` — but there is no network namespace,
|
|
1254
|
+
no user-namespace mapping, and no parity with Docker or Podman.
|
|
1237
1255
|
- **Bind mount hygiene**: crashed sessions or orphan processes can leave
|
|
1238
1256
|
mounts busy; `unmount` and lazy unmount mitigate this but orphaned
|
|
1239
1257
|
processes should be cleaned up.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "chroot-distro"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.2"
|
|
8
8
|
description = "chroot-distro is a lightweight Linux container management utility built around chroot."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -402,6 +402,19 @@ HELP_PAGES: dict[str, dict[str, typing.Any]] = {
|
|
|
402
402
|
if IS_TERMUX
|
|
403
403
|
else []
|
|
404
404
|
),
|
|
405
|
+
{
|
|
406
|
+
"title": "ENVIRONMENT",
|
|
407
|
+
"intro": (
|
|
408
|
+
"CD_USE_NS=1 enables full Linux namespace isolation "
|
|
409
|
+
"(mount, PID, UTS, IPC via unshare/nsenter) by default for "
|
|
410
|
+
"every login, while still keeping all the default bind "
|
|
411
|
+
"mounts. Unlike --isolated, it does NOT skip the extra "
|
|
412
|
+
"Android system/storage/$PREFIX (or Linux /tmp and display) "
|
|
413
|
+
"mounts: the goal is only namespace isolation, not a "
|
|
414
|
+
"reduced mount set. Accepts 1/true/yes/on. Use --isolated "
|
|
415
|
+
"when you also want the reduced mount set."
|
|
416
|
+
),
|
|
417
|
+
},
|
|
405
418
|
{
|
|
406
419
|
"title": "NOTES",
|
|
407
420
|
"intro": (
|
|
@@ -563,6 +576,19 @@ HELP_PAGES: dict[str, dict[str, typing.Any]] = {
|
|
|
563
576
|
f"{PROGRAM_NAME} run nextcloud",
|
|
564
577
|
f"{PROGRAM_NAME} run ubuntu --isolated -- /bin/echo hi",
|
|
565
578
|
],
|
|
579
|
+
"footer": [
|
|
580
|
+
{
|
|
581
|
+
"title": "ENVIRONMENT",
|
|
582
|
+
"intro": (
|
|
583
|
+
"CD_USE_NS=1 enables full Linux namespace isolation "
|
|
584
|
+
"(mount, PID, UTS, IPC via unshare/nsenter) by default for "
|
|
585
|
+
"every run, while still keeping all the default bind "
|
|
586
|
+
"mounts. Unlike --isolated, it does NOT skip the extra "
|
|
587
|
+
"Android system/storage/$PREFIX (or Linux /tmp and display) "
|
|
588
|
+
"mounts. Accepts 1/true/yes/on."
|
|
589
|
+
),
|
|
590
|
+
},
|
|
591
|
+
],
|
|
566
592
|
},
|
|
567
593
|
"kill": {
|
|
568
594
|
"usage": "kill CONTAINER",
|
|
@@ -445,6 +445,12 @@ def _command_login_inner(container_name: str, args) -> None:
|
|
|
445
445
|
login_wd = getattr(args, "work_dir", "") or ""
|
|
446
446
|
isolated = getattr(args, "isolated", False)
|
|
447
447
|
minimal = getattr(args, "minimal", False)
|
|
448
|
+
# `--isolated` skips the extra Android/host mounts AND uses namespaces.
|
|
449
|
+
# `CD_USE_NS` only turns on namespace isolation, keeping every mount.
|
|
450
|
+
# `skip_extra_mounts` therefore tracks only the real `--isolated` flag,
|
|
451
|
+
# while namespace setup is decided separately by should_use_namespaces().
|
|
452
|
+
skip_extra_mounts = isolated
|
|
453
|
+
use_ns_requested = namespace.should_use_namespaces(isolated)
|
|
448
454
|
use_shared_home = getattr(args, "shared_home", False)
|
|
449
455
|
shared_tmp = getattr(args, "shared_tmp", False)
|
|
450
456
|
shared_display = getattr(args, "shared_display", False)
|
|
@@ -504,13 +510,13 @@ def _command_login_inner(container_name: str, args) -> None:
|
|
|
504
510
|
container_path,
|
|
505
511
|
extra_env,
|
|
506
512
|
minimal,
|
|
507
|
-
|
|
513
|
+
skip_extra_mounts,
|
|
508
514
|
container_name=hostname_arg,
|
|
509
515
|
)
|
|
510
516
|
|
|
511
517
|
# A termux-type guest still needs its own cache dir to exist; create
|
|
512
518
|
# it inside the rootfs (never bound from the host).
|
|
513
|
-
if IS_TERMUX and not
|
|
519
|
+
if IS_TERMUX and not skip_extra_mounts:
|
|
514
520
|
os.makedirs(
|
|
515
521
|
os.path.join(rootfs, "data", "data", TERMUX_APP_PACKAGE, "cache"),
|
|
516
522
|
exist_ok=True,
|
|
@@ -656,7 +662,7 @@ def _command_login_inner(container_name: str, args) -> None:
|
|
|
656
662
|
login_home,
|
|
657
663
|
extra_env,
|
|
658
664
|
minimal,
|
|
659
|
-
|
|
665
|
+
skip_extra_mounts,
|
|
660
666
|
container_name=hostname_arg,
|
|
661
667
|
)
|
|
662
668
|
|
|
@@ -671,18 +677,18 @@ def _command_login_inner(container_name: str, args) -> None:
|
|
|
671
677
|
# guest's supplementary groups, DNS and all networking fail inside the
|
|
672
678
|
# chroot ("Temporary failure resolving"). Grant them on Termux unless the
|
|
673
679
|
# session is isolated or minimal.
|
|
674
|
-
if IS_TERMUX and not
|
|
680
|
+
if IS_TERMUX and not skip_extra_mounts and not minimal:
|
|
675
681
|
groups = list(groups)
|
|
676
682
|
for net_gid in ("3003", "3004"):
|
|
677
683
|
if net_gid not in groups:
|
|
678
684
|
groups.append(net_gid)
|
|
679
685
|
|
|
680
|
-
if IS_TERMUX and not
|
|
686
|
+
if IS_TERMUX and not skip_extra_mounts and not minimal:
|
|
681
687
|
termux_bin = f"{TERMUX_PREFIX}/bin"
|
|
682
688
|
components = [c for c in child_env.get("PATH", "").split(":") if c and c != termux_bin]
|
|
683
689
|
child_env["PATH"] = ":".join(components)
|
|
684
690
|
|
|
685
|
-
if dist_type == "normal" and IS_TERMUX and not
|
|
691
|
+
if dist_type == "normal" and IS_TERMUX and not skip_extra_mounts and not minimal:
|
|
686
692
|
profile_uid = int(login_uid) if login_uid is not None else 0
|
|
687
693
|
profile_gid = int(login_gid) if login_gid is not None else profile_uid
|
|
688
694
|
inject_termux_profile(
|
|
@@ -775,7 +781,8 @@ def _command_login_inner(container_name: str, args) -> None:
|
|
|
775
781
|
resolved_binds, rslave_targets = bindings.get_bindings(
|
|
776
782
|
rootfs=rootfs,
|
|
777
783
|
minimal=minimal,
|
|
778
|
-
isolated=
|
|
784
|
+
isolated=skip_extra_mounts,
|
|
785
|
+
use_namespaces=use_ns_requested and not minimal,
|
|
779
786
|
shared_home=use_shared_home,
|
|
780
787
|
shared_tmp=shared_tmp,
|
|
781
788
|
shared_display=shared_display,
|
|
@@ -795,7 +802,7 @@ def _command_login_inner(container_name: str, args) -> None:
|
|
|
795
802
|
if key not in user_env_keys_all:
|
|
796
803
|
child_env[key] = val
|
|
797
804
|
|
|
798
|
-
use_namespaces =
|
|
805
|
+
use_namespaces = use_ns_requested and not minimal
|
|
799
806
|
holder = None
|
|
800
807
|
pipe_w = None
|
|
801
808
|
chroot_args = None
|
|
@@ -812,7 +819,6 @@ def _command_login_inner(container_name: str, args) -> None:
|
|
|
812
819
|
f"(missing: {' '.join(missing)}). Falling back to non-isolated login."
|
|
813
820
|
)
|
|
814
821
|
use_namespaces = False
|
|
815
|
-
isolated = False
|
|
816
822
|
|
|
817
823
|
try:
|
|
818
824
|
host_mounts_exist = bool(mount_manager.get_active_mounts(rootfs))
|
|
@@ -874,7 +880,7 @@ def _command_login_inner(container_name: str, args) -> None:
|
|
|
874
880
|
else:
|
|
875
881
|
namespace.write_isolation_mode(container_name, namespace.ISOLATION_MODE_HOST)
|
|
876
882
|
|
|
877
|
-
if IS_TERMUX and not
|
|
883
|
+
if IS_TERMUX and not skip_extra_mounts and not minimal:
|
|
878
884
|
ensure_data_suid()
|
|
879
885
|
# Pre-clean stale mounts if any
|
|
880
886
|
with contextlib.suppress(Exception):
|
|
@@ -424,6 +424,7 @@ def get_bindings(
|
|
|
424
424
|
*,
|
|
425
425
|
minimal: bool = False,
|
|
426
426
|
isolated: bool = False,
|
|
427
|
+
use_namespaces: bool | None = None,
|
|
427
428
|
shared_home: bool = False,
|
|
428
429
|
shared_tmp: bool = False,
|
|
429
430
|
shared_display: bool = False,
|
|
@@ -444,11 +445,19 @@ def get_bindings(
|
|
|
444
445
|
binds = []
|
|
445
446
|
rslave_targets: list[str] = []
|
|
446
447
|
|
|
448
|
+
# When namespaces are active, a fresh procfs is mounted in the PID
|
|
449
|
+
# namespace by get_special_mounts(). CD_USE_NS keeps every other mount
|
|
450
|
+
# (isolated=False) but still namespaces PIDs, so the /proc decision must
|
|
451
|
+
# follow namespace use, not the mount-skipping flag. Default to *isolated*
|
|
452
|
+
# for backward compatibility when the caller does not pass it explicitly.
|
|
453
|
+
if use_namespaces is None:
|
|
454
|
+
use_namespaces = isolated
|
|
455
|
+
|
|
447
456
|
# 1. Base Linux mounts (always needed for chroot to function correctly)
|
|
448
457
|
# Target paths are absolute guest paths (e.g. /dev) which we will mount nested under rootfs.
|
|
449
458
|
binds.append(("/dev", "/dev"))
|
|
450
459
|
# Host /proc bind breaks PID namespace isolation; mount procfs in get_special_mounts().
|
|
451
|
-
if not
|
|
460
|
+
if not use_namespaces:
|
|
452
461
|
binds.append(("/proc", "/proc"))
|
|
453
462
|
binds.append(("/sys", "/sys"))
|
|
454
463
|
|
|
@@ -6,12 +6,35 @@ import sys
|
|
|
6
6
|
from chroot_distro.constants import IS_TERMUX
|
|
7
7
|
from chroot_distro.exceptions import RootRequiredError
|
|
8
8
|
|
|
9
|
+
# Runtime CD_* environment variables that influence behaviour *after* the
|
|
10
|
+
# tool re-executes as root. They must be forwarded explicitly across the
|
|
11
|
+
# privilege-elevation boundary because many sudoers policies strip the
|
|
12
|
+
# environment and ignore `sudo -E` ("preserving the entire environment is
|
|
13
|
+
# not supported, '-E' is ignored").
|
|
14
|
+
_FORWARDED_ENV_VARS = (
|
|
15
|
+
"CD_USE_NS",
|
|
16
|
+
"CD_DOCKER_AUTH",
|
|
17
|
+
"CD_DOWNLOAD_WORKERS",
|
|
18
|
+
"CD_DOWNLOAD_MAX_RETRIES",
|
|
19
|
+
"CD_DOWNLOAD_RATE_LIMIT",
|
|
20
|
+
)
|
|
21
|
+
|
|
9
22
|
|
|
10
23
|
def is_root() -> bool:
|
|
11
24
|
"""Check if the current process is running with root privileges (UID 0)."""
|
|
12
25
|
return os.getuid() == 0
|
|
13
26
|
|
|
14
27
|
|
|
28
|
+
def _forwarded_env_assignments() -> list[str]:
|
|
29
|
+
"""Return ``VAR=value`` strings for the CD_* vars present in the env."""
|
|
30
|
+
assignments: list[str] = []
|
|
31
|
+
for name in _FORWARDED_ENV_VARS:
|
|
32
|
+
value = os.environ.get(name)
|
|
33
|
+
if value is not None:
|
|
34
|
+
assignments.append(f"{name}={value}")
|
|
35
|
+
return assignments
|
|
36
|
+
|
|
37
|
+
|
|
15
38
|
def get_reexec_argv() -> list[str]:
|
|
16
39
|
"""Build the argument list for re-executing the current process."""
|
|
17
40
|
args = list(sys.argv)
|
|
@@ -71,14 +94,30 @@ def elevate_or_die() -> None:
|
|
|
71
94
|
|
|
72
95
|
reexec_argv = get_reexec_argv()
|
|
73
96
|
|
|
97
|
+
# Runtime CD_* vars set by the invoking user must cross the elevation
|
|
98
|
+
# boundary explicitly: `sudo -E` is frequently ignored by sudoers policy,
|
|
99
|
+
# which would silently drop e.g. CD_USE_NS and skip namespace isolation.
|
|
100
|
+
# The loop sentinel is forwarded the same way so it survives a stripped
|
|
101
|
+
# environment and still prevents an elevation loop.
|
|
102
|
+
env_assignments = ["_CHROOT_DISTRO_ELEVATING=1", *_forwarded_env_assignments()]
|
|
103
|
+
|
|
104
|
+
tool_name = tool_cmd[0]
|
|
105
|
+
|
|
106
|
+
# Prefix the re-executed program with `env VAR=value ...` so the
|
|
107
|
+
# forwarded variables are set by the root-side `env` binary. This is
|
|
108
|
+
# independent of the elevation tool's own environment policy (sudoers
|
|
109
|
+
# env_keep / -E, doas keepenv, pkexec sanitisation), which can otherwise
|
|
110
|
+
# silently drop CD_USE_NS and skip namespace isolation.
|
|
111
|
+
env_prefix = ["env", *env_assignments] if env_assignments else []
|
|
112
|
+
|
|
74
113
|
# Construct the final command line
|
|
75
114
|
if tool_cmd[-1] == "-c":
|
|
76
|
-
|
|
115
|
+
# su -c "<command string>": the whole invocation is a single string.
|
|
116
|
+
cmd_str = shlex.join([*env_prefix, *reexec_argv])
|
|
77
117
|
full_argv = [*tool_cmd, cmd_str]
|
|
78
118
|
else:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
tool_name = tool_cmd[0]
|
|
119
|
+
# sudo / doas / pkexec: run `env VAR=value <reexec>` as root.
|
|
120
|
+
full_argv = [*tool_cmd, *env_prefix, *reexec_argv]
|
|
82
121
|
|
|
83
122
|
try:
|
|
84
123
|
os.execvp(full_argv[0], full_argv)
|
|
@@ -16,17 +16,51 @@ from chroot_distro.exceptions import ChrootDistroError
|
|
|
16
16
|
|
|
17
17
|
log = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# Namespaces that must all be available for isolation to be acquired. The
|
|
20
|
+
# pre-flight check in command_login is all-or-nothing over this set, so it is
|
|
21
|
+
# kept to the widely supported namespaces; missing any of them means a full
|
|
22
|
+
# fall back to host mode rather than a half-isolated session.
|
|
23
|
+
_REQUIRED_PROBE_FLAGS = ("--pid", "--mount", "--uts", "--ipc")
|
|
24
|
+
# The cgroup namespace is acquired opportunistically: it is added to the
|
|
25
|
+
# holder when the kernel supports it (probe_unshare_flags drops unsupported
|
|
26
|
+
# flags), but it is NOT part of the strict pre-check because several Android
|
|
27
|
+
# kernels lack cgroupns and we still want pid/mount/uts/ipc isolation there.
|
|
28
|
+
_OPTIONAL_PROBE_FLAGS = ("--cgroup",)
|
|
29
|
+
_PROBE_FLAGS = _REQUIRED_PROBE_FLAGS + _OPTIONAL_PROBE_FLAGS
|
|
20
30
|
_LONG_TO_SHORT = {
|
|
21
31
|
"--mount": "-m",
|
|
22
32
|
"--uts": "-u",
|
|
23
33
|
"--ipc": "-i",
|
|
24
34
|
"--pid": "-p",
|
|
35
|
+
"--cgroup": "-C",
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
ISOLATION_MODE_NAMESPACE = "namespace"
|
|
28
39
|
ISOLATION_MODE_HOST = "host"
|
|
29
40
|
|
|
41
|
+
# Truthy spellings accepted for the CD_USE_NS environment variable.
|
|
42
|
+
_TRUTHY_ENV_VALUES = frozenset({"1", "true", "yes", "on"})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def use_ns_env_enabled() -> bool:
|
|
46
|
+
"""Return True when CD_USE_NS requests full namespace isolation.
|
|
47
|
+
|
|
48
|
+
CD_USE_NS turns on the same PID/UTS/IPC/mount namespace isolation that
|
|
49
|
+
``--isolated`` uses, but *without* skipping any of the default bind
|
|
50
|
+
mounts. Accepts ``1``/``true``/``yes``/``on`` (case-insensitive).
|
|
51
|
+
"""
|
|
52
|
+
return os.environ.get("CD_USE_NS", "").strip().lower() in _TRUTHY_ENV_VALUES
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def should_use_namespaces(isolated: bool) -> bool:
|
|
56
|
+
"""Decide whether to set up Linux namespace isolation.
|
|
57
|
+
|
|
58
|
+
Namespaces are used when the user passes ``--isolated`` (which also
|
|
59
|
+
skips the extra host mounts) or when ``CD_USE_NS`` is set (which keeps
|
|
60
|
+
every default mount and only adds namespace isolation).
|
|
61
|
+
"""
|
|
62
|
+
return bool(isolated) or use_ns_env_enabled()
|
|
63
|
+
|
|
30
64
|
# Android's toybox/toolbox `sleep` rejects the GNU coreutils `infinity`
|
|
31
65
|
# keyword ("sleep: Not a number 'infinity'") and aborts immediately, which
|
|
32
66
|
# tears down the namespace holder the moment it is created. Use a large
|
|
@@ -126,7 +160,7 @@ def probe_unshare_flags() -> list[str]:
|
|
|
126
160
|
return supported
|
|
127
161
|
|
|
128
162
|
|
|
129
|
-
def probe_namespace_support(flags: tuple[str, ...] =
|
|
163
|
+
def probe_namespace_support(flags: tuple[str, ...] = _REQUIRED_PROBE_FLAGS) -> list[str]:
|
|
130
164
|
"""Return the subset of *flags* the kernel does NOT support.
|
|
131
165
|
|
|
132
166
|
Probes each flag with `unshare <flag> true` without entering any
|
|
@@ -5,6 +5,7 @@ import pytest
|
|
|
5
5
|
|
|
6
6
|
from chroot_distro.elevate import (
|
|
7
7
|
_find_escalation_tool,
|
|
8
|
+
_forwarded_env_assignments,
|
|
8
9
|
elevate_or_die,
|
|
9
10
|
get_reexec_argv,
|
|
10
11
|
is_root,
|
|
@@ -127,14 +128,24 @@ def test_elevate_or_die_exec_sudo():
|
|
|
127
128
|
patch("chroot_distro.elevate._find_escalation_tool", return_value=["sudo", "-E"]),
|
|
128
129
|
patch("chroot_distro.elevate.get_reexec_argv", return_value=["/usr/bin/chroot-distro", "login", "alpine"]),
|
|
129
130
|
patch("os.execvp", mock_exec),
|
|
130
|
-
patch.dict("os.environ", {}),
|
|
131
|
+
patch.dict("os.environ", {}, clear=True),
|
|
131
132
|
):
|
|
132
133
|
elevate_or_die()
|
|
133
134
|
|
|
134
135
|
mock_exec.assert_called_once()
|
|
135
136
|
args, _kwargs = mock_exec.call_args
|
|
136
137
|
assert args[0] == "sudo"
|
|
137
|
-
|
|
138
|
+
# The loop sentinel is always forwarded via the `env` prefix so it
|
|
139
|
+
# survives a stripped environment.
|
|
140
|
+
assert args[1] == [
|
|
141
|
+
"sudo",
|
|
142
|
+
"-E",
|
|
143
|
+
"env",
|
|
144
|
+
"_CHROOT_DISTRO_ELEVATING=1",
|
|
145
|
+
"/usr/bin/chroot-distro",
|
|
146
|
+
"login",
|
|
147
|
+
"alpine",
|
|
148
|
+
]
|
|
138
149
|
assert os.environ.get("_CHROOT_DISTRO_ELEVATING") == "1"
|
|
139
150
|
|
|
140
151
|
|
|
@@ -145,12 +156,98 @@ def test_elevate_or_die_exec_su():
|
|
|
145
156
|
patch("chroot_distro.elevate._find_escalation_tool", return_value=["su", "-c"]),
|
|
146
157
|
patch("chroot_distro.elevate.get_reexec_argv", return_value=["/usr/bin/chroot-distro", "login", "alpine"]),
|
|
147
158
|
patch("os.execvp", mock_exec),
|
|
148
|
-
patch.dict("os.environ", {}),
|
|
159
|
+
patch.dict("os.environ", {}, clear=True),
|
|
149
160
|
):
|
|
150
161
|
elevate_or_die()
|
|
151
162
|
|
|
152
163
|
mock_exec.assert_called_once()
|
|
153
164
|
args, _kwargs = mock_exec.call_args
|
|
154
165
|
assert args[0] == "su"
|
|
155
|
-
|
|
166
|
+
# su -c gets a single command string with the env prefix embedded.
|
|
167
|
+
assert args[1] == [
|
|
168
|
+
"su",
|
|
169
|
+
"-c",
|
|
170
|
+
"env _CHROOT_DISTRO_ELEVATING=1 /usr/bin/chroot-distro login alpine",
|
|
171
|
+
]
|
|
156
172
|
assert os.environ.get("_CHROOT_DISTRO_ELEVATING") == "1"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_forwarded_env_assignments_only_present_vars():
|
|
176
|
+
with patch.dict(
|
|
177
|
+
"os.environ",
|
|
178
|
+
{"CD_USE_NS": "1", "CD_DOWNLOAD_WORKERS": "8", "UNRELATED": "x"},
|
|
179
|
+
clear=True,
|
|
180
|
+
):
|
|
181
|
+
assignments = _forwarded_env_assignments()
|
|
182
|
+
assert "CD_USE_NS=1" in assignments
|
|
183
|
+
assert "CD_DOWNLOAD_WORKERS=8" in assignments
|
|
184
|
+
# Variables that are not set must not appear.
|
|
185
|
+
assert all(not a.startswith("CD_DOCKER_AUTH=") for a in assignments)
|
|
186
|
+
# Unrelated host vars are never forwarded.
|
|
187
|
+
assert all(not a.startswith("UNRELATED=") for a in assignments)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_elevate_forwards_cd_use_ns_via_sudo():
|
|
191
|
+
mock_exec = MagicMock()
|
|
192
|
+
with (
|
|
193
|
+
patch("chroot_distro.elevate.is_root", return_value=False),
|
|
194
|
+
patch("chroot_distro.elevate._find_escalation_tool", return_value=["sudo", "-E"]),
|
|
195
|
+
patch("chroot_distro.elevate.get_reexec_argv", return_value=["/usr/bin/chroot-distro", "login", "ubuntu"]),
|
|
196
|
+
patch("os.execvp", mock_exec),
|
|
197
|
+
patch.dict("os.environ", {"CD_USE_NS": "1"}, clear=True),
|
|
198
|
+
):
|
|
199
|
+
elevate_or_die()
|
|
200
|
+
|
|
201
|
+
args, _kwargs = mock_exec.call_args
|
|
202
|
+
full_argv = args[1]
|
|
203
|
+
# CD_USE_NS must cross the boundary via the root-side `env` binary, not
|
|
204
|
+
# via `sudo -E` (which sudoers policy often ignores).
|
|
205
|
+
assert "env" in full_argv
|
|
206
|
+
assert "CD_USE_NS=1" in full_argv
|
|
207
|
+
# The forwarded assignments must precede the re-exec'd program.
|
|
208
|
+
assert full_argv.index("CD_USE_NS=1") < full_argv.index("/usr/bin/chroot-distro")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@pytest.mark.parametrize(
|
|
212
|
+
"tool_cmd",
|
|
213
|
+
[
|
|
214
|
+
["doas", "--"],
|
|
215
|
+
["pkexec", "--disable-internal-agent"],
|
|
216
|
+
],
|
|
217
|
+
)
|
|
218
|
+
def test_elevate_forwards_cd_use_ns_via_other_tools(tool_cmd):
|
|
219
|
+
mock_exec = MagicMock()
|
|
220
|
+
with (
|
|
221
|
+
patch("chroot_distro.elevate.is_root", return_value=False),
|
|
222
|
+
patch("chroot_distro.elevate._find_escalation_tool", return_value=tool_cmd),
|
|
223
|
+
patch("chroot_distro.elevate.get_reexec_argv", return_value=["/usr/bin/chroot-distro", "login", "ubuntu"]),
|
|
224
|
+
patch("os.execvp", mock_exec),
|
|
225
|
+
patch.dict("os.environ", {"CD_USE_NS": "yes"}, clear=True),
|
|
226
|
+
):
|
|
227
|
+
elevate_or_die()
|
|
228
|
+
|
|
229
|
+
args, _kwargs = mock_exec.call_args
|
|
230
|
+
full_argv = args[1]
|
|
231
|
+
assert full_argv[: len(tool_cmd)] == tool_cmd
|
|
232
|
+
assert "env" in full_argv
|
|
233
|
+
assert "CD_USE_NS=yes" in full_argv
|
|
234
|
+
assert full_argv.index("CD_USE_NS=yes") < full_argv.index("/usr/bin/chroot-distro")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_elevate_forwards_cd_use_ns_via_su():
|
|
238
|
+
mock_exec = MagicMock()
|
|
239
|
+
with (
|
|
240
|
+
patch("chroot_distro.elevate.is_root", return_value=False),
|
|
241
|
+
patch("chroot_distro.elevate._find_escalation_tool", return_value=["su", "-c"]),
|
|
242
|
+
patch("chroot_distro.elevate.get_reexec_argv", return_value=["/usr/bin/chroot-distro", "login", "ubuntu"]),
|
|
243
|
+
patch("os.execvp", mock_exec),
|
|
244
|
+
patch.dict("os.environ", {"CD_USE_NS": "1"}, clear=True),
|
|
245
|
+
):
|
|
246
|
+
elevate_or_die()
|
|
247
|
+
|
|
248
|
+
args, _kwargs = mock_exec.call_args
|
|
249
|
+
cmd_str = args[1][2]
|
|
250
|
+
assert "env" in cmd_str
|
|
251
|
+
assert "CD_USE_NS=1" in cmd_str
|
|
252
|
+
# Sentinel + CD_USE_NS appear before the program inside the command string.
|
|
253
|
+
assert cmd_str.index("CD_USE_NS=1") < cmd_str.index("/usr/bin/chroot-distro")
|