aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-11-04 08:00:37 +0900
committerOphestra <cat@gensokyo.uk>2025-11-04 08:24:41 +0900
commitcb9ebf0e1541c0e49380cda6d4c2d619a0e9933f (patch)
tree8bd0bfe3714fd5975dcacb69c0a2afcd2e04c687 /cmd
parent9a2a7b749f41f73c2c11e4a4557516307838457b (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')
-rw-r--r--cmd/hakurei/command.go4
-rw-r--r--cmd/hakurei/parse_test.go2
-rw-r--r--cmd/hpkg/test/test.py4
-rw-r--r--cmd/hsu/hst.go16
-rw-r--r--cmd/hsu/main.go67
-rw-r--r--cmd/hsu/parse.go104
-rw-r--r--cmd/hsu/parse_test.go41
7 files changed, 150 insertions, 88 deletions
diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go
index 6f727124..a741ecf6 100644
--- a/cmd/hakurei/command.go
+++ b/cmd/hakurei/command.go
@@ -86,7 +86,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
)
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
- if flagIdentity < hst.IdentityMin || flagIdentity > hst.IdentityMax {
+ if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
log.Fatalf("identity %d out of range", flagIdentity)
}
@@ -95,7 +95,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
- us := strconv.Itoa(outcome.HsuUid(new(outcome.Hsu).MustID(msg), flagIdentity))
+ us := strconv.Itoa(hst.ToUser(new(outcome.Hsu).MustID(msg), flagIdentity))
if u, err := user.LookupId(us); err != nil {
msg.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
diff --git a/cmd/hakurei/parse_test.go b/cmd/hakurei/parse_test.go
index e98e945f..ae7c8d51 100644
--- a/cmd/hakurei/parse_test.go
+++ b/cmd/hakurei/parse_test.go
@@ -41,7 +41,7 @@ func TestTryIdentifier(t *testing.T) {
{ID: (hst.ID)(bytes.Repeat([]byte{0xfe}, len(hst.ID{}))), PID: 0xbed, ShimPID: 0xfff, Config: func() *hst.Config {
template := hst.Template()
- template.Identity = hst.IdentityMax
+ template.Identity = hst.IdentityEnd
return template
}(), Time: time.Unix(0, 0xcafebabe0)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xfc}, len(hst.ID{}))), PID: 0x1bed, ShimPID: 0x1fff, Config: func() *hst.Config {
diff --git a/cmd/hpkg/test/test.py b/cmd/hpkg/test/test.py
index 79d383ff..790377cc 100644
--- a/cmd/hpkg/test/test.py
+++ b/cmd/hpkg/test/test.py
@@ -92,11 +92,11 @@ machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
collect_state_ui("app_wayland")
check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
# Verify acl on XDG_RUNTIME_DIR:
-print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
+print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002"))
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR:
-machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
+machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002")
# Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False)
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)
}
})
}