Say the login session a mesh-cli line comes from, and pin the SDK at go/v0.1.9 (hq ADR 0272)
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

A service running as the operator — the tool runner, a unit of the
account's user manager — shares the operator's user id, and was as much the
terminal on the control-node as a person typing. The engine now reads the
asking process's cgroup and sends its login session's scope
(session-<id>.scope under the account's own user-<uid>.slice), or nothing;
the controller makes nothing else the terminal. The SDK is pinned at its
tag now that go/v0.1.9 exists.
This commit is contained in:
jochen
2026-10-09 13:40:17 +02:00
parent 1ac7741c63
commit 4a5dcb0209
6 changed files with 82 additions and 5 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ func AskTheController(ctx context.Context, ask AskFunc, asked Asked) cli.Answer
return cli.NotKnown(call, fmt.Sprintf("no answer within the time mesh-cli waits (%s)", cli.AskWithin))
case <-time.After(followEvery):
}
body, _ := json.Marshal(Asked{Follow: call, Account: asked.Account, UID: asked.UID})
body, _ := json.Marshal(Asked{Follow: call, Account: asked.Account, UID: asked.UID, Session: asked.Session})
if reply, err = ask(ctx, body); err == nil {
break
}
+36 -1
View File
@@ -19,6 +19,7 @@ import (
"os/user"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@@ -35,6 +36,11 @@ type Asked struct {
UID uint32 `json:"uid"`
// Follow is a call a line runs as, asked about again by the account that asked it (AskTheController).
Follow string `json:"follow,omitempty"`
// Session is the login session the asking process runs in — its systemd scope, `session-<id>.scope` under the
// account's own slice — as the kernel's cgroup says it; empty for anything else: a system service, a user unit,
// a process another account's session started. Only a login session is a person at a terminal (novox/hq ADR
// 0272): the controller makes nothing else the terminal.
Session string `json:"session,omitempty"`
}
// Forward asks the controller and follows the line to what came of it (AskTheController): what it printed, that
@@ -84,6 +90,35 @@ func (s *slots) give(uid uint32) {
// test can say what the kernel would; it is the only source of the account.
var peerOf = peerCredentials
// sessionOf reads the login session a process runs in, from the kernel. A variable so a test can say.
var sessionOf = func(pid int32, uid uint32) string {
raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pid))
if err != nil {
return ""
}
return loginSessionIn(string(raw), uid)
}
// loginSessionIn is the login session scope a process's cgroup names, for its own account: the unified hierarchy's
// line, `0::/user.slice/user-<uid>.slice/session-<id>.scope`, and nothing below or beside it. Anything else is no
// login session — a service under system.slice, a unit under the account's user manager (user@<uid>.service), a
// scope of another account's.
func loginSessionIn(cgroup string, uid uint32) string {
for _, line := range strings.Split(cgroup, "\n") {
path, ok := strings.CutPrefix(line, "0::")
if !ok {
continue
}
prefix := fmt.Sprintf("/user.slice/user-%d.slice/", uid)
scope, ok := strings.CutPrefix(path, prefix)
if !ok || strings.Contains(scope, "/") || !strings.HasPrefix(scope, "session-") || !strings.HasSuffix(scope, ".scope") {
return ""
}
return scope
}
return ""
}
// sameNamespaces refuses a peer whose mount or user namespace is not the engine's. A variable so a test can say.
var sameNamespaces = namespacesOf
@@ -204,5 +239,5 @@ func answer(ctx context.Context, conn *net.UnixConn, held *slots, forward Forwar
_ = conn.SetDeadline(time.Now().Add(cli.AskWithin))
asking, cancel := context.WithTimeout(ctx, cli.AskWithin-30*time.Second)
defer cancel()
reply(forward(asking, Asked{Line: req.Line, Account: account, UID: uid}))
reply(forward(asking, Asked{Line: req.Line, Account: account, UID: uid, Session: sessionOf(pid, uid)}))
}
+37
View File
@@ -126,6 +126,11 @@ func TestTheRequestToTheControllerKeepsItsFieldNames(t *testing.T) {
if err != nil {
t.Fatal(err)
}
body, _ = json.Marshal(Asked{Line: []string{"status"}, Account: "a", UID: 1, Session: "session-3.scope"})
if got := keysOf(t, body); got != "account line session uid" {
t.Fatalf("the request's fields are %q", got)
}
body, _ = json.Marshal(Asked{Line: []string{"status"}, Account: "a", UID: 1})
if got := keysOf(t, body); got != "account line uid" {
t.Fatalf("the request's fields are %q", got)
}
@@ -216,3 +221,35 @@ func TestAPeerInAnotherNamespaceIsRefused(t *testing.T) {
t.Fatalf("a peer in another namespace was asked (%v): %+v", asked, a)
}
}
// **The session a line comes from is the kernel's word, and only a login session counts** (review of ADR 0272): a
// service running as the operator — the tool runner, a user unit — is not a person at a terminal. The engine reads
// the peer's cgroup and sends the login session's scope, or nothing; the controller makes no other terminal.
func TestTheLoginSessionAskingIsReadFromTheKernel(t *testing.T) {
for _, c := range []struct{ cgroup, session string }{
{"0::/user.slice/user-1000.slice/session-3.scope\n", "session-3.scope"},
{"0::/user.slice/user-1000.slice/session-c1.scope\n", "session-c1.scope"},
{"0::/user.slice/user-1000.slice/user@1000.service/app.slice/tmux.scope\n", ""},
{"0::/system.slice/node-tools.service\n", ""},
{"0::/user.slice/user-1001.slice/session-3.scope\n", ""}, // another account's session
{"12:pids:/user.slice\n0::/user.slice/user-1000.slice/session-9.scope/sub\n", ""},
{"", ""},
} {
if got := loginSessionIn(c.cgroup, 1000); got != c.session {
t.Errorf("%q read as %q, want %q", c.cgroup, got, c.session)
}
}
// And it is sent: the test process's own, whatever it is, as the kernel says it.
var got Asked
socket := serving(t, func(_ context.Context, asked Asked) cli.Answer {
got = asked
return cli.Answer{}
})
if _, err := cli.Ask(context.Background(), socket, []string{"status"}); err != nil {
t.Fatal(err)
}
raw, _ := os.ReadFile("/proc/self/cgroup")
if want := loginSessionIn(string(raw), uint32(os.Getuid())); got.Session != want {
t.Fatalf("sent session %q, the kernel says %q", got.Session, want)
}
}