aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-09-15 02:30:47 +0900
committerOphestra <cat@gensokyo.uk>2025-09-15 02:30:47 +0900
commita2a291791c6447528d3b1abc1ec57c7b213730b6 (patch)
tree4d4515976d798b461d8df358c272364c5cf0b2e1
parent8690419c2d577a27efe3cfcafc39c05b7646926b (diff)
internal/sys: separate hsu uid cache
This begins the effort of the removal of the sys package. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--cmd/hakurei/command.go19
-rw-r--r--internal/app/errors.go18
-rw-r--r--internal/sys/hsu.go81
-rw-r--r--internal/sys/std.go99
-rw-r--r--test/test.py6
5 files changed, 124 insertions, 99 deletions
diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go
index c3b9d85d..eaefb893 100644
--- a/cmd/hakurei/command.go
+++ b/cmd/hakurei/command.go
@@ -2,6 +2,7 @@ package main
import (
"context"
+ "errors"
"fmt"
"io"
"log"
@@ -20,6 +21,7 @@ import (
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
+ "hakurei.app/internal/sys"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
@@ -272,20 +274,19 @@ func runApp(config *hst.Config) {
// fatal prints the error message according to [container.GetErrorMessage], or fallback
// prepended to err if an error message is not available, followed by a call to [os.Exit](1).
func fatal(fallback string, err error) {
- m, ok := container.GetErrorMessage(err)
- if !ok {
- log.Fatal(fallback, err)
- return
- }
-
// this indicates the error message has already reached stderr, outside the current process's control;
- // this is only reached when hsu fails for any reason, as we do not want a second error message following hsu
- // TODO(ophestra): handle the hsu error here instead of relying on a magic string
- if m == "\x00" {
+ // this is only reached when hsu fails for any reason, as a second error message following hsu is confusing
+ if errors.Is(err, sys.ErrHsuAccess) {
hlog.Verbose("*"+fallback, err)
os.Exit(1)
return
}
+ m, ok := container.GetErrorMessage(err)
+ if !ok {
+ log.Fatalln(fallback, err)
+ return
+ }
+
log.Fatal(m)
}
diff --git a/internal/app/errors.go b/internal/app/errors.go
index 566103d4..f7fa2b1c 100644
--- a/internal/app/errors.go
+++ b/internal/app/errors.go
@@ -7,6 +7,7 @@ import (
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
+ "hakurei.app/internal/sys"
)
// PrintRunStateErr prints an error message via [log] if runErr is not nil, and returns an appropriate exit code.
@@ -98,13 +99,20 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) {
// TODO(ophestra): this duplicates code in cmd/hakurei/command.go, keep this up to date until removal
func printMessageError(fallback string, err error) {
- if m, ok := container.GetErrorMessage(err); ok {
- if m != "\x00" {
- log.Print(m)
- }
- } else {
+ // this indicates the error message has already reached stderr, outside the current process's control;
+ // this is only reached when hsu fails for any reason, as a second error message following hsu is confusing
+ if errors.Is(err, sys.ErrHsuAccess) {
+ hlog.Verbose("*"+fallback, err)
+ return
+ }
+
+ m, ok := container.GetErrorMessage(err)
+ if !ok {
log.Println(fallback, err)
+ return
}
+
+ log.Print(m)
}
// StateStoreError is returned for a failed state save.
diff --git a/internal/sys/hsu.go b/internal/sys/hsu.go
new file mode 100644
index 00000000..84bb0731
--- /dev/null
+++ b/internal/sys/hsu.go
@@ -0,0 +1,81 @@
+package sys
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "strconv"
+ "sync"
+
+ "hakurei.app/container"
+ "hakurei.app/hst"
+ "hakurei.app/internal"
+)
+
+// Hsu caches responses from cmd/hsu.
+type Hsu struct {
+ uidOnce sync.Once
+ uidCopy map[int]struct {
+ uid int
+ err error
+ }
+ uidMu sync.RWMutex
+}
+
+var ErrHsuAccess = errors.New("current user is not in the hsurc file")
+
+func (h *Hsu) Uid(identity int) (int, error) {
+ h.uidOnce.Do(func() {
+ h.uidCopy = make(map[int]struct {
+ uid int
+ err error
+ })
+ })
+
+ {
+ h.uidMu.RLock()
+ u, ok := h.uidCopy[identity]
+ h.uidMu.RUnlock()
+ if ok {
+ return u.uid, u.err
+ }
+ }
+
+ h.uidMu.Lock()
+ defer h.uidMu.Unlock()
+
+ u := struct {
+ uid int
+ err error
+ }{}
+ defer func() { h.uidCopy[identity] = u }()
+
+ u.uid = -1
+ hsuPath := internal.MustHsuPath()
+
+ cmd := exec.Command(hsuPath)
+ cmd.Path = hsuPath
+ cmd.Stderr = os.Stderr // pass through fatal messages
+ cmd.Env = []string{"HAKUREI_APP_ID=" + strconv.Itoa(identity)}
+ cmd.Dir = container.FHSRoot
+ var (
+ p []byte
+ exitError *exec.ExitError
+ )
+
+ const step = "obtain uid from hsu"
+ if p, u.err = cmd.Output(); u.err == nil {
+ u.uid, u.err = strconv.Atoi(string(p))
+ if u.err != nil {
+ u.err = &hst.AppError{Step: step, Err: u.err, Msg: "invalid uid string from hsu"}
+ }
+ } else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
+ // hsu prints an error message in this case
+ u.err = &hst.AppError{Step: step, Err: ErrHsuAccess}
+ } else if os.IsNotExist(u.err) {
+ u.err = &hst.AppError{Step: step, Err: os.ErrNotExist,
+ Msg: fmt.Sprintf("the setuid helper is missing: %s", hsuPath)}
+ }
+ return u.uid, u.err
+}
diff --git a/internal/sys/std.go b/internal/sys/std.go
index c46bdd58..24b36fa2 100644
--- a/internal/sys/std.go
+++ b/internal/sys/std.go
@@ -2,16 +2,13 @@ package sys
import (
"errors"
- "fmt"
"io/fs"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
- "strconv"
"sync"
- "syscall"
"hakurei.app/container"
"hakurei.app/hst"
@@ -23,13 +20,7 @@ import (
type Std struct {
paths hst.Paths
pathsOnce sync.Once
-
- uidOnce sync.Once
- uidCopy map[int]struct {
- uid int
- err error
- }
- uidMu sync.RWMutex
+ Hsu
}
func (s *Std) Getuid() int { return os.Getuid() }
@@ -53,81 +44,27 @@ func (s *Std) Paths() hst.Paths {
s.pathsOnce.Do(func() {
if userid, err := GetUserID(s); err != nil {
// TODO(ophestra): this duplicates code in cmd/hakurei/command.go, keep this up to date until removal
- if m, ok := container.GetErrorMessage(err); ok {
- if m != "\x00" {
- log.Print(m)
- }
- } else {
- log.Println("cannot obtain user id from hsu:", err)
- }
hlog.BeforeExit()
- s.Exit(1)
+ const fallback = "cannot obtain user id from hsu:"
+
+ // this indicates the error message has already reached stderr, outside the current process's control;
+ // this is only reached when hsu fails for any reason, as a second error message following hsu is confusing
+ if errors.Is(err, ErrHsuAccess) {
+ hlog.Verbose("*"+fallback, err)
+ os.Exit(1)
+ return
+ }
+
+ m, ok := container.GetErrorMessage(err)
+ if !ok {
+ log.Fatalln(fallback, err)
+ return
+ }
+
+ log.Fatal(m)
} else {
CopyPaths(s, &s.paths, userid)
}
})
return s.paths
}
-
-// this is a temporary placeholder until this package is removed
-type wrappedError struct {
- Err error
- Msg string
-}
-
-func (e *wrappedError) Error() string { return e.Err.Error() }
-func (e *wrappedError) Unwrap() error { return e.Err }
-func (e *wrappedError) Message() string { return e.Msg }
-
-func (s *Std) Uid(identity int) (int, error) {
- s.uidOnce.Do(func() {
- s.uidCopy = make(map[int]struct {
- uid int
- err error
- })
- })
-
- {
- s.uidMu.RLock()
- u, ok := s.uidCopy[identity]
- s.uidMu.RUnlock()
- if ok {
- return u.uid, u.err
- }
- }
-
- s.uidMu.Lock()
- defer s.uidMu.Unlock()
-
- u := struct {
- uid int
- err error
- }{}
- defer func() { s.uidCopy[identity] = u }()
-
- u.uid = -1
- hsuPath := internal.MustHsuPath()
-
- cmd := exec.Command(hsuPath)
- cmd.Path = hsuPath
- cmd.Stderr = os.Stderr // pass through fatal messages
- cmd.Env = []string{"HAKUREI_APP_ID=" + strconv.Itoa(identity)}
- cmd.Dir = container.FHSRoot
- var (
- p []byte
- exitError *exec.ExitError
- )
-
- if p, u.err = cmd.Output(); u.err == nil {
- u.uid, u.err = strconv.Atoi(string(p))
- if u.err != nil {
- u.err = &wrappedError{u.err, "invalid uid string from hsu"}
- }
- } else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
- // hsu prints an error message in this case
- u.err = &wrappedError{syscall.EACCES, "\x00"} // this drops the message, handled in cmd/hakurei/command.go
- } else if os.IsNotExist(u.err) {
- u.err = &wrappedError{os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", hsuPath)}
- }
- return u.uid, u.err
-}
diff --git a/test/test.py b/test/test.py
index 4d31ceee..537b4b16 100644
--- a/test/test.py
+++ b/test/test.py
@@ -8,9 +8,7 @@ NODE_GROUPS = ["nodes", "floating_nodes"]
def swaymsg(command: str = "", succeed=True, type="command"):
assert command != "" or type != "command", "Must specify command or type"
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
- with machine.nested(
- f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
- ):
+ with machine.nested(f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)):
ret = (machine.succeed if succeed else machine.execute)(
f"su - alice -c {shell}"
)
@@ -102,7 +100,7 @@ print(machine.fail("sudo -u alice -i hsu"))
# Verify hsu fault behaviour:
if denyOutput != "hsu: uid 1001 is not in the hsurc file\n":
raise Exception(f"unexpected deny output:\n{denyOutput}")
-if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot obtain uid from setuid wrapper: permission denied\n":
+if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot obtain uid from setuid wrapper: current user is not in the hsurc file\n":
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
check_offset = 0