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.
Files changed (138) hide show
  1. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/PKG-INFO +35 -17
  2. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/README.md +34 -16
  3. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/pyproject.toml +1 -1
  4. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/help/pages.py +26 -0
  5. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/__init__.py +16 -10
  6. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/bindings.py +10 -1
  7. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/elevate.py +43 -4
  8. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/namespace.py +36 -2
  9. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_elevate.py +101 -4
  10. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_namespace.py +94 -1
  11. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/uv.lock +1 -1
  12. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.editorconfig +0 -0
  13. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/codeql/codeql-config.yml +0 -0
  14. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/dependabot.yml +0 -0
  15. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/workflows/ci.yml +0 -0
  16. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/workflows/codeql.yml +0 -0
  17. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.github/workflows/publish.yml +0 -0
  18. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.gitignore +0 -0
  19. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/.python-version +0 -0
  20. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/LICENSE +0 -0
  21. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/check-before-commit.sh +0 -0
  22. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/check-config.sh +0 -0
  23. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/__init__.py +0 -0
  24. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/arch.py +0 -0
  25. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/atomic.py +0 -0
  26. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/cli.py +0 -0
  27. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/backup.py +0 -0
  28. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/build.py +0 -0
  29. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/clear_cache.py +0 -0
  30. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/copy.py +0 -0
  31. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/diff.py +0 -0
  32. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/help/__init__.py +0 -0
  33. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/help/render.py +0 -0
  34. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/info.py +0 -0
  35. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/install.py +0 -0
  36. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/install_local.py +0 -0
  37. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/kill.py +0 -0
  38. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/list_cmd.py +0 -0
  39. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/chroot_cmd.py +0 -0
  40. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/env.py +0 -0
  41. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/login/passwd.py +0 -0
  42. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/ps.py +0 -0
  43. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/push.py +0 -0
  44. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/remove.py +0 -0
  45. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/rename.py +0 -0
  46. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/reset.py +0 -0
  47. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/restore.py +0 -0
  48. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/run.py +0 -0
  49. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/search.py +0 -0
  50. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/sync.py +0 -0
  51. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/commands/unmount.py +0 -0
  52. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/completions/_chroot-distro +0 -0
  53. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/completions/chroot-distro.bash +0 -0
  54. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/completions/chroot-distro.fish +0 -0
  55. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/constants.py +0 -0
  56. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/exceptions.py +0 -0
  57. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/__init__.py +0 -0
  58. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/android.py +0 -0
  59. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_cache.py +0 -0
  60. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/__init__.py +0 -0
  61. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/constants.py +0 -0
  62. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/copy_step.py +0 -0
  63. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/dockerignore.py +0 -0
  64. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/engine.py +0 -0
  65. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/errors.py +0 -0
  66. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/handlers.py +0 -0
  67. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/parsing.py +0 -0
  68. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/run_step.py +0 -0
  69. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/stage.py +0 -0
  70. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/build_engine/users.py +0 -0
  71. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/display.py +0 -0
  72. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/__init__.py +0 -0
  73. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/cache.py +0 -0
  74. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/layers.py +0 -0
  75. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/media.py +0 -0
  76. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/pull.py +0 -0
  77. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/push.py +0 -0
  78. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/refs.py +0 -0
  79. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/docker/transport.py +0 -0
  80. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/dockerfile.py +0 -0
  81. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/download.py +0 -0
  82. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/gpu.py +0 -0
  83. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/layer_diff.py +0 -0
  84. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/mount_manager.py +0 -0
  85. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/nvidia.py +0 -0
  86. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/oci_writer.py +0 -0
  87. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/rootfs.py +0 -0
  88. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/session.py +0 -0
  89. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/sound.py +0 -0
  90. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/tar_extract.py +0 -0
  91. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/wayland.py +0 -0
  92. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/helpers/x11.py +0 -0
  93. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/locking.py +0 -0
  94. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/message.py +0 -0
  95. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/names.py +0 -0
  96. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/parser.py +0 -0
  97. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/paths.py +0 -0
  98. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/progress.py +0 -0
  99. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/py.typed +0 -0
  100. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/src/chroot_distro/rate_limit.py +0 -0
  101. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/conftest.py +0 -0
  102. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_android.py +0 -0
  103. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_arch.py +0 -0
  104. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_backup_restore.py +0 -0
  105. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_bind_options.py +0 -0
  106. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_cli.py +0 -0
  107. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_constants.py +0 -0
  108. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_diff_baseline_cache.py +0 -0
  109. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_display.py +0 -0
  110. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_display_sockets.py +0 -0
  111. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_docker_refs.py +0 -0
  112. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_dockerfile.py +0 -0
  113. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_download_algorithms.py +0 -0
  114. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_download_blob_multi.py +0 -0
  115. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_download_multi.py +0 -0
  116. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_gpu.py +0 -0
  117. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_info.py +0 -0
  118. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_install.py +0 -0
  119. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_install_local.py +0 -0
  120. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_kill.py +0 -0
  121. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_layer_diff.py +0 -0
  122. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_list.py +0 -0
  123. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_locking.py +0 -0
  124. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_login_helpers.py +0 -0
  125. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_message.py +0 -0
  126. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_mount_manager_ns.py +0 -0
  127. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_names.py +0 -0
  128. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_parser.py +0 -0
  129. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_paths.py +0 -0
  130. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_progress.py +0 -0
  131. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_push_chunked.py +0 -0
  132. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_remove.py +0 -0
  133. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_rootfs.py +0 -0
  134. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_sound.py +0 -0
  135. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_tar_extract.py +0 -0
  136. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_unmount.py +0 -0
  137. {chroot_distro-2.3.1 → chroot_distro-2.3.2}/tests/unit/test_wayland.py +0 -0
  138. {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.1
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 **IPC**. Isolation is **all-or-nothing**: chroot-distro probes the full
571
- requested namespace set first, and if any one of them is unsupported on the
572
- kernel it acquires none of them and falls back fully to a non-isolated
573
- login (with a warning naming the missing namespace), so a session is never
574
- left half-isolated. This is inspired by
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
- Do not mix `--isolated` and non-isolated logins on the same container
580
- without running `chroot-distro unmount <name>` first. Concurrent
581
- `--isolated` sessions share the same holder and mounts.
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 under `--isolated`, where the
639
- UTS namespace is given a real hostname; without `--isolated` they still
640
- report the host's name (no UTS namespace to change).
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` provides mount/PID/UTS/IPC isolation via
1264
- `unshare`/`nsenter`, but there is no network namespace and no parity with
1265
- Docker or Podman.
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 **IPC**. Isolation is **all-or-nothing**: chroot-distro probes the full
542
- requested namespace set first, and if any one of them is unsupported on the
543
- kernel it acquires none of them and falls back fully to a non-isolated
544
- login (with a warning naming the missing namespace), so a session is never
545
- left half-isolated. This is inspired by
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
- Do not mix `--isolated` and non-isolated logins on the same container
551
- without running `chroot-distro unmount <name>` first. Concurrent
552
- `--isolated` sessions share the same holder and mounts.
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 under `--isolated`, where the
610
- UTS namespace is given a real hostname; without `--isolated` they still
611
- report the host's name (no UTS namespace to change).
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` provides mount/PID/UTS/IPC isolation via
1235
- `unshare`/`nsenter`, but there is no network namespace and no parity with
1236
- Docker or Podman.
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.1"
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
- isolated,
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 isolated:
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
- isolated,
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 isolated and not minimal:
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 isolated and not minimal:
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 isolated and not minimal:
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=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 = isolated and not minimal
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 isolated and not minimal:
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 isolated:
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
- cmd_str = shlex.join(reexec_argv)
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
- full_argv = [*tool_cmd, *reexec_argv]
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
- _PROBE_FLAGS = ("--pid", "--mount", "--uts", "--ipc")
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, ...] = _PROBE_FLAGS) -> list[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
- assert args[1] == ["sudo", "-E", "/usr/bin/chroot-distro", "login", "alpine"]
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
- assert args[1] == ["su", "-c", "/usr/bin/chroot-distro login alpine"]
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")