From 0e82b87cfaf4bbe1a6ecb12fca2d2a081e5d90f3 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Wed, 18 Dec 2024 16:47:48 -0600 Subject: [PATCH] cpud: initial darwin support Destigmatize darwin cpud by enabling it without filesystem mounts or private namespace. Later can add nfs mounts, binds, and chroot in. Signed-off-by: Eric Van Hensbergen --- cmds/cpud/init_darwin.go | 81 ++++++++++++++++++++++++ cmds/cpud/main_darwin.go | 123 +++++++++++++++++++++++++++++++++++- mount/mount_darwin.go | 130 +++++++++++++++++++++++++++++++++++++++ server/server_other.go | 76 ++++++++++------------- session/session_other.go | 8 +-- 5 files changed, 368 insertions(+), 50 deletions(-) create mode 100644 cmds/cpud/init_darwin.go create mode 100644 mount/mount_darwin.go diff --git a/cmds/cpud/init_darwin.go b/cmds/cpud/init_darwin.go new file mode 100644 index 00000000..fc1086c8 --- /dev/null +++ b/cmds/cpud/init_darwin.go @@ -0,0 +1,81 @@ +// Copyright 2018-2019 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This is init code for the case that cpu finds itself as pid 1. +// This is duplicative of the real init, but we're implementing it +// as a duplicate so we can get some idea of: +// what an init package should have +// what an init interface should have +// So we take a bit of duplication now to better understand these +// things. We also assume for now this is a busybox environment. +// It is unusual (I guess?) for cpu to be an init in anything else. +// So far, the case for an init pkg is not as strong as I thought +// it might be. +package main + +import ( + "log" + "runtime" + "syscall" + "time" + + "github.com/u-root/u-root/pkg/libinit" +) + +func cpuSetup() error { + // The process reaper runs from here, and needs to run + // as PID 1. + runtime.LockOSThread() + log.Printf(` + + #### ##### # # ## + # # # # # # ## + # # # # # ## + # ##### # # ## + # # # # # + #### # #### ## +`) + //libinit.SetEnv() + //libinit.CreateRootfs() + libinit.NetInit() + // Wait for orphans, forever. + // Since there is no way of knowning when we are + // done for good, our work here is never done. + // A complication is that for long periods of time, there + // may be no orphans.In that case, sleep for one second, + // and try again. This background load is hardly enough + // to matter. And, in general, it will happen by definition + // when there is nothing to wait for, i.e. there is nothing + // on the node to be upset about. + // Were this ever to be a concern, an option is to kick off + // a process that will never exit, such that wait4 will always + // block and always return when any child process exits. + go func() { + var numReaped int + for { + var ( + s syscall.WaitStatus + r syscall.Rusage + ) + p, err := syscall.Wait4(-1, &s, 0, &r) + // Once per second, Wait 4 returns if there's nothing + // else to do. + if err != nil && err.Error() == "no child processes" { + continue + } + verbose("orphan reaper: returns with %v", p) + if p == -1 { + verbose("Nothing to wait for, %d wait for so far", numReaped) + time.Sleep(time.Second) + } + if err != nil { + log.Printf("CPUD: a process exited with %v, status %v, rusage %v, err %v", p, s, r, err) + } + numReaped++ + } + }() + + runtime.UnlockOSThread() + return nil +} diff --git a/cmds/cpud/main_darwin.go b/cmds/cpud/main_darwin.go index bd3b67d8..6560423a 100644 --- a/cmds/cpud/main_darwin.go +++ b/cmds/cpud/main_darwin.go @@ -5,9 +5,128 @@ package main import ( - "fmt" + "flag" + "log" + "os" + "time" + + // We use this ssh because it implements port redirection. + // It can not, however, unpack password-protected keys yet. + + "github.com/u-root/cpu/session" ) +var ( + // For the ssh server part + hostKeyFile = flag.String("hk", "" /*"/etc/ssh/ssh_host_rsa_key"*/, "file for host key") + pubKeyFile = flag.String("pk", "key.pub", "file for public key") + port = flag.String("sp", "17010", "cpu default port") + + debug = flag.Bool("d", false, "enable debug prints") + runAsInit = flag.Bool("init", false, "run as init (Debug only; normal test is if we are pid 1") + // v allows debug printing. + // Do not call it directly, call verbose instead. + v = func(string, ...interface{}) {} + remote = flag.Bool("remote", false, "indicates we are the remote side of the cpu session") + network = flag.String("net", "tcp", "network to use") + port9p = flag.String("port9p", "", "port9p # on remote machine for 9p mount") + klog = flag.Bool("klog", false, "Log cpud messages in kernel log, not stdout") + + // Some networks are not well behaved, and for them we implement registration. + registerAddr = flag.String("register", "", "address and port to register with after listen on cpu server port") + registerTO = flag.Duration("registerTO", time.Duration(5*time.Second), "time.Duration for Dial address for registering") + + // if we start up too quickly, mDNS won't work correctly. + // This sleep may be useful for other cases, so it is here, + // not specifically for mDNS uses. + sleepBeforeServing = flag.Duration("sleepBeforeServing", 0, "add a sleep before serving -- usually only needed if cpud runs as init with mDNS") + + pid1 bool +) + +func verbose(f string, a ...interface{}) { + if *remote { + v("CPUD(remote):"+f+"\r\n", a...) + } else { + v("CPUD:"+f, a...) + } +} + +// There are three distinct cases to cover. +// 1. running as init (indicated by pid == 1 OR -init=true switch +// 2. running as server. pid != 1 AND -remote=true AND -init=false +// 3. running as 'remote', i.e. the thing that starts a command for +// a client. Indicated by remote=true. +// +// case (3) overrides case 2 and 1. +// This has evolved over the years, and, likely, the init and remote +// switches ought to be renamed to 'role'. But so it goes. +// The rules on arguments are very strict now. In the remote case, +// os.Args[1] MUST be remote; no other invocation is accepted, because +// the args to remote and the args to server are different. +// This invocation requirement is known to the server package. func main() { - fmt.Println("No support on Darwin right now") + log.Println("WARNING: cpud support on Darwin currently limited (no private namespaces or remote fs)") + if len(os.Args) > 1 && (os.Args[1] == "-remote" || os.Args[1] == "-remote=true") { + *remote = true + } + + if *remote { + // remote has far fewer args. Since they are specified by the client, + // we want to limit the set of args it can set. + flag.CommandLine = flag.NewFlagSet("cpud-remote", flag.ExitOnError) + debug = flag.Bool("d", false, "enable debug prints") + remote = flag.Bool("remote", false, "indicates we are the remote side of the cpu session") + port9p = flag.String("port9p", "", "port9p # on remote machine for 9p mount") + + flag.Parse() + if *debug { + v = log.Printf + session.SetVerbose(verbose) + } + // If we are here, no matter what they may set, *remote must be true. + // sadly, cpud -d -remote=true -remote=false ... works. + *remote = true + } else { + flag.Parse() + // If we are here, no matter what they may set, *remote must be false. + *remote = false + if err := commonsetup(); err != nil { + log.Fatal(err) + } + } + pid := os.Getpid() + pid1 = pid == 1 + *runAsInit = *runAsInit || pid1 + verbose("Args %v pid %d *runasinit %v *remote %v env %v", os.Args, pid, *runAsInit, *remote, os.Environ()) + args := flag.Args() + if *remote { + verbose("args %q, port9p %v", args, *port9p) + + // This can happen if the user gets clever and + // invokes cpu with, e.g., nothing but switches. + if len(args) == 0 { + shell, ok := os.LookupEnv("SHELL") + if !ok { + log.Fatal("No arguments and $SHELL is not set") + } + args = []string{shell} + } + s := session.New(*port9p, args[0], args[1:]...) + if err := s.Run(); err != nil { + log.Fatalf("CPUD(remote): %v", err) + } + } else { + log.Printf("CPUD:PID(%d):running as a server (a.k.a. starter of cpud's for sessions)", pid) + if *runAsInit { + log.Printf("CPUD:also running as init") + if err := initsetup(); err != nil { + log.Fatal(err) + } + } + time.Sleep(*sleepBeforeServing) + if err := serve(os.Args[0]); err != nil { + log.Fatal(err) + } + } } diff --git a/mount/mount_darwin.go b/mount/mount_darwin.go new file mode 100644 index 00000000..601b1c46 --- /dev/null +++ b/mount/mount_darwin.go @@ -0,0 +1,130 @@ +// Copyright 2018-2022 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mount + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "path/filepath" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +func init() { + for k, v := range map[string]uintptr{ + "mnt_async": unix.MNT_ASYNC, + "mnt_automounted": unix.MNT_AUTOMOUNTED, + "mnt_cmdflags": unix.MNT_CMDFLAGS, + "mnt_exported": unix.MNT_EXPORTED, + "mnt_force": unix.MNT_FORCE, + "mnt_local": unix.MNT_LOCAL, + "mnt_multilabel": unix.MNT_MULTILABEL, + "mnt_noatime": unix.MNT_NOATIME, + "mnt_noexec": unix.MNT_NOEXEC, + "mnt_nosuid": unix.MNT_NOSUID, + "mnt_nowait": unix.MNT_NOWAIT, + "mnt_quota": unix.MNT_QUOTA, + "mnt_rdonly": unix.MNT_RDONLY, + "mnt_reload": unix.MNT_RELOAD, + "mnt_rootfs": unix.MNT_ROOTFS, + "mnt_snapshot": unix.MNT_SNAPSHOT, + "mnt_synchronous": unix.MNT_SYNCHRONOUS, + "mnt_union": unix.MNT_UNION, + "mnt_update": unix.MNT_UPDATE, + "mnt_visflagmask": unix.MNT_VISFLAGMASK, + "mnt_wait": unix.MNT_WAIT, + } { + convert[k] = v + } +} + +// iov returns an iovec for a string. +// there is no official package, and it is simple +// enough, that we just create it here. +func iovstring(val string) syscall.Iovec { + s := val + "\x00" + vec := syscall.Iovec{Base: (*byte)(unsafe.Pointer(&[]byte(s)[0]))} + vec.SetLen(len(s)) + return vec +} + +// Mount takes a full fstab as a string and does whatever mounts are needed. +// It ignores comment lines, and lines with less than 6 fields. In principal, +// Mount should be able to do a full remount with the contents of /proc/mounts. +// Mount makes a best-case effort to mount the mounts passed in a +// string formatted to the fstab standard. Callers should not die on +// a returned error, but be left in a situation in which further +// diagnostics are possible. i.e., follow the "Boots not Bricks" +// principle. +// Freebsd has very different ways of working than linux, so +// we shell out to mount for now. +func Mount(fstab string) error { + f, err := ioutil.TempFile("", "cpu") + if err != nil { + return err + } + defer f.Close() + + if _, err := io.WriteString(f, fstab); err != nil { + return err + } + + //if o, err := exec.Command("mount", "-a", "-F", f.Name()).CombinedOutput(); err != nil { + // return fmt.Errorf("mount -F %q:%s:%w", f.Name(), string(o), err) + //} + + return nil + var lineno int + s := bufio.NewScanner(strings.NewReader(fstab)) + for s.Scan() { + lineno++ + l := s.Text() + if strings.HasPrefix(l, "#") { + continue + } + f := strings.Fields(l) + // fstab is historical, pretty free format. + // Users may have dropped a random fstab in and we need + // to be forgiving. + // The last two fields no longer have any meaning or use. + if len(f) < 6 { + continue + } + + // fstab fields: + // /dev/disk/by-uuid/c0d2b09d-5330-4d08-a787-6e0e95592bf3 /boot ext4 defaults 0 0 + // what to mount, where to mount, fstype, options + // We do need NOT to set MS_PRIVATE, since we've done a successful unshare. + // This note is here in case someone gets confused in the future. + // Setting MS_PRIVATE will get an EINVAL. + dev, where, fstype, opts := f[0], f[1], f[2], f[3] + + // surprise! It turns out that correct behavior from mount is to follow symlinks + // on where and device and use that. That's why /bin -> /usr/bin gets mounted + // correctly. + if w, err := filepath.EvalSymlinks(where); err == nil { + where = w + } + if w, err := filepath.EvalSymlinks(dev); err == nil { + dev = w + } + + // The man page implies that the Linux kernel handles flags of "defaults" + // we do no further manipulation of opts. + flags, data := parse(opts) + + fmt.Println("WARNING: DARWIN can't mount", dev, where, fstype, flags, data) + err = nil + //if _, e := mount.Mount(dev, where, fstype, data, flags); e != nil { + // err = errors.Join(err, fmt.Errorf("Mount(%q, %q, %q, %q=>(%#x, %q)): %w", dev, where, fstype, opts, flags, data, e)) + //} + } + return err +} diff --git a/server/server_other.go b/server/server_other.go index da9a3be0..5ca6eb48 100644 --- a/server/server_other.go +++ b/server/server_other.go @@ -8,56 +8,48 @@ package server import ( - "fmt" - "net" "os" "os/exec" ) -// Namespace assembles a NameSpace for this cpud, iff CPU_NAMESPACE -// is set. -// CPU_NAMESPACE can be the empty string. -// It also requires that CPU_NONCE exist. -func (s *Session) Namespace() (error, error) { - var warning error - // Get the nonce and remove it from the environment. - // N.B. We do not save the nonce in the cpu struct. - nonce := os.Getenv("CPUNONCE") - os.Unsetenv("CPUNONCE") - verbose("namespace is %q", s.binds) - - // Connect to the socket, return the nonce. - a := net.JoinHostPort("localhost", s.port9p) - verbose("Dial %v", a) - so, err := net.Dial("tcp", a) - if err != nil { - return warning, fmt.Errorf("CPUD:Dial 9p port: %v", err) - } - verbose("Connected: write nonce %s\n", nonce) - if _, err := fmt.Fprintf(so, "%s", nonce); err != nil { - return warning, fmt.Errorf("CPUD:Write nonce: %v", err) +// cpud can run in one of three modes +// o init +// o daemon started by init +// o manager of one cpu session. +// It is *critical* that the session manager have a private +// name space, else every cpu session will interfere with every +// other session's mounts. What's the best way to ensure the manager +// gets a private name space, and ensure that no improper use +// of this package will result in NOT having a private name space? +// How do we make the logic failsafe? +// +// It turns out there is no harm in always privatizing the name space, +// no matter the mode. +// So in this init function, we do not parse flags (that breaks tests; +// flag.Parse() in init is a no-no), and then, no +// matter what, privatize the namespace, and mount a private /tmp/cpu if we +// are not pid1. As for pid1 tasks, they should be specified by the cpud +// itself, not this package. This code merely ensures correction operation +// of cpud no matter what mode it is invoked in. +func init() { + // placeholder. It's not clear we ever want to do this. We used to create + // a root file system here, but that should be up to the server. The files + // might magically exist, b/c of initrd; or be automagically mounted via + // some other mechanism. + if os.Getpid() == 1 { + verbose("PID 1") } - verbose("Wrote the nonce") - // Zero it. I realize I am not a crypto person. - // improvements welcome. - copy([]byte(nonce), make([]byte, len(nonce))) - - return warning, fmt.Errorf("CPUD: cannot use 9p connection yet") -} - -func osMounts() error { - return nil -} - -func logopts() { } func command(n string, args ...string) *exec.Cmd { cmd := exec.Command(n, args...) + // N.B.: in the go runtime, after not long ago, CLONE_NEWNS in the Unshareflags + // also does two things: an unshare, and a remount of / to unshare mounts. + // see d8ed449d8eae5b39ffe227ef7f56785e978dd5e2 in the go tree for a discussion. + // This meant we could remove ALL calls of unshare and mount from cpud. + // Fun fact: I wrote that fix years ago, and then forgot to remove + // the support code from cpu. Oops. + // DARWIN DOESN'T HAVE NAMESPACE SO SKIP IT + //cmd.SysProcAttr = &syscall.SysProcAttr{Unshareflags: syscall.CLONE_NEWNS} return cmd } - -// runSetup performs kernel-specific operations for starting a Session. -func runSetup() error { - return nil -} diff --git a/session/session_other.go b/session/session_other.go index 155e8365..7ce855b2 100644 --- a/session/session_other.go +++ b/session/session_other.go @@ -6,17 +6,13 @@ package session -import ( - "fmt" - "os" -) - // Namespace assembles a NameSpace for this cpud, iff CPU_NAMESPACE // is set. // CPU_NAMESPACE can be the empty string. // It also requires that CPU_NONCE exist. func (s *Session) Namespace() error { - return fmt.Errorf("CPUD: 9p mounts are only valid on Linux:%w", os.ErrNotExist) + //return fmt.Errorf("CPUD: 9p mounts are only valid on Linux:%w", os.ErrNotExist) + return nil } func osMounts() error {