diff --git a/internal/shim/main.go b/internal/shim/main.go new file mode 100644 index 0000000..618f466 --- /dev/null +++ b/internal/shim/main.go @@ -0,0 +1,188 @@ +package shim + +import ( + "encoding/gob" + "errors" + "fmt" + "net" + "os" + "strconv" + "strings" + "syscall" + + "git.ophivana.moe/cat/fortify/helper" + "git.ophivana.moe/cat/fortify/helper/bwrap" + "git.ophivana.moe/cat/fortify/internal/verbose" +) + +// everything beyond this point runs as target user +// proceed with caution! + +func shim(socket string) { + verbose.Prefix = "fortify-shim:" + + // dial setup socket + var conn *net.UnixConn + if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socket, Net: "unix"}); err != nil { + fmt.Println("fortify-shim: cannot dial setup socket:", err) + os.Exit(1) + } else { + conn = c + } + + // decode payload gob stream + var payload Payload + if err := gob.NewDecoder(conn).Decode(&payload); err != nil { + fmt.Println("fortify-shim: cannot decode shim payload:", err) + os.Exit(1) + } else { + // sharing stdout with parent + // USE WITH CAUTION + verbose.Set(payload.Verbose) + } + + // receive wayland fd over socket + wfd := -1 + if payload.WL { + if fd, err := receiveWLfd(conn); err != nil { + fmt.Println("fortify-shim: cannot receive wayland fd:", err) + os.Exit(1) + } else { + wfd = fd + } + } + + // close setup socket + if err := conn.Close(); err != nil { + fmt.Println("fortify-shim: cannot close setup socket:", err) + // not fatal + } + + // resolve argv0 + var ( + argv0 string + argv = payload.Argv + ) + if len(argv) > 0 { + // looked up from $PATH by parent + argv0 = payload.Exec[1] + } else { + // no argv, look up shell instead + var ok bool + if argv0, ok = os.LookupEnv("SHELL"); !ok { + fmt.Println("fortify-shim: no command was specified and $SHELL was unset") + os.Exit(1) + } + + argv = []string{argv0} + } + + _ = conn.Close() + + conf := payload.Bwrap + if conf == nil { + verbose.Println("sandbox configuration not supplied, PROCEED WITH CAUTION") + conf = &bwrap.Config{ + Net: true, + UserNS: true, + Clearenv: true, + Procfs: []string{"/proc"}, + DevTmpfs: []string{"/dev"}, + Mqueue: []string{"/dev/mqueue"}, + DieWithParent: true, + } + + if d, err := os.ReadDir("/"); err != nil { + fmt.Println("fortify-shim: cannot readdir '/':", err) + } else { + conf.Bind = make([][2]string, 0, len(d)) + for _, ent := range d { + name := ent.Name() + switch name { + case "proc": + case "dev": + default: + p := "/" + name + conf.Bind = append(conf.Bind, [2]string{p, p}) + } + } + } + } + if conf.SetEnv == nil { + conf.SetEnv = make(map[string]string, len(payload.Env)) + } + + var extraFiles []*os.File + + // set environment passed by parent + for _, s := range payload.Env { + kv := strings.SplitN(s, "=", 2) + if len(kv) != 2 { + fmt.Println("fortify-shim: invalid environment string:", s) + } else { + conf.SetEnv[kv[0]] = kv[1] + } + } + + // pass wayland fd + if wfd != -1 { + if f := os.NewFile(uintptr(wfd), "wayland"); f != nil { + conf.SetEnv["WAYLAND_SOCKET"] = strconv.Itoa(3 + len(extraFiles)) + extraFiles = append(extraFiles, f) + } + } + + helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent + if b, err := helper.NewBwrap(conf, nil, argv0, func(_, _ int) []string { return argv[1:] }); err != nil { + fmt.Println("fortify-shim: malformed sandbox config:", err) + os.Exit(1) + } else { + cmd := b.Unwrap() + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + cmd.ExtraFiles = extraFiles + + if verbose.Get() { + verbose.Println("bwrap args:", conf.Args()) + } + + // run and pass through exit code + if err = b.Start(); err != nil { + fmt.Println("fortify-shim: cannot start target process:", err) + os.Exit(1) + } else if err = b.Wait(); err != nil { + verbose.Println("wait:", err) + } + if b.Unwrap().ProcessState != nil { + os.Exit(b.Unwrap().ProcessState.ExitCode()) + } else { + os.Exit(127) + } + } +} + +func receiveWLfd(conn *net.UnixConn) (int, error) { + oob := make([]byte, syscall.CmsgSpace(4)) // single fd + + if _, oobn, _, _, err := conn.ReadMsgUnix(nil, oob); err != nil { + return -1, err + } else if len(oob) != oobn { + return -1, errors.New("invalid message length") + } + + var msg syscall.SocketControlMessage + if messages, err := syscall.ParseSocketControlMessage(oob); err != nil { + return -1, err + } else if len(messages) != 1 { + return -1, errors.New("unexpected message count") + } else { + msg = messages[0] + } + + if fds, err := syscall.ParseUnixRights(&msg); err != nil { + return -1, err + } else if len(fds) != 1 { + return -1, errors.New("unexpected fd count") + } else { + return fds[0], nil + } +} diff --git a/internal/shim/parent.go b/internal/shim/parent.go new file mode 100644 index 0000000..7ffc1c4 --- /dev/null +++ b/internal/shim/parent.go @@ -0,0 +1,90 @@ +package shim + +import ( + "encoding/gob" + "errors" + "fmt" + "net" + "os" + "syscall" + + "git.ophivana.moe/cat/fortify/internal/verbose" +) + +// called in the parent process + +func ServeConfig(socket string, payload *Payload, wl string, done chan struct{}) (*net.UnixConn, error) { + var ws *net.UnixConn + if payload.WL { + if f, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: wl, Net: "unix"}); err != nil { + return nil, err + } else { + verbose.Println("connected to wayland at", wl) + ws = f + } + } + + if c, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"}); err != nil { + return nil, err + } else { + verbose.Println("configuring shim on socket", socket) + if err = os.Chmod(socket, 0777); err != nil { + fmt.Println("fortify: cannot change permissions of shim setup socket:", err) + } + + go func() { + var conn *net.UnixConn + if conn, err = c.AcceptUnix(); err != nil { + fmt.Println("fortify: cannot accept connection from shim:", err) + } else { + if err = gob.NewEncoder(conn).Encode(*payload); err != nil { + fmt.Println("fortify: cannot stream shim payload:", err) + return + } + + if payload.WL { + // get raw connection + var rc syscall.RawConn + if rc, err = ws.SyscallConn(); err != nil { + fmt.Println("fortify: cannot obtain raw wayland connection:", err) + return + } else { + go func() { + // pass wayland socket fd + if err = rc.Control(func(fd uintptr) { + if _, _, err = conn.WriteMsgUnix(nil, syscall.UnixRights(int(fd)), nil); err != nil { + fmt.Println("fortify: cannot pass wayland connection to shim:", err) + return + } + _ = conn.Close() + + // block until shim exits + <-done + verbose.Println("releasing wayland connection") + }); err != nil { + fmt.Println("fortify: cannot obtain wayland connection fd:", err) + } + }() + } + } else { + _ = conn.Close() + } + } + if err = c.Close(); err != nil { + fmt.Println("fortify: cannot close shim socket:", err) + } + if err = os.Remove(socket); err != nil && !errors.Is(err, os.ErrNotExist) { + fmt.Println("fortify: cannot remove dangling shim socket:", err) + } + }() + return ws, nil + } +} + +// Try runs shim and stops execution if FORTIFY_SHIM is set. +func Try() { + if s, ok := os.LookupEnv(EnvShim); ok { + shim(s) + } + panic("unreachable") +} diff --git a/internal/shim/payload.go b/internal/shim/payload.go new file mode 100644 index 0000000..6202bfe --- /dev/null +++ b/internal/shim/payload.go @@ -0,0 +1,23 @@ +package shim + +import ( + "git.ophivana.moe/cat/fortify/helper/bwrap" +) + +const EnvShim = "FORTIFY_SHIM" + +type Payload struct { + // child full argv + Argv []string + // env variables passed through to bwrap + Env []string + // bwrap, target full exec path + Exec [2]string + // bwrap config, nil for permissive + Bwrap *bwrap.Config + // whether to pas wayland fd + WL bool + + // verbosity pass through + Verbose bool +}