Serve a module from a runtime on its own account instead of switching users, keep bus words from bundles, and never give up a channel's work (hq ADR 0259 revision)
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
mesh/delivery-group group feat/asks-answered-on-any-channel stopped: a member was stopped
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
mesh/delivery-group group feat/asks-answered-on-any-channel stopped: a member was stopped
This commit is contained in:
@@ -181,12 +181,6 @@ func Start(module, entry string, env []string, mesh Bus, logf func(string, ...an
|
||||
start := func() (*child, error) {
|
||||
cmd := exec.Command(entry)
|
||||
cmd.Env = env
|
||||
// **A module that names an account of its own runs as it** (novox/hq ADR 0259 §8): its secrets and
|
||||
// state are that account's, and the operator's account — which every agent runs as — reaches
|
||||
// neither. Never root, never the operator's.
|
||||
if err := runAs(cmd, env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -310,7 +304,7 @@ func Start(module, entry string, env []string, mesh Bus, logf func(string, ...an
|
||||
}
|
||||
if err != nil {
|
||||
_ = c.write(map[string]any{"jsonrpc": "2.0", "id": m.ID,
|
||||
"error": map[string]any{"code": -32000, "message": err.Error()}})
|
||||
"error": map[string]any{"code": errorCode(err), "message": err.Error()}})
|
||||
return
|
||||
}
|
||||
_ = c.write(map[string]any{"jsonrpc": "2.0", "id": m.ID, "result": result})
|
||||
@@ -599,3 +593,17 @@ func (c *child) deathReason() error {
|
||||
}
|
||||
return errors.New("the bundle exited")
|
||||
}
|
||||
|
||||
// CodeChanged is the error code a bundle is answered a lost compare-and-set with (mesh-sdk's
|
||||
// stdio.CodeChanged): told by its code, never by its words (novox/hq ADR 0259).
|
||||
const CodeChanged = -32010
|
||||
|
||||
// errorCode is the code an error is answered to a bundle with: a lost compare-and-set's own, else the
|
||||
// general one.
|
||||
func errorCode(err error) int {
|
||||
var coded interface{ ErrorCode() int }
|
||||
if errors.As(err, &coded) {
|
||||
return coded.ErrorCode()
|
||||
}
|
||||
return -32000
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package launch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// RunAs is the word in a bundle's environment naming the account it runs as (novox/hq ADR 0259 §8), and
|
||||
// OperatorAccount the word naming the operator's account on this machine, which the runtime is given.
|
||||
const (
|
||||
RunAs = "MESH_RUN_AS"
|
||||
OperatorAccount = "MESH_OPERATOR_ACCOUNT"
|
||||
)
|
||||
|
||||
// lookupUser is user.Lookup, a variable so a test can name accounts this machine does not have.
|
||||
var lookupUser = user.Lookup
|
||||
|
||||
func word(env []string, name string) string {
|
||||
for _, kv := range env {
|
||||
if k, v, ok := strings.Cut(kv, "="); ok && k == name {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// runAs starts cmd as the account its environment names, with that account's home, or leaves it as the
|
||||
// runtime's own when none is named. It refuses root and the operator's account: a module asks for an
|
||||
// account of its own to keep what it holds from the agents, and every agent runs as the operator.
|
||||
func runAs(cmd *exec.Cmd, env []string) error {
|
||||
name := word(env, RunAs)
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
if operator := word(env, OperatorAccount); operator != "" && name == operator {
|
||||
return fmt.Errorf("%s names the operator's account %s; a module runs as an account of its own, never "+
|
||||
"the one every agent runs as (novox/hq ADR 0259)", RunAs, name)
|
||||
}
|
||||
u, err := lookupUser(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s names the account %s, which this machine does not have: the module makes it with "+
|
||||
"a user resource (novox/hq ADR 0259): %w", RunAs, name, err)
|
||||
}
|
||||
uid, err := strconv.ParseUint(u.Uid, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the account %s has no usable uid %q", name, u.Uid)
|
||||
}
|
||||
gid, err := strconv.ParseUint(u.Gid, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the account %s has no usable gid %q", name, u.Gid)
|
||||
}
|
||||
if uid == 0 || name == "root" {
|
||||
return fmt.Errorf("%s names root; a module that asks for an account of its own is not given root", RunAs)
|
||||
}
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}}
|
||||
var kept []string
|
||||
for _, kv := range cmd.Env {
|
||||
if !strings.HasPrefix(kv, "HOME=") && !strings.HasPrefix(kv, "USER=") && !strings.HasPrefix(kv, "LOGNAME=") {
|
||||
kept = append(kept, kv)
|
||||
}
|
||||
}
|
||||
cmd.Env = append(kept, "HOME="+u.HomeDir, "USER="+name, "LOGNAME="+name)
|
||||
return nil
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package launch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// novox/hq ADR 0259 §8: a module naming an account of its own runs as it, never as root or the operator.
|
||||
func TestABundleRunsAsTheAccountItNamesAndNeverRootOrTheOperator(t *testing.T) {
|
||||
was := lookupUser
|
||||
t.Cleanup(func() { lookupUser = was })
|
||||
lookupUser = func(name string) (*user.User, error) {
|
||||
switch name {
|
||||
case "telegram":
|
||||
return &user.User{Username: "telegram", Uid: "961", Gid: "961", HomeDir: "/var/lib/telegram"}, nil
|
||||
case "root":
|
||||
return &user.User{Username: "root", Uid: "0", Gid: "0", HomeDir: "/root"}, nil
|
||||
case "toor":
|
||||
return &user.User{Username: "toor", Uid: "0", Gid: "0", HomeDir: "/root"}, nil
|
||||
}
|
||||
return nil, errors.New("unknown user")
|
||||
}
|
||||
cmd := exec.Command("/bin/true")
|
||||
cmd.Env = []string{"HOME=/root", "PATH=/usr/bin"}
|
||||
if err := runAs(cmd, []string{RunAs + "=telegram", OperatorAccount + "=jo"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c := cmd.SysProcAttr.Credential; c == nil || c.Uid != 961 || c.Gid != 961 {
|
||||
t.Fatalf("not started as telegram: %+v", cmd.SysProcAttr)
|
||||
}
|
||||
if env := strings.Join(cmd.Env, " "); !strings.Contains(env, "HOME=/var/lib/telegram") || strings.Contains(env, "HOME=/root") {
|
||||
t.Fatalf("the account's home is not its own: %s", env)
|
||||
}
|
||||
for name, want := range map[string]string{"jo": "operator's account", "root": "names root", "toor": "names root",
|
||||
"nobody-here": "does not have"} {
|
||||
err := runAs(exec.Command("/bin/true"), []string{RunAs + "=" + name, OperatorAccount + "=jo"})
|
||||
if err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("%s: %v, want a refusal saying %q", name, err, want)
|
||||
}
|
||||
}
|
||||
plain := exec.Command("/bin/true")
|
||||
if err := runAs(plain, nil); err != nil || plain.SysProcAttr != nil {
|
||||
t.Error("a bundle naming no account was changed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user