Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m55s
Test / Hakurei (push) Successful in 4m56s
Test / Sandbox (race detector) (push) Successful in 6m7s
Test / Hakurei (race detector) (push) Successful in 7m20s
Test / ShareFS (push) Successful in 7m35s
Test / Flake checks (push) Successful in 1m7s
This is theoretically susceptible to the PID race, but such an event is statistically impossible, and not exploitable in the intended use case, regardless. Signed-off-by: Ophestra <cat@gensokyo.uk>
410 lines
12 KiB
Go
410 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"strconv"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"hakurei.app/check"
|
|
"hakurei.app/command"
|
|
"hakurei.app/ext"
|
|
"hakurei.app/fhs"
|
|
"hakurei.app/hst"
|
|
"hakurei.app/internal/dbus"
|
|
"hakurei.app/internal/env"
|
|
"hakurei.app/internal/info"
|
|
"hakurei.app/internal/outcome"
|
|
"hakurei.app/message"
|
|
)
|
|
|
|
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
|
|
// if it is not nil, or the original value if it is.
|
|
func optionalErrorUnwrap(err error) error {
|
|
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
|
|
return underlyingErr
|
|
}
|
|
return err
|
|
}
|
|
|
|
var errSuccess = errors.New("success")
|
|
|
|
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
|
var (
|
|
flagVerbose bool
|
|
flagInsecure bool
|
|
flagJSON bool
|
|
)
|
|
c := command.New(out, log.Printf, "hakurei", func([]string) error {
|
|
msg.SwapVerbose(flagVerbose)
|
|
|
|
if early.yamaLSM != nil {
|
|
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
|
|
// not fatal
|
|
}
|
|
|
|
if early.dumpable != nil {
|
|
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
|
|
// not fatal
|
|
}
|
|
|
|
return nil
|
|
}).
|
|
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
|
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
|
|
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
|
|
|
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
|
|
|
|
{
|
|
var (
|
|
flagIdentifierFile int
|
|
)
|
|
c.NewCommand("run", "Load and start container from configuration file", func(args []string) error {
|
|
if len(args) < 1 {
|
|
log.Fatal("run requires at least 1 argument")
|
|
}
|
|
|
|
config := tryPath(msg, args[0])
|
|
if config != nil && config.Container != nil {
|
|
config.Container.Args = append(config.Container.Args, args[1:]...)
|
|
}
|
|
|
|
var flags int
|
|
if flagInsecure {
|
|
flags |= hst.VAllowInsecure
|
|
}
|
|
|
|
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
|
|
panic("unreachable")
|
|
}).
|
|
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
|
|
"Write identifier of current instance to fd after successful startup")
|
|
}
|
|
|
|
{
|
|
var (
|
|
flagDBusConfigSession string
|
|
flagDBusConfigSystem string
|
|
flagDBusMpris bool
|
|
flagDBusVerbose bool
|
|
|
|
flagID string
|
|
flagIdentity int
|
|
flagGroups command.RepeatableFlag
|
|
flagHomeDir string
|
|
flagUserName string
|
|
|
|
flagSchedPolicy string
|
|
flagSchedPriority int
|
|
|
|
flagPrivateRuntime, flagPrivateTmpdir bool
|
|
|
|
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
|
|
)
|
|
|
|
c.NewCommand("exec", "Configure and start a permissive container", func(args []string) error {
|
|
if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
|
|
log.Fatalf("identity %d out of range", flagIdentity)
|
|
}
|
|
|
|
// resolve home/username from os when flag is unset
|
|
var (
|
|
passwd *user.User
|
|
passwdOnce sync.Once
|
|
passwdFunc = func() {
|
|
us := strconv.Itoa(hst.ToUser(new(outcome.Hsu).MustID(msg), flagIdentity))
|
|
if u, err := user.LookupId(us); err != nil {
|
|
msg.Verbosef("cannot look up uid %s", us)
|
|
passwd = &user.User{
|
|
Uid: us,
|
|
Gid: us,
|
|
Username: "chronos",
|
|
Name: "Hakurei Permissive Default",
|
|
HomeDir: fhs.VarEmpty,
|
|
}
|
|
} else {
|
|
passwd = u
|
|
}
|
|
}
|
|
)
|
|
|
|
// paths are identical, resolve inner shell and program path
|
|
shell := fhs.AbsRoot.Append("bin", "sh")
|
|
if a, err := check.NewAbs(os.Getenv("SHELL")); err == nil {
|
|
shell = a
|
|
}
|
|
progPath := shell
|
|
if len(args) > 0 {
|
|
if p, err := exec.LookPath(args[0]); err != nil {
|
|
log.Fatal(optionalErrorUnwrap(err))
|
|
return err
|
|
} else if progPath, err = check.NewAbs(p); err != nil {
|
|
log.Fatal(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
var et hst.Enablements
|
|
if flagWayland {
|
|
et |= hst.EWayland
|
|
}
|
|
if flagX11 {
|
|
et |= hst.EX11
|
|
}
|
|
if flagDBus {
|
|
et |= hst.EDBus
|
|
}
|
|
if flagPipeWire || flagPulse {
|
|
et |= hst.EPipeWire
|
|
}
|
|
|
|
config := hst.Config{
|
|
ID: flagID,
|
|
Identity: flagIdentity,
|
|
Groups: flagGroups,
|
|
Enablements: &et,
|
|
|
|
Container: &hst.ContainerConfig{
|
|
Filesystem: []hst.FilesystemConfigJSON{
|
|
// autoroot, includes the home directory
|
|
{FilesystemConfig: &hst.FSBind{
|
|
Target: fhs.AbsRoot,
|
|
Source: fhs.AbsRoot,
|
|
Write: true,
|
|
Special: true,
|
|
}},
|
|
},
|
|
|
|
Username: flagUserName,
|
|
Shell: shell,
|
|
|
|
Path: progPath,
|
|
Args: args,
|
|
|
|
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
|
|
},
|
|
}
|
|
|
|
if err := config.SchedPolicy.UnmarshalText(
|
|
[]byte(flagSchedPolicy),
|
|
); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
config.SchedPriority = ext.Int(flagSchedPriority)
|
|
|
|
// bind GPU stuff
|
|
if et&(hst.EX11|hst.EWayland) != 0 {
|
|
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
|
Source: fhs.AbsDev.Append("dri"),
|
|
Device: true,
|
|
Optional: true,
|
|
}})
|
|
}
|
|
|
|
config.Container.Filesystem = append(config.Container.Filesystem,
|
|
// opportunistically bind kvm
|
|
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
|
Source: fhs.AbsDev.Append("kvm"),
|
|
Device: true,
|
|
Optional: true,
|
|
}},
|
|
|
|
// do autoetc last
|
|
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
|
Target: fhs.AbsEtc,
|
|
Source: fhs.AbsEtc,
|
|
Special: true,
|
|
}},
|
|
)
|
|
|
|
if config.Container.Username == "chronos" {
|
|
passwdOnce.Do(passwdFunc)
|
|
config.Container.Username = passwd.Username
|
|
}
|
|
|
|
{
|
|
homeDir := flagHomeDir
|
|
if homeDir == "os" {
|
|
passwdOnce.Do(passwdFunc)
|
|
homeDir = passwd.HomeDir
|
|
}
|
|
if a, err := check.NewAbs(homeDir); err != nil {
|
|
log.Fatal(err)
|
|
return err
|
|
} else {
|
|
config.Container.Home = a
|
|
}
|
|
}
|
|
|
|
if !flagPrivateRuntime {
|
|
config.Container.Flags |= hst.FShareRuntime
|
|
}
|
|
if !flagPrivateTmpdir {
|
|
config.Container.Flags |= hst.FShareTmpdir
|
|
}
|
|
|
|
// parse D-Bus config file from flags if applicable
|
|
if flagDBus {
|
|
if flagDBusConfigSession == "builtin" {
|
|
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
|
|
} else {
|
|
if f, err := os.Open(flagDBusConfigSession); err != nil {
|
|
log.Fatal(err)
|
|
} else {
|
|
decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
|
|
if err = f.Close(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// system bus proxy is optional
|
|
if flagDBusConfigSystem != "nil" {
|
|
if f, err := os.Open(flagDBusConfigSystem); err != nil {
|
|
log.Fatal(err)
|
|
} else {
|
|
decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
|
|
if err = f.Close(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// override log from configuration
|
|
if flagDBusVerbose {
|
|
if config.SessionBus != nil {
|
|
config.SessionBus.Log = true
|
|
}
|
|
if config.SystemBus != nil {
|
|
config.SystemBus.Log = true
|
|
}
|
|
}
|
|
}
|
|
|
|
outcome.Main(ctx, msg, &config, 0, -1)
|
|
panic("unreachable")
|
|
}).
|
|
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
|
"Path to session bus proxy config file, or \"builtin\" for defaults").
|
|
Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
|
"Path to system bus proxy config file, or \"nil\" to disable").
|
|
Flag(&flagDBusMpris, "mpris", command.BoolFlag(false),
|
|
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
|
Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false),
|
|
"Force buffered logging in the D-Bus proxy").
|
|
Flag(&flagID, "id", command.StringFlag(""),
|
|
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
|
|
Flag(&flagIdentity, "a", command.IntFlag(0),
|
|
"Application identity").
|
|
Flag(nil, "g", &flagGroups,
|
|
"Groups inherited by all container processes").
|
|
Flag(&flagHomeDir, "d", command.StringFlag("os"),
|
|
"Container home directory").
|
|
Flag(&flagUserName, "u", command.StringFlag("chronos"),
|
|
"Passwd user name within sandbox").
|
|
Flag(&flagSchedPolicy, "policy", command.StringFlag(""),
|
|
"Scheduling policy to set for the container").
|
|
Flag(&flagSchedPriority, "priority", command.IntFlag(0),
|
|
"Scheduling priority to set for the container").
|
|
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
|
|
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
|
|
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
|
|
"Do not share TMPDIR between containers under the same identity").
|
|
Flag(&flagWayland, "wayland", command.BoolFlag(false),
|
|
"Enable connection to Wayland via security-context-v1").
|
|
Flag(&flagX11, "X", command.BoolFlag(false),
|
|
"Enable direct connection to X11").
|
|
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
|
"Enable proxied connection to D-Bus").
|
|
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
|
|
"Enable connection to PipeWire via SecurityContext").
|
|
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
|
"Enable PulseAudio compatibility daemon")
|
|
}
|
|
|
|
{
|
|
var (
|
|
flagShort bool
|
|
flagNoStore bool
|
|
)
|
|
c.NewCommand("show", "Show live or local instance configuration", func(args []string) error {
|
|
switch len(args) {
|
|
case 0: // system
|
|
printShowSystem(os.Stdout, flagShort, flagJSON)
|
|
|
|
case 1: // instance
|
|
name := args[0]
|
|
|
|
var (
|
|
config *hst.Config
|
|
entry *hst.State
|
|
)
|
|
if !flagNoStore {
|
|
var sc hst.Paths
|
|
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
|
|
entry = tryIdentifier(msg, name, outcome.NewStore(&sc))
|
|
}
|
|
|
|
if entry == nil {
|
|
config = tryPath(msg, name)
|
|
} else {
|
|
config = entry.Config
|
|
}
|
|
|
|
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
|
|
os.Exit(1)
|
|
}
|
|
|
|
default:
|
|
log.Fatal("show requires 1 argument")
|
|
}
|
|
return errSuccess
|
|
}).
|
|
Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information").
|
|
Flag(&flagNoStore, "no-store", command.BoolFlag(false), "Do not attempt to match from active instances")
|
|
}
|
|
|
|
c.NewCommand("kill", "Terminate an active instance", func(args []string) error {
|
|
if len(args) != 1 {
|
|
log.Fatal("kill requires 1 argument")
|
|
}
|
|
|
|
var sc hst.Paths
|
|
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
|
|
entry := tryIdentifier(msg, args[0], outcome.NewStore(&sc))
|
|
if entry == nil {
|
|
log.Fatalf("%q does not match any active instance", args[0])
|
|
}
|
|
if err := syscall.Kill(entry.PID, syscall.SIGTERM); err != nil {
|
|
log.Fatalf("cannot terminate %s: %v", entry.ID.String(), err)
|
|
}
|
|
return errSuccess
|
|
})
|
|
|
|
{
|
|
var flagShort bool
|
|
c.NewCommand("ps", "List active instances", func(args []string) error {
|
|
var sc hst.Paths
|
|
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
|
|
printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON)
|
|
return errSuccess
|
|
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
|
|
}
|
|
|
|
c.Command("version", "Display version information", func(args []string) error { fmt.Println(info.Version()); return errSuccess })
|
|
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
|
|
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
|
|
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
|
|
|
|
return c
|
|
}
|