Skip to content

fix(setup): Linux setup names the removal command for an earlier release's Browser host access - #418

Draft
SashaMIT wants to merge 4 commits into
developfrom
fix/browser-helper-leftover-notice
Draft

SashaMIT wants to merge 4 commits into
developfrom
fix/browser-helper-leftover-notice

Conversation

@SashaMIT

@SashaMIT SashaMIT commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Earlier releases' install.sh (prepare_browser_linux_host, scripts/install.sh:907-937 on develop) could run scripts/browser-vm-linux-network.py setup with sudo, after sudo install -D -o root -g root -m 755 of the helper (install.sh:925). That setup creates root-owned access for the account:

  • a helper copy at /usr/local/lib/elastos/browser-vm-linux-network.py (mode 0755, browser-vm-linux-network.py:311),
  • /etc/systemd/system/elastos-browser-network-<uid>.service,
  • /etc/udev/rules.d/70-elastos-browser-kvm-<uid>.rules,
  • a KVM user ACL, four TAP devices with firewall rules,
  • /var/lib/elastos-browser/<uid>.json (mode 0644, write_json, browser-vm-linux-network.py:197-201; written in configure, :259-318).

Browser is now unavailable on Linux (browser_available, setup/browser_config.rs:166). retire_host removes the user-owned Browser files from the data directory, but it cannot reach the root-owned access, and nothing told the user it was still there.

Change

