From 62002efd08158512f6015d47461d264a14961553 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sat, 28 Mar 2026 18:06:12 +0900 Subject: [PATCH] cmd/hsu: document hsurc format and internals This was previously only documented via an unexported function. Signed-off-by: Ophestra --- cmd/hsu/hst.go | 2 +- cmd/hsu/main.go | 83 +++++++++++++++++++++++++++++++++++++++++------- cmd/hsu/parse.go | 21 ++++++------ 3 files changed, 83 insertions(+), 23 deletions(-) diff --git a/cmd/hsu/hst.go b/cmd/hsu/hst.go index d7ffb89a..ed0e5575 100644 --- a/cmd/hsu/hst.go +++ b/cmd/hsu/hst.go @@ -1,6 +1,6 @@ package main -/* copied from hst and must never be changed */ +/* keep in sync with hst */ const ( userOffset = 100000 diff --git a/cmd/hsu/main.go b/cmd/hsu/main.go index 5c626a86..497279b8 100644 --- a/cmd/hsu/main.go +++ b/cmd/hsu/main.go @@ -1,7 +1,58 @@ +// hsu starts the hakurei shim as the target subordinate user. +// +// The hsu program must be installed with the setuid and setgid bit set, and +// owned by root. A configuration file must be installed at /etc/hsurc with +// permission bits 0400, and owned by root. Each line of the file specifies a +// hakurei userid to kernel uid mapping. A line consists of the decimal string +// representation of the uid of the user wishing to start hakurei containers, +// followed by a space, followed by the decimal string representation of its +// userid. Duplicate uid entries are ignored, with the first occurrence taking +// effect. +// +// For example, to map the kernel uid 1000 to the hakurei user id 0: +// +// 1000 0 +// +// # Internals +// +// Hakurei and hsu holds pathnames pointing to each other set at link time. For +// this reason, a distribution of hakurei has fixed installation prefix. Since +// this program is never invoked by the user, behaviour described in the +// following paragraphs are considered an internal detail and not covered by the +// compatibility promise. +// +// After checking credentials, hsu checks via /proc/ the absolute pathname of +// its parent process, and fails if it does not match the hakurei pathname set +// at link time. This is not a security feature: the priv-side is considered +// trusted, and this feature makes no attempt to address the racy nature of +// querying /proc/, or debuggers attached to the parent process. Instead, this +// aims to discourage misuse and reduce confusion if the user accidentally +// stumbles upon this program. It also prevents accidental use of the incorrect +// installation of hsu in some environments. +// +// Since target container environment variables are set up in shim via the +// [container] infrastructure, the environment is used for parameters from the +// parent process. +// +// HAKUREI_SHIM specifies a single byte between '3' and '9' representing the +// setup pipe file descriptor. It is passed as is to the shim process and is the +// only value in the environment of the shim process. Since hsurc is not +// accessible to the parent process, leaving this unset causes hsu to print the +// corresponding hakurei user id of the parent and terminate. +// +// HAKUREI_IDENTITY specifies the identity of the instance being started and is +// used to produce the kernel uid alongside hakurei user id looked up from hsurc. +// +// HAKUREI_GROUPS specifies supplementary groups to inherit from the credentials +// of the parent process in a ' ' separated list of decimal string +// representations of gid. This has the unfortunate consequence of allowing +// users mapped via hsurc to effectively drop group membership, so special care +// must be taken to ensure this does not lead to an increase in access. This is +// not applicable to Rosa OS since unsigned code execution is not permitted +// outside hakurei containers, and is generally nonapplicable to the security +// model of hakurei, where all untrusted code runs within containers. package main -// minimise imports to avoid inadvertently calling init or global variable functions - import ( "bytes" "fmt" @@ -16,10 +67,13 @@ import ( ) const ( - // envIdentity is the name of the environment variable holding a - // single byte representing the shim setup pipe file descriptor. + // envShim is the name of the environment variable holding a single byte + // representing the shim setup pipe file descriptor. envShim = "HAKUREI_SHIM" - // envGroups holds a ' ' separated list of string representations of + // envIdentity is the name of the environment variable holding a decimal + // string representation of the current application identity. + envIdentity = "HAKUREI_IDENTITY" + // envGroups holds a ' ' separated list of decimal string representations of // supplementary group gid. Membership requirements are enforced. envGroups = "HAKUREI_GROUPS" ) @@ -35,7 +89,6 @@ func main() { log.SetFlags(0) log.SetPrefix("hsu: ") - log.SetOutput(os.Stderr) if os.Geteuid() != 0 { log.Fatal("this program must be owned by uid 0 and have the setuid bit set") @@ -99,8 +152,6 @@ func main() { // last possible uid outcome uidEnd = 999919999 ) - - // cast to int for use with library functions uid := int(toUser(userid, identity)) // final bounds check to catch any bugs @@ -136,7 +187,6 @@ func main() { } // careful! users in the allowlist is effectively allowed to drop groups via hsu - if err := syscall.Setresgid(uid, uid, uid); err != nil { log.Fatalf("cannot set gid: %v", err) } @@ -146,10 +196,21 @@ func main() { if err := syscall.Setresuid(uid, uid, uid); err != nil { log.Fatalf("cannot set uid: %v", err) } - if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 { + + if _, _, errno := syscall.AllThreadsSyscall( + syscall.SYS_PRCTL, + PR_SET_NO_NEW_PRIVS, 1, + 0, + ); errno != 0 { log.Fatalf("cannot set no_new_privs flag: %s", errno.Error()) } - if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil { + + if err := syscall.Exec(toolPath, []string{ + "hakurei", + "shim", + }, []string{ + envShim + "=" + shimSetupFd, + }); err != nil { log.Fatalf("cannot start shim: %v", err) } diff --git a/cmd/hsu/parse.go b/cmd/hsu/parse.go index 6003f6cf..d4f7df43 100644 --- a/cmd/hsu/parse.go +++ b/cmd/hsu/parse.go @@ -18,8 +18,9 @@ const ( useridEnd = useridStart + rangeSize - 1 ) -// parseUint32Fast parses a string representation of an unsigned 32-bit integer value -// using the fast path only. This limits the range of values it is defined in. +// parseUint32Fast parses a string representation of an unsigned 32-bit integer +// value using the fast path only. This limits the range of values it is defined +// in but is perfectly adequate for this use case. func parseUint32Fast(s string) (uint32, error) { sLen := len(s) if sLen < 1 { @@ -40,12 +41,14 @@ func parseUint32Fast(s string) (uint32, error) { return n, nil } -// parseConfig reads a list of allowed users from r until it encounters puid or [io.EOF]. +// parseConfig reads a list of allowed users from r until it encounters puid or +// [io.EOF]. // -// Each line of the file specifies a hakurei userid to kernel uid mapping. A line consists -// of the string representation of the uid of the user wishing to start hakurei containers, -// followed by a space, followed by the string representation of its userid. Duplicate uid -// entries are ignored, with the first occurrence taking effect. +// Each line of the file specifies a hakurei userid to kernel uid mapping. A +// line consists of the string representation of the uid of the user wishing to +// start hakurei containers, followed by a space, followed by the string +// representation of its userid. Duplicate uid entries are ignored, with the +// first occurrence taking effect. // // All string representations are parsed by calling parseUint32Fast. func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) { @@ -112,10 +115,6 @@ func mustParseConfig(puid int) (userid uint32) { return } -// envIdentity is the name of the environment variable holding a -// string representation of the current application identity. -var envIdentity = "HAKUREI_IDENTITY" - // mustReadIdentity calls parseUint32Fast to interpret the value stored in envIdentity, // terminating the program if the value is not set, malformed, or out of bounds. func mustReadIdentity() uint32 {