hive-c0re: grant hive-admin group a polkit rule for choom

This commit is contained in:
damocles 2026-09-07 23:27:15 +02:00
commit 66c3138dd1
3 changed files with 89 additions and 20 deletions

View file

@ -10,21 +10,65 @@ use anyhow::{Result, bail};
use crate::util::agent_exists;
/// Refuse early when the caller isn't root.
/// Whether the caller's supplementary groups include `hive-admin`.
///
/// `machinectl shell` needs root or a polkit grant, and hyperhive ships
/// no polkit rules — so unprivileged callers hit an authentication
/// prompt/refusal from systemd that says nothing about which hivectl
/// verb wanted it. Naming the requirement here keeps the failure honest
/// and points at the one thing that would fix it.
fn require_root() -> Result<()> {
// SAFETY: `geteuid` takes no arguments, reads a process attribute
// the kernel always has, and cannot fail.
/// hive-c0re's polkit rule (`nix/host-modules/hive-c0re/default.nix`) grants
/// `org.freedesktop.machine1.shell` to this group, so a member reaches the
/// same outcome as root without one. `getgrnam`/`getgroups` read the
/// *process's* actual credentials, the same ones the kernel would check —
/// not a fresh `/etc/group` lookup by name — so this can say no for a user
/// who was just added to the group but hasn't logged back in yet, same
/// caveat as the host-socket grant (`hivectl/src/client.rs::connect_hint`).
fn in_hive_admin_group() -> bool {
// SAFETY: `getgrnam` takes a valid NUL-terminated string literal and
// returns either null (no such group) or a pointer to static storage
// owned by libc, which is read once, immediately, before any other
// libc call could invalidate it.
let gid = unsafe {
let grp = libc::getgrnam(c"hive-admin".as_ptr());
if grp.is_null() {
return false;
}
(*grp).gr_gid
};
// SAFETY: a zero-length call with a null buffer is documented to return
// the caller's supplementary-group count without writing anything, so
// the real buffer below is sized to exactly what the second call needs.
let count = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
let Ok(count) = usize::try_from(count) else {
return false;
};
let mut groups = vec![0u32; count];
let Ok(capacity) = i32::try_from(groups.len()) else {
return false;
};
// SAFETY: `capacity` is exactly `groups.len()`, so this write cannot
// overflow the buffer.
let n = unsafe { libc::getgroups(capacity, groups.as_mut_ptr()) };
let Ok(n) = usize::try_from(n) else {
return false;
};
groups.truncate(n);
groups.contains(&gid)
}
/// Refuse early when the caller can't actually run `machinectl shell`.
///
/// Root always can. So can a `hive-admin` member, via the polkit grant
/// above. Anyone else hits an opaque polkit authentication prompt/refusal
/// from systemd that names neither hivectl nor the fix — naming both here
/// instead is the entire point of this check.
fn require_privilege() -> Result<()> {
// SAFETY: `geteuid` takes no arguments, reads a process attribute the
// kernel always has, and cannot fail.
let euid = unsafe { libc::geteuid() };
if euid != 0 {
if euid != 0 && !in_hive_admin_group() {
bail!(
"choom needs root: it runs `machinectl shell`, which requires root \
(hyperhive ships no polkit rule granting it) - re-run with sudo"
"choom needs root or `hive-admin` group membership: it runs `machinectl shell`, \
gated by a polkit rule for that group. If you were just added to `hive-admin`, \
log out and back in first secondary group membership applies at login, so a \
shell opened before the change still won't have it. `id -nG` shows what your \
running shell actually has"
);
}
Ok(())
@ -36,8 +80,8 @@ fn require_root() -> Result<()> {
/// user from its state dir, reproducing the harness's per-turn claude
/// invocation (flags, session selection, why it never collides with the
/// live harness session). See `docs/tools/hivectl.md` (Choom) for the
/// full rationale. Inherits the caller's PTY; requires root + a running
/// container.
/// full rationale. Inherits the caller's PTY; requires root or `hive-admin`
/// group membership, plus a running container.
///
/// The existence check goes over the daemon socket even though the exec
/// itself doesn't need it: the agents root isn't readable by the
@ -50,7 +94,7 @@ pub(crate) async fn choom(socket: &Path, name: &str, resume_session: Option<&str
hive_host_sock::AGENTS_ROOT
);
}
require_root()?;
require_privilege()?;
let container = hive_host_sock::container_name(name);
// Enter as the agent's unix user (== agent name) so claude reads the
// right `$HOME/.claude`.