Head: 4bf4b26a. Two commits on top of develop plus the zip 9 merge (241ba6a7, from #417, so cargo-audit can pass):

  1. 6ef7073d — retire_host calls earlier_linux_host_access on Linux (setup/browser_config.rs:356-360). It only reads file metadata for this account's paths and changes nothing outside the data directory. It does not ask for sudo.

  2. 4bf4b26a — the sudo command is printed only when every path it relies on is trusted (browser_config.rs:399-401):

    • the helper and the access record (root_only_file, :438): lstat (no link followed), a regular file, owned by uid 0, no set-id or sticky bit, no group or other write;
    • every folder above each, up to / (root_only_folders, :424): a real folder, not a link, owned by uid 0, no group or other write;
    • /usr/bin/python3 (root_only_program, :450): followed through at most 8 links, each owned by uid 0 in such folders, with no .. in a link target, to an executable regular file with the same rules.

    This is the rule of the helper's own trusted_path (scripts/browser-vm-linux-network.py:113-117: st_uid != 0, st_mode & 0o022, S_ISLNK, for the file and every parent), plus the regular-file and no-set-id checks.

    Then setup prints the helper's own removal command (the same command install.sh:936 printed):

    sudo /usr/bin/python3 -I /usr/local/lib/elastos/browser-vm-linux-network.py remove --user "$(id -un)"
    

    In every other case (a missing or untrusted helper, access record or interpreter) it fails closed: it prints a plain note naming this account's leftover files and "An administrator can remove these files", with no command.

The trusted owner is a parameter (earlier_linux_host_access(root, uid, owner), :384); production passes ROOT_UID (0), so tests run both as root and as an ordinary user.

Tests

All in setup/browser_config.rs, Linux only, on a fake root (earlier_linux_host_layout, :1071: folders 0755, helper and interpreter 0755, access record 0644, usr/bin/python3 -> python3.12):

  • earlier_linux_host_access_names_its_exact_removal_command (:1132): fresh host and another account's files give no notice; this account's trusted files give the exact command and stay in place; a missing access record or helper gives a file list without sudo.
  • earlier_linux_host_access_names_no_sudo_command_for_files_root_does_not_own (:1168): as root, each of the helper, /usr/local/lib/elastos, /usr/local, the access record, /var/lib/elastos-browser, /usr/bin/python3 (link, lchown), /usr/bin/python3.12, /usr/bin and / in turn is given to uid 1000 and the command disappears. As a non-root user (CI) the test's own files are ordinary user files and give no command with owner 0; the per-path chown cases are skipped with a message, because only uid 0 can give a file away. Both: files whose owner is not the trusted owner give no command.
  • earlier_linux_host_access_names_no_sudo_command_through_a_link (:1212): helper link, access-record link, helper folder link, higher folder (/usr/local) link, interpreter link with .., interpreter link loop.
  • earlier_linux_host_access_names_no_sudo_command_for_a_wrong_type_or_mode (:1262): helper or access record is a folder; helper 0775, 0757, 04755; access record 0666; helper folder 0777; interpreter 0644; /usr/bin 0775.

Negative proof. I reverted the check at browser_config.rs:399-401 to the 6ef7073d condition (helper.is_file() && state exists) and ran the tests as root:

test setup::browser_config::tests::earlier_linux_host_access_names_its_exact_removal_command ... ok
test setup::browser_config::tests::earlier_linux_host_access_names_no_sudo_command_for_files_root_does_not_own ... FAILED
test setup::browser_config::tests::earlier_linux_host_access_names_no_sudo_command_through_a_link ... FAILED
test setup::browser_config::tests::earlier_linux_host_access_names_no_sudo_command_for_a_wrong_type_or_mode ... FAILED
sudo command named: ["/usr/local/lib/elastos/browser-vm-linux-network.py", "/usr/local/lib/elastos", "/usr/local", "/var/lib/elastos-browser/1000.json", "/var/lib/elastos-browser", "/usr/bin/python3", "/usr/bin/python3.12", "/usr/bin", "/"]
sudo command named: ["access record is a link", "helper folder is a link", "a higher helper folder is a link", "interpreter link leaves its folder with ..", "interpreter links in a loop"]
sudo command named: ["access record is a folder", "helper is group-writable", "helper is writable by others", "helper is set-user-id", "access record is writable by others", "helper folder is writable by others", "interpreter is not executable", "interpreter folder is group-writable"]
test result: FAILED. 1 passed; 3 failed

(The old code already refused a helper that is a link or a folder.) With the fix restored, as root and as uid 65534 (setpriv):

test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 2989 filtered out   (cargo test -p elastos-server --lib browser_config, root)
skipped the per-path chown cases: only uid 0 can give a file to another account
test result: ok. 4 passed; 0 failed   (earlier_linux_host, uid 65534)

cargo clippy -p elastos-server --lib --tests -- -D warnings, cargo fmt --all -- --check and git diff --check are clean.

Gaps

  • The removal still needs a terminal and sudo. The launcher cannot do it without root, and this PR does not change the launcher. Decision Q3: the launcher will show this notice in plain language through structured status, not stdout: follow-up Runtime: report leftover root-owned Browser host access as structured status for the launcher #429 (the Runtime must emit the status first).
  • The check is advisory and read-only. Between the check and the user's sudo, only root can change the checked paths (they are root-owned in root-only folders), and the helper's remove re-checks its access record with trusted_json (browser-vm-linux-network.py:272).
  • On a host where /usr/local or /usr/local/lib is group-writable (for example an old Debian root:staff 2775 layout), the notice gives no command. That matches the helper's own trusted_path, which refuses that layout too.
  • /usr/bin/python3 is not checked to be Python; it is checked to be a root-owned executable that only root can change.
  • The notice repeats on each Linux elastos setup until the access is removed.

Generated by Claude Code

cargo-audit fails on every PR: zip 2.4.2 in elastos/Cargo.lock and in
capsules/object-provider/Cargo.lock (through its elastos-server path
dependency) has three advisories against ZipArchive::extract and the
stream extract API. The runtime calls neither; it reads each entry
itself. zip 9.0.4 is the patched release.

- elastos-server uses zip 9.0.4, still without default features. 9.x has
  no separate flate2 feature; deflate-flate2 now enables the dependency.
- Both lockfiles change only for zip: nix 0.31.3 and typed-path come in,
  derive_arbitrary goes. `cargo update -p zip` also moved unrelated range
  dependencies (windows-sys, getrandom); those lines stay as they were,
  and `cargo metadata --locked` accepts both files.
- ZipFile::name now returns a Result. The ZIP readers report an error as
  "invalid ZIP archive entry".
- Whole-archive extract keeps its rule for entry names. zip 9
  enclosed_name turns "/a.txt" into "a.txt" and folds "x/../a.txt" into
  "a.txt", so an entry that extract refuses today would be written. The
  runtime now checks the name with the rule zip 2.x applied: no NUL byte,
  no root or drive prefix, no climb above the archive root.

test_library_provider_extract_zip_refuses_absolute_dotdot_and_symlink_entries
covers absolute, "..", inner "..", backslash and symlink entries. It
passes on zip 2.4.2 and with this change, and fails on the Absolute.zip
case when extract uses zip 9 enclosed_name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…ase's Browser host access

An earlier installer could run scripts/browser-vm-linux-network.py setup with
sudo. That setup left a root-owned helper copy, a boot service, a KVM udev rule,
a KVM ACL and private network devices for the account. Linux setup now removes
only the user-owned Browser files, and nothing told the user that the root-owned
access was still there. retire_host now checks this account's files read-only.
When the helper and its access record are present, it prints the helper's own
removal command. Otherwise it names the files that are left. Setup changes
nothing outside the data directory and does not ask for sudo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
zip 9 for RUSTSEC-2026-0367/0368/0369 so cargo-audit passes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
The leftover Browser host access notice printed a sudo command when the
helper copy and its access record merely existed. A file that another
account can write, a link, or a link in a folder on the path could then
put a command that runs that account's code as root in front of the
user. The notice now checks each path with lstat, as the helper's own
trusted_path does: the helper, its access record and /usr/bin/python3
(through root-owned links) must be root-owned regular files with no
set-id bit and no group or other write, in real folders that only root
can change. Otherwise the notice names the files and no command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

4bf4b26a: the sudo removal command is printed only when the helper, its access record and /usr/bin/python3 pass lstat checks (root-owned regular files, no links, no group/other write, root-only folders up to /); otherwise a plain note with no command.
Proof: three new tests in setup/browser_config.rs (non-root owner per path, links, wrong type/mode) fail on the old check and pass now; follow-up for structured launcher status is #429.


Generated by Claude Code

@SashaMIT

Copy link
Copy Markdown
Contributor Author

Round 3 review: PASS (A). The sudo hint appears only when every path is root-owned; tests pass.

@andersalm andersalm mentioned this pull request Oct 17, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants