Skip to content

Commit e1f15c0

Browse files
committed
feat: load gated services on demand
1 parent 433e397 commit e1f15c0

12 files changed

Lines changed: 715 additions & 31 deletions

File tree

internal/control/sdb.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package control
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"os"
8+
"os/user"
9+
"path/filepath"
10+
"strconv"
11+
"syscall"
12+
"time"
13+
14+
"github.com/singularityos-lab/atom/internal/core"
15+
)
16+
17+
const (
18+
DefaultSDBSocket = "/run/atom/sdb-control.sock"
19+
defaultSDBUnit = "sdbd.service"
20+
defaultSDBMarker = "/var/lib/sinty-sdb/enabled"
21+
defaultDevGate = "/etc/atom/dev.enabled"
22+
defaultBrokerExe = "/usr/bin/ush-broker"
23+
defaultSDBGroup = "singularity-session"
24+
defaultMinimumUID = 1000
25+
)
26+
27+
// SDBConfig defines the single gated service exposed to the desktop broker.
28+
// The dedicated socket never accepts arbitrary unit names or init operations.
29+
type SDBConfig struct {
30+
Unit string
31+
Marker string
32+
DevelopmentGate string
33+
BrokerExecutable string
34+
GroupID int
35+
MinimumUID uint32
36+
}
37+
38+
// DefaultSDBConfig resolves the session group used by the installed image.
39+
func DefaultSDBConfig() (SDBConfig, error) {
40+
group, err := user.LookupGroup(defaultSDBGroup)
41+
if err != nil {
42+
return SDBConfig{}, fmt.Errorf("look up %s group: %w", defaultSDBGroup, err)
43+
}
44+
gid, err := strconv.Atoi(group.Gid)
45+
if err != nil {
46+
return SDBConfig{}, fmt.Errorf("parse %s gid: %w", defaultSDBGroup, err)
47+
}
48+
return SDBConfig{
49+
Unit: defaultSDBUnit,
50+
Marker: defaultSDBMarker,
51+
DevelopmentGate: defaultDevGate,
52+
BrokerExecutable: defaultBrokerExe,
53+
GroupID: gid,
54+
MinimumUID: defaultMinimumUID,
55+
}, nil
56+
}
57+
58+
// ListenSDB creates the narrow control socket used by ush-broker.
59+
func ListenSDB(path string, m *core.Manager, cfg SDBConfig) (*SDBServer, error) {
60+
if cfg.Unit == "" || cfg.Marker == "" || cfg.DevelopmentGate == "" ||
61+
cfg.BrokerExecutable == "" || cfg.GroupID < 0 || cfg.MinimumUID == 0 {
62+
return nil, fmt.Errorf("incomplete sdb control configuration")
63+
}
64+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
65+
return nil, err
66+
}
67+
_ = os.Remove(path)
68+
ln, err := net.Listen("unix", path)
69+
if err != nil {
70+
return nil, err
71+
}
72+
if err := os.Chown(path, 0, cfg.GroupID); err != nil {
73+
_ = ln.Close()
74+
_ = os.Remove(path)
75+
return nil, fmt.Errorf("set sdb control group: %w", err)
76+
}
77+
if err := os.Chmod(path, 0o660); err != nil {
78+
_ = ln.Close()
79+
_ = os.Remove(path)
80+
return nil, fmt.Errorf("set sdb control mode: %w", err)
81+
}
82+
return &SDBServer{ln: ln, m: m, cfg: cfg}, nil
83+
}
84+
85+
// SDBServer serves only status, enable and disable for sdbd.service.
86+
type SDBServer struct {
87+
ln net.Listener
88+
m *core.Manager
89+
cfg SDBConfig
90+
}
91+
92+
func (s *SDBServer) Serve() {
93+
for {
94+
conn, err := s.ln.Accept()
95+
if err != nil {
96+
return
97+
}
98+
go s.handle(conn)
99+
}
100+
}
101+
102+
func (s *SDBServer) Close() error { return s.ln.Close() }
103+
104+
func (s *SDBServer) handle(conn net.Conn) {
105+
defer conn.Close()
106+
defer func() { _ = recover() }()
107+
_ = conn.SetDeadline(time.Now().Add(70 * time.Second))
108+
109+
var req Request
110+
if err := readFrame(conn, &req); err != nil {
111+
return
112+
}
113+
if !s.authorize(conn) {
114+
_ = writeFrame(conn, Reply{Error: "permission denied"})
115+
return
116+
}
117+
118+
switch req.Cmd {
119+
case "sdb-status":
120+
_ = writeFrame(conn, Reply{OK: true, State: s.m.State(s.cfg.Unit)})
121+
case "sdb-enable":
122+
_ = writeFrame(conn, s.setEnabled(true))
123+
case "sdb-disable":
124+
_ = writeFrame(conn, s.setEnabled(false))
125+
default:
126+
_ = writeFrame(conn, Reply{Error: "unknown command: " + req.Cmd})
127+
}
128+
}
129+
130+
func (s *SDBServer) setEnabled(on bool) Reply {
131+
if on {
132+
if _, err := os.Stat(s.cfg.DevelopmentGate); err != nil {
133+
return Reply{Error: "development mode is not enabled"}
134+
}
135+
if err := os.MkdirAll(filepath.Dir(s.cfg.Marker), 0o755); err != nil {
136+
return Reply{Error: err.Error()}
137+
}
138+
f, err := os.OpenFile(s.cfg.Marker, os.O_CREATE|os.O_WRONLY, 0o644)
139+
if err != nil {
140+
return Reply{Error: err.Error()}
141+
}
142+
if err := f.Close(); err != nil {
143+
return Reply{Error: err.Error()}
144+
}
145+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
146+
defer cancel()
147+
if err := s.m.StartUnit(ctx, s.cfg.Unit); err != nil {
148+
_ = os.Remove(s.cfg.Marker)
149+
return Reply{Error: err.Error()}
150+
}
151+
return Reply{OK: true, State: s.m.State(s.cfg.Unit)}
152+
}
153+
154+
if err := os.Remove(s.cfg.Marker); err != nil && !os.IsNotExist(err) {
155+
return Reply{Error: err.Error()}
156+
}
157+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
158+
defer cancel()
159+
if err := s.m.StopUnit(ctx, s.cfg.Unit); err != nil {
160+
return Reply{Error: err.Error()}
161+
}
162+
return Reply{OK: true, State: s.m.State(s.cfg.Unit)}
163+
}
164+
165+
func (s *SDBServer) authorize(conn net.Conn) bool {
166+
uc, ok := conn.(*net.UnixConn)
167+
if !ok {
168+
return false
169+
}
170+
raw, err := uc.SyscallConn()
171+
if err != nil {
172+
return false
173+
}
174+
var cred *syscall.Ucred
175+
var inner error
176+
if err := raw.Control(func(fd uintptr) {
177+
cred, inner = syscall.GetsockoptUcred(int(fd), syscall.SOL_SOCKET, syscall.SO_PEERCRED)
178+
}); err != nil || inner != nil || cred == nil {
179+
return false
180+
}
181+
return s.authorizePeer(cred)
182+
}
183+
184+
func (s *SDBServer) authorizePeer(cred *syscall.Ucred) bool {
185+
if cred.Uid == 0 {
186+
return true
187+
}
188+
if cred.Uid < s.cfg.MinimumUID {
189+
return false
190+
}
191+
peerExe, err := os.Stat(filepath.Join("/proc", strconv.Itoa(int(cred.Pid)), "exe"))
192+
if err != nil {
193+
return false
194+
}
195+
trustedExe, err := os.Stat(s.cfg.BrokerExecutable)
196+
if err != nil {
197+
return false
198+
}
199+
return os.SameFile(peerExe, trustedExe)
200+
}

internal/control/sdb_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package control
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"syscall"
7+
"testing"
8+
9+
"github.com/singularityos-lab/atom/internal/core"
10+
"github.com/singularityos-lab/atom/internal/unit"
11+
)
12+
13+
func TestSDBControlOwnsGateAndService(t *testing.T) {
14+
if os.Geteuid() != 0 {
15+
t.Skip("the production socket owner check requires root")
16+
}
17+
exe, err := os.Executable()
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
dir := t.TempDir()
22+
units := filepath.Join(dir, "units")
23+
if err := os.Mkdir(units, 0o755); err != nil {
24+
t.Fatal(err)
25+
}
26+
writeUnit(t, units, "base.target", "[Unit]\n")
27+
writeUnit(t, units, "sdbd.service", "[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=/bin/true\n")
28+
m, err := core.Build(&unit.Loader{Paths: []string{units}}, "base.target")
29+
if err != nil {
30+
t.Fatal(err)
31+
}
32+
33+
devGate := filepath.Join(dir, "dev.enabled")
34+
marker := filepath.Join(dir, "state", "enabled")
35+
sock := filepath.Join(dir, "sdb.sock")
36+
srv, err := ListenSDB(sock, m, SDBConfig{
37+
Unit: "sdbd.service",
38+
Marker: marker,
39+
DevelopmentGate: devGate,
40+
BrokerExecutable: exe,
41+
GroupID: os.Getgid(),
42+
MinimumUID: 1000,
43+
})
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
go srv.Serve()
48+
defer srv.Close()
49+
50+
rep, err := Send(sock, Request{Cmd: "sdb-enable"})
51+
if err != nil {
52+
t.Fatal(err)
53+
}
54+
if rep.OK {
55+
t.Fatal("enable succeeded without development mode")
56+
}
57+
if err := os.WriteFile(devGate, nil, 0o644); err != nil {
58+
t.Fatal(err)
59+
}
60+
rep, err = Send(sock, Request{Cmd: "sdb-enable"})
61+
if err != nil || !rep.OK || rep.State != "active" {
62+
t.Fatalf("enable: err=%v rep=%+v", err, rep)
63+
}
64+
if _, err := os.Stat(marker); err != nil {
65+
t.Fatalf("marker: %v", err)
66+
}
67+
68+
rep, err = Send(sock, Request{Cmd: "sdb-disable"})
69+
if err != nil || !rep.OK || rep.State != "inactive" {
70+
t.Fatalf("disable: err=%v rep=%+v", err, rep)
71+
}
72+
if _, err := os.Stat(marker); !os.IsNotExist(err) {
73+
t.Fatalf("marker remains after disable: %v", err)
74+
}
75+
}
76+
77+
func TestSDBControlChecksPeerIdentity(t *testing.T) {
78+
exe, err := os.Executable()
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
srv := &SDBServer{cfg: SDBConfig{
83+
BrokerExecutable: exe,
84+
MinimumUID: 1000,
85+
}}
86+
if !srv.authorizePeer(&syscall.Ucred{Uid: 0, Pid: int32(os.Getpid())}) {
87+
t.Fatal("root peer refused")
88+
}
89+
if srv.authorizePeer(&syscall.Ucred{Uid: 102, Pid: int32(os.Getpid())}) {
90+
t.Fatal("system user accepted")
91+
}
92+
if !srv.authorizePeer(&syscall.Ucred{Uid: 1000, Pid: int32(os.Getpid())}) {
93+
t.Fatal("trusted executable refused")
94+
}
95+
96+
copyPath := filepath.Join(t.TempDir(), "ush-broker")
97+
data, err := os.ReadFile(exe)
98+
if err != nil {
99+
t.Fatal(err)
100+
}
101+
if err := os.WriteFile(copyPath, data, 0o755); err != nil {
102+
t.Fatal(err)
103+
}
104+
srv.cfg.BrokerExecutable = copyPath
105+
if srv.authorizePeer(&syscall.Ucred{Uid: 1000, Pid: int32(os.Getpid())}) {
106+
t.Fatal("different inode accepted")
107+
}
108+
}

internal/core/lazyload_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package core
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/singularityos-lab/atom/internal/unit"
10+
)
11+
12+
// A unit deliberately shipped outside every target.wants (so it never starts at
13+
// boot) must still be startable on demand: StartUnit loads it lazily. Before
14+
// this, StartUnit only knew the boot graph, so an explicit start of such a unit
15+
// returned "unknown unit" and the unit could never run. This matters for a gated
16+
// service like the debug bridge, which must be startable by a request but must
17+
// not start at boot.
18+
func TestStartUnitLazyLoadsOffGraph(t *testing.T) {
19+
if _, err := os.Stat("/bin/true"); err != nil {
20+
t.Skip("no /bin/true")
21+
}
22+
dir := t.TempDir()
23+
marker := filepath.Join(dir, "gate")
24+
if err := os.WriteFile(marker, []byte("x"), 0o644); err != nil {
25+
t.Fatal(err)
26+
}
27+
// The off-graph unit, gated on a marker that IS present so it may run.
28+
write(t, filepath.Join(dir, "ondemand.service"),
29+
"[Unit]\nConditionPathExists="+marker+"\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=/bin/true\n")
30+
// A boot target that does NOT want ondemand.service.
31+
write(t, filepath.Join(dir, "boot.target"), "[Unit]\n")
32+
33+
loader := &unit.Loader{Paths: []string{dir}}
34+
m, err := Build(loader, "boot.target")
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
if _, ok := m.Get("ondemand.service"); ok {
39+
t.Fatal("ondemand.service should not be in the boot graph")
40+
}
41+
if st := m.State("ondemand.service"); st != "inactive" {
42+
t.Fatalf("off-graph unit state = %q, want inactive", st)
43+
}
44+
if st := m.State("missing.service"); st != "unknown" {
45+
t.Fatalf("missing unit state = %q, want unknown", st)
46+
}
47+
if err := m.StartUnit(context.Background(), "ondemand.service"); err != nil {
48+
t.Fatalf("StartUnit lazy load: %v", err)
49+
}
50+
if st := m.State("ondemand.service"); st != "active" {
51+
t.Errorf("off-graph unit state = %q, want active", st)
52+
}
53+
54+
// And an off-graph unit whose condition is UNMET is loaded but skipped, not
55+
// started: the two fixes compose.
56+
absent := filepath.Join(dir, "nope")
57+
write(t, filepath.Join(dir, "gated.service"),
58+
"[Unit]\nConditionPathExists="+absent+"\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=/bin/true\n")
59+
if err := m.StartUnit(context.Background(), "gated.service"); err != nil {
60+
t.Fatalf("StartUnit gated (skip is clean): %v", err)
61+
}
62+
if st := m.State("gated.service"); st == "active" {
63+
t.Errorf("off-graph unit with unmet condition = %q, want not active", st)
64+
}
65+
}

0 commit comments

Comments
 (0)