|
1 | 1 | defmodule Mix.Tasks.Suidhelper.Install do |
2 | | - @shortdoc "Build, stamp, and install the setuid helper (wraps `cargo xtask install`)" |
| 2 | + @shortdoc "Build, stamp, and install the setuid helper" |
3 | 3 | @moduledoc """ |
4 | | - Builds, stamps, and installs the Rust setuid helper by wrapping |
5 | | - `cargo xtask install` in `native/suidhelper`. |
| 4 | + Builds, stamps, and installs the Rust setuid helper. |
6 | 5 |
|
7 | 6 | mix suidhelper.install |
8 | 7 |
|
9 | | - The xtask first stamps the release binary (BLAKE3 self-checksum into |
10 | | - `.note.sum`, the same step the `:suidhelper_stamp` compiler runs) and then |
11 | | - installs it setuid-root to `/usr/local/bin/hyper-suidhelper` via `sudo |
12 | | - install`. `sudo` may prompt for a password on the controlling terminal. |
| 8 | + Two steps: |
13 | 9 |
|
14 | | - This is the privileged counterpart to `mix suidhelper.stamp`: that one only |
15 | | - rebuilds, stamps, and re-captures the embedded build identity; this one also |
16 | | - places the binary on `PATH` setuid-root. `cargo` and the helper's toolchain |
17 | | - (see `native/suidhelper/rust-toolchain.toml`) must be installed. |
| 10 | + 1. `cargo xtask stamp` in `native/suidhelper` builds the release binary and |
| 11 | + writes its BLAKE3 self-checksum into `.note.sum` (the same step the |
| 12 | + `:suidhelper_stamp` compiler runs). |
| 13 | + 2. The stamped binary is copied setuid-root (mode `4755`) to |
| 14 | + `/usr/local/bin/hyper-suidhelper`. |
| 15 | +
|
| 16 | + The copy needs root, but Mix runs every subprocess in its own session with no |
| 17 | + controlling terminal (`erl_child_setup` calls `setsid`), so a nested `sudo` |
| 18 | + cannot open `/dev/tty` to prompt for a password. This task therefore only runs |
| 19 | + `sudo` itself when it is already non-interactive (`sudo -n` succeeds, e.g. |
| 20 | + `NOPASSWD` or a usable cached credential). Otherwise it prints the exact |
| 21 | + privileged command for you to run in your own terminal. |
| 22 | +
|
| 23 | + This is the privileged counterpart to `mix suidhelper.stamp`, which stamps |
| 24 | + only. `cargo` and the helper's toolchain (see |
| 25 | + `native/suidhelper/rust-toolchain.toml`) must be installed. |
18 | 26 | """ |
19 | 27 |
|
20 | 28 | use Mix.Task |
21 | 29 |
|
22 | 30 | @helper_dir "native/suidhelper" |
| 31 | + @source Path.join(@helper_dir, "target/release/hyper-suidhelper") |
| 32 | + # Must match `Hyper.Config`'s default `suid_helper` path and the xtask's |
| 33 | + # `INSTALL_PATH`: a `PATH` location the unprivileged node can exec. |
| 34 | + @install_path "/usr/local/bin/hyper-suidhelper" |
23 | 35 |
|
24 | 36 | @impl Mix.Task |
25 | 37 | def run(argv) do |
26 | | - {_, 0} = |
27 | | - System.cmd("cargo", ["xtask", "install" | argv], |
28 | | - cd: @helper_dir, |
29 | | - into: IO.stream(:stdio, :line) |
30 | | - ) |
31 | | - rescue |
32 | | - MatchError -> |
33 | | - Mix.raise(""" |
34 | | - `cargo xtask install` failed installing the suidhelper. |
35 | | -
|
36 | | - Ensure `cargo` and the helper's toolchain (see #{@helper_dir}/rust-toolchain.toml) |
37 | | - are installed, and that `sudo` is available for the setuid install step. |
38 | | - """) |
| 38 | + stamp!(argv) |
| 39 | + install_privileged() |
| 40 | + end |
| 41 | + |
| 42 | + defp stamp!(argv) do |
| 43 | + case System.cmd("cargo", ["xtask", "stamp" | argv], |
| 44 | + cd: @helper_dir, |
| 45 | + into: IO.stream(:stdio, :line) |
| 46 | + ) do |
| 47 | + {_, 0} -> |
| 48 | + :ok |
| 49 | + |
| 50 | + {_, _} -> |
| 51 | + Mix.raise(""" |
| 52 | + `cargo xtask stamp` failed building the suidhelper. |
| 53 | +
|
| 54 | + Ensure `cargo` and the helper's toolchain (see #{@helper_dir}/rust-toolchain.toml) |
| 55 | + are installed. |
| 56 | + """) |
| 57 | + end |
| 58 | + end |
| 59 | + |
| 60 | + defp install_privileged do |
| 61 | + if passwordless_sudo?() do |
| 62 | + Mix.shell().info("Installing #{@source} -> #{@install_path} (setuid root)") |
| 63 | + |
| 64 | + case System.cmd("sudo", install_argv(), into: IO.stream(:stdio, :line)) do |
| 65 | + {_, 0} -> Mix.shell().info("installed #{@install_path} (setuid root)") |
| 66 | + {_, _} -> Mix.raise(manual_instructions()) |
| 67 | + end |
| 68 | + else |
| 69 | + Mix.shell().info(manual_instructions()) |
| 70 | + end |
| 71 | + end |
| 72 | + |
| 73 | + # `sudo -n true` exits 0 only when sudo can run without prompting. With no |
| 74 | + # controlling terminal a cached `tty_tickets` credential is invisible, so this |
| 75 | + # is true essentially only under `NOPASSWD` -- exactly the case where the |
| 76 | + # nested `sudo install` below can succeed. |
| 77 | + defp passwordless_sudo? do |
| 78 | + match?({_, 0}, System.cmd("sudo", ["-n", "true"], stderr_to_stdout: true)) |
| 79 | + end |
| 80 | + |
| 81 | + defp install_argv, |
| 82 | + do: ["install", "-o", "root", "-g", "root", "-m", "4755", @source, @install_path] |
| 83 | + |
| 84 | + defp manual_instructions do |
| 85 | + """ |
| 86 | +
|
| 87 | + The binary is built and stamped, but installing it setuid-root needs a |
| 88 | + password and `sudo` has no terminal to prompt on here. Run the copy yourself: |
| 89 | +
|
| 90 | + sudo #{Enum.join(install_argv(), " ")} |
| 91 | + """ |
39 | 92 | end |
40 | 93 | end |
0 commit comments