diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-11-04 08:00:37 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-11-04 08:24:41 +0900 |
| commit | cb9ebf0e1541c0e49380cda6d4c2d619a0e9933f (patch) | |
| tree | 8bd0bfe3714fd5975dcacb69c0a2afcd2e04c687 /cmd/hsu | |
| parent | 9a2a7b749f41f73c2c11e4a4557516307838457b (diff) | |
hst/grp_pwd: specify new uid format
This leaves slots available for additional uid ranges in Rosa OS.
This breaks all existing installations! Users are required to fix ownership manually.
Closes #18.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd/hsu')
| -rw-r--r-- | cmd/hsu/hst.go | 16 | ||||
| -rw-r--r-- | cmd/hsu/main.go | 67 | ||||
| -rw-r--r-- | cmd/hsu/parse.go | 104 | ||||
| -rw-r--r-- | cmd/hsu/parse_test.go | 41 |
4 files changed, 145 insertions, 83 deletions
diff --git a/cmd/hsu/hst.go b/cmd/hsu/hst.go new file mode 100644 index 00000000..d7ffb89a --- /dev/null +++ b/cmd/hsu/hst.go @@ -0,0 +1,16 @@ +package main + +/* copied from hst and must never be changed */ + +const ( + userOffset = 100000 + rangeSize = userOffset / 10 + + identityStart = 0 + identityEnd = appEnd - appStart + + appStart = rangeSize * 1 + appEnd = appStart + rangeSize - 1 +) + +func toUser(userid, appid uint32) uint32 { return userid*userOffset + appStart + appid } diff --git a/cmd/hsu/main.go b/cmd/hsu/main.go index 7e3b3a7b..5c626a86 100644 --- a/cmd/hsu/main.go +++ b/cmd/hsu/main.go @@ -16,15 +16,12 @@ import ( ) const ( - hsuConfFile = "/etc/hsurc" - envShim = "HAKUREI_SHIM" - envIdentity = "HAKUREI_IDENTITY" - envGroups = "HAKUREI_GROUPS" - - PR_SET_NO_NEW_PRIVS = 0x26 - - identityMin = 0 - identityMax = 9999 + // envIdentity 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 + // supplementary group gid. Membership requirements are enforced. + envGroups = "HAKUREI_GROUPS" ) // hakureiPath is the absolute path to Hakurei. @@ -33,6 +30,7 @@ const ( var hakureiPath string func main() { + const PR_SET_NO_NEW_PRIVS = 0x26 runtime.LockOSThread() log.SetFlags(0) @@ -68,13 +66,8 @@ func main() { toolPath = p } - // uid = 1000000 + - // id * 10000 + - // identity - uid := 1000000 - // refuse to run if hsurc is not protected correctly - if s, err := os.Stat(hsuConfFile); err != nil { + if s, err := os.Stat(hsuConfPath); err != nil { log.Fatal(err) } else if s.Mode().Perm() != 0400 { log.Fatal("bad hsurc perm") @@ -83,25 +76,13 @@ func main() { } // authenticate before accepting user input - var id int - if f, err := os.Open(hsuConfFile); err != nil { - log.Fatal(err) - } else if v, ok := mustParseConfig(f, puid); !ok { - log.Fatalf("uid %d is not in the hsurc file", puid) - } else { - id = v - if err = f.Close(); err != nil { - log.Fatal(err) - } - - uid += id * 10000 - } + userid := mustParseConfig(puid) // pass through setup fd to shim var shimSetupFd string if s, ok := os.LookupEnv(envShim); !ok { // hakurei requests hsurc user id - fmt.Print(id) + fmt.Print(userid) os.Exit(0) } else if len(s) != 1 || s[0] > '9' || s[0] < '3' { log.Fatal("HAKUREI_SHIM holds an invalid value") @@ -109,13 +90,22 @@ func main() { shimSetupFd = s } - // allowed identity range 0 to 9999 - if as, ok := os.LookupEnv(envIdentity); !ok { - log.Fatal("HAKUREI_IDENTITY not set") - } else if identity, err := parseUint32Fast(as); err != nil || identity < identityMin || identity > identityMax { - log.Fatal("invalid identity") - } else { - uid += identity + // start is going ahead at this point + identity := mustReadIdentity() + + const ( + // first possible uid outcome + uidStart = 10000 + // 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 + if uid < uidStart || uid >= uidEnd { + panic("uid out of bounds") } // supplementary groups @@ -145,11 +135,6 @@ func main() { suppGroups = []int{uid} } - // final bounds check to catch any bugs - if uid < 1000000 || uid >= 2000000 { - panic("uid out of bounds") - } - // careful! users in the allowlist is effectively allowed to drop groups via hsu if err := syscall.Setresgid(uid, uid, uid); err != nil { diff --git a/cmd/hsu/parse.go b/cmd/hsu/parse.go index 35704c61..6003f6cf 100644 --- a/cmd/hsu/parse.go +++ b/cmd/hsu/parse.go @@ -6,62 +6,128 @@ import ( "fmt" "io" "log" + "math" + "os" "strings" ) -func parseUint32Fast(s string) (int, error) { +const ( + // useridStart is the first userid. + useridStart = 0 + // useridEnd is the last userid. + 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. +func parseUint32Fast(s string) (uint32, error) { sLen := len(s) if sLen < 1 { - return -1, errors.New("zero length string") + return 0, errors.New("zero length string") } if sLen > 10 { - return -1, errors.New("string too long") + return 0, errors.New("string too long") } - n := 0 + var n uint32 for i, ch := range []byte(s) { ch -= '0' if ch > 9 { - return -1, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i) + return 0, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i) } - n = n*10 + int(ch) + n = n*10 + uint32(ch) } return n, nil } -func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) { +// 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. +// +// All string representations are parsed by calling parseUint32Fast. +func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) { s := bufio.NewScanner(r) - var line, puid0 int + var ( + line uintptr + puid0 uint32 + ) for s.Scan() { line++ - // <puid> <fid> + // <puid> <userid> lf := strings.SplitN(s.Text(), " ", 2) if len(lf) != 2 { - return -1, false, fmt.Errorf("invalid entry on line %d", line) + return useridEnd + 1, false, fmt.Errorf("invalid entry on line %d", line) } puid0, err = parseUint32Fast(lf[0]) if err != nil || puid0 < 1 { - return -1, false, fmt.Errorf("invalid parent uid on line %d", line) + return useridEnd + 1, false, fmt.Errorf("invalid parent uid on line %d", line) } ok = puid0 == puid if ok { - // allowed fid range 0 to 99 - if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 { - return -1, false, fmt.Errorf("invalid identity on line %d", line) + // userid bound to a range, uint32 size allows this to be increased if needed + if userid, err = parseUint32Fast(lf[1]); err != nil || + userid < useridStart || userid > useridEnd { + return useridEnd + 1, false, fmt.Errorf("invalid userid on line %d", line) } return } } - return -1, false, s.Err() + return useridEnd + 1, false, s.Err() } -func mustParseConfig(r io.Reader, puid int) (int, bool) { - fid, ok, err := parseConfig(r, puid) - if err != nil { +// hsuConfPath is an absolute pathname to the hsu configuration file. +// Its contents are interpreted by parseConfig. +const hsuConfPath = "/etc/hsurc" + +// mustParseConfig calls parseConfig to interpret the contents of hsuConfPath, +// terminating the program if an error is encountered, the syntax is incorrect, +// or the current user is not authorised to use hsu because its uid is missing. +// +// Therefore, code after this function call can assume an authenticated state. +// +// mustParseConfig returns the userid value of the current user. +func mustParseConfig(puid int) (userid uint32) { + if puid > math.MaxUint32 { + log.Fatalf("got impossible uid %d", puid) + } + + var ok bool + if f, err := os.Open(hsuConfPath); err != nil { + log.Fatal(err) + } else if userid, ok, err = parseConfig(f, uint32(puid)); err != nil { + log.Fatal(err) + } else if err = f.Close(); err != nil { log.Fatal(err) } - return fid, ok + if !ok { + log.Fatalf("uid %d is not in the hsurc file", puid) + } + + 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 { + // ranges defined in hst and copied to this package to avoid importing hst + if as, ok := os.LookupEnv(envIdentity); !ok { + log.Fatal("HAKUREI_IDENTITY not set") + panic("unreachable") + } else if identity, err := parseUint32Fast(as); err != nil || + identity < identityStart || identity > identityEnd { + log.Fatal("invalid identity") + panic("unreachable") + } else { + return identity + } } diff --git a/cmd/hsu/parse_test.go b/cmd/hsu/parse_test.go index 7b5ec09f..d90851d7 100644 --- a/cmd/hsu/parse_test.go +++ b/cmd/hsu/parse_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "math" "strconv" "testing" ) @@ -39,22 +40,20 @@ func TestParseUint32Fast(t *testing.T) { t.Run("range", func(t *testing.T) { t.Parallel() - testRange := func(i, end int) { + testRange := func(i, end uint32) { for ; i < end; i++ { - s := strconv.Itoa(i) + s := strconv.Itoa(int(i)) w := i t.Run("parse "+s, func(t *testing.T) { t.Parallel() v, err := parseUint32Fast(s) if err != nil { - t.Errorf("parseUint32Fast(%q): error = %v", - s, err) + t.Errorf("parseUint32Fast(%q): error = %v", s, err) return } if v != w { - t.Errorf("parseUint32Fast(%q): got %v", - s, v) + t.Errorf("parseUint32Fast(%q): got %v", s, v) return } }) @@ -63,7 +62,7 @@ func TestParseUint32Fast(t *testing.T) { testRange(0, 2500) testRange(23002500, 23005000) - testRange(7890002500, 7890005000) + testRange(math.MaxUint32-2500, math.MaxUint32) }) } @@ -72,14 +71,14 @@ func TestParseConfig(t *testing.T) { testCases := []struct { name string - puid, want int + puid, want uint32 wantErr string rc string }{ - {"empty", 0, -1, "", ``}, - {"invalid field", 0, -1, "invalid entry on line 1", `9`}, - {"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`}, - {"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`}, + {"empty", 0, useridEnd + 1, "", ``}, + {"invalid field", 0, useridEnd + 1, "invalid entry on line 1", `9`}, + {"invalid puid", 0, useridEnd + 1, "invalid parent uid on line 1", `f 9`}, + {"invalid userid", 1000, useridEnd + 1, "invalid userid on line 1", `1000 f`}, {"match", 1000, 0, "", `1000 0`}, } @@ -87,25 +86,21 @@ func TestParseConfig(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - fid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid) + userid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid) if err == nil && tc.wantErr != "" { - t.Errorf("parseConfig: error = %v; wantErr %q", - err, tc.wantErr) + t.Errorf("parseConfig: error = %v; want %q", err, tc.wantErr) return } if err != nil && err.Error() != tc.wantErr { - t.Errorf("parseConfig: error = %q; wantErr %q", - err, tc.wantErr) + t.Errorf("parseConfig: error = %q; want %q", err, tc.wantErr) return } - if ok == (tc.want == -1) { - t.Errorf("parseConfig: ok = %v; want %v", - ok, tc.want) + if ok == (tc.want == useridEnd+1) { + t.Errorf("parseConfig: ok = %v; want %v", ok, tc.want) return } - if fid != tc.want { - t.Errorf("parseConfig: fid = %v; want %v", - fid, tc.want) + if userid != tc.want { + t.Errorf("parseConfig: %v; want %v", userid, tc.want) } }) } |
