diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/app/app.go | 2 | ||||
| -rw-r--r-- | internal/app/app_nixos_test.go | 66 | ||||
| -rw-r--r-- | internal/app/config.go | 20 | ||||
| -rw-r--r-- | internal/app/launch.machinectl.go | 57 | ||||
| -rw-r--r-- | internal/app/launch.sudo.go | 30 | ||||
| -rw-r--r-- | internal/app/seal.go | 114 | ||||
| -rw-r--r-- | internal/app/share.display.go | 2 | ||||
| -rw-r--r-- | internal/app/share.system.go | 24 | ||||
| -rw-r--r-- | internal/app/start.go | 55 | ||||
| -rw-r--r-- | internal/app/system.go | 24 | ||||
| -rw-r--r-- | internal/fmsg/defer.go | 3 | ||||
| -rw-r--r-- | internal/linux/interface.go | 8 | ||||
| -rw-r--r-- | internal/linux/std.go | 85 | ||||
| -rw-r--r-- | internal/path.go | 1 | ||||
| -rw-r--r-- | internal/state/print.go | 12 | ||||
| -rw-r--r-- | internal/state/state.go | 2 |
16 files changed, 227 insertions, 278 deletions
diff --git a/internal/app/app.go b/internal/app/app.go index 28564ebf..3260deab 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -53,7 +53,7 @@ func (a *app) String() string { } if a.seal != nil { - return "(sealed fortified app as uid " + a.seal.sys.user.Uid + ")" + return "(sealed fortified app as uid " + a.seal.sys.user.us + ")" } return "(unsealed fortified app)" diff --git a/internal/app/app_nixos_test.go b/internal/app/app_nixos_test.go index a154486d..4a350caf 100644 --- a/internal/app/app_nixos_test.go +++ b/internal/app/app_nixos_test.go @@ -19,9 +19,12 @@ var testCasesNixos = []sealTestCase{ { "nixos permissive defaults no enablements", new(stubNixOS), &app.Config{ - User: "chronos", Command: make([]string, 0), - Method: "sudo", + Confinement: app.ConfinementConfig{ + AppID: 0, + Username: "chronos", + Home: "/home/chronos", + }, }, app.ID{ 0x4a, 0x45, 0x0b, 0x65, @@ -29,11 +32,11 @@ var testCasesNixos = []sealTestCase{ 0xbd, 0x01, 0x78, 0x0e, 0xb9, 0xa6, 0x07, 0xac, }, - system.New(150). - Ensure("/tmp/fortify.1971", 0701). - Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0701). + system.New(1000000). + Ensure("/tmp/fortify.1971", 0711). + Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711). Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute). - Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute). + Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute). Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute). Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute). @@ -43,6 +46,7 @@ var testCasesNixos = []sealTestCase{ Net: true, UserNS: true, Clearenv: true, + Chdir: "/home/chronos", SetEnv: map[string]string{ "HOME": "/home/chronos", "SHELL": "/run/current-system/sw/bin/zsh", @@ -182,10 +186,11 @@ var testCasesNixos = []sealTestCase{ Symlink("/fortify/etc/zprofile", "/etc/zprofile"). Symlink("/fortify/etc/zshenv", "/etc/zshenv"). Symlink("/fortify/etc/zshrc", "/etc/zshrc"). - Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true). + Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true). Tmpfs("/tmp/fortify.1971", 1048576). Tmpfs("/run/user", 1048576). Tmpfs("/run/user/65534", 8388608). + Bind("/home/chronos", "/home/chronos", false, true). Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd"). Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group"). Tmpfs("/var/run/nscd", 8192), @@ -194,9 +199,12 @@ var testCasesNixos = []sealTestCase{ "nixos permissive defaults chromium", new(stubNixOS), &app.Config{ ID: "org.chromium.Chromium", - User: "chronos", Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "}, Confinement: app.ConfinementConfig{ + AppID: 9, + Groups: []string{"video"}, + Username: "chronos", + Home: "/home/chronos", SessionBus: &dbus.Config{ Talk: []string{ "org.freedesktop.Notifications", @@ -230,7 +238,6 @@ var testCasesNixos = []sealTestCase{ }, Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(), }, - Method: "systemd", }, app.ID{ 0xeb, 0xf0, 0x83, 0xd1, @@ -238,11 +245,11 @@ var testCasesNixos = []sealTestCase{ 0x82, 0xd4, 0x13, 0x36, 0x9b, 0x64, 0xce, 0x7c, }, - system.New(150). - Ensure("/tmp/fortify.1971", 0701). - Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0701). + system.New(1000009). + Ensure("/tmp/fortify.1971", 0711). + Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711). Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute). - Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute). + Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute). Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute). Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute). @@ -287,6 +294,7 @@ var testCasesNixos = []sealTestCase{ (&bwrap.Config{ Net: true, UserNS: true, + Chdir: "/home/chronos", Clearenv: true, SetEnv: map[string]string{ "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus", @@ -434,10 +442,11 @@ var testCasesNixos = []sealTestCase{ Symlink("/fortify/etc/zprofile", "/etc/zprofile"). Symlink("/fortify/etc/zshenv", "/etc/zshenv"). Symlink("/fortify/etc/zshrc", "/etc/zshrc"). - Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true). + Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true). Tmpfs("/tmp/fortify.1971", 1048576). Tmpfs("/run/user", 1048576). Tmpfs("/run/user/65534", 8388608). + Bind("/home/chronos", "/home/chronos", false, true). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group"). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0"). @@ -504,23 +513,12 @@ func (s *stubNixOS) Executable() (string, error) { return "/home/ophestra/.nix-profile/bin/fortify", nil } -func (s *stubNixOS) Lookup(username string) (*user.User, error) { - if s.usernameErr != nil { - if err, ok := s.usernameErr[username]; ok { - return nil, err - } - } - - switch username { - case "chronos": - return &user.User{ - Uid: "150", - Gid: "101", - Username: "chronos", - HomeDir: "/home/chronos", - }, nil +func (s *stubNixOS) LookupGroup(name string) (*user.Group, error) { + switch name { + case "video": + return &user.Group{Gid: "26", Name: "video"}, nil default: - return nil, user.UnknownUserError(username) + return nil, user.UnknownGroupError(name) } } @@ -586,10 +584,6 @@ func (s *stubNixOS) Stdout() io.Writer { panic("requested stdout") } -func (s *stubNixOS) FshimPath() string { - return "/nix/store/00000000000000000000000000000000-fortify-0.0.10/bin/.fshim" -} - func (s *stubNixOS) Paths() linux.Paths { return linux.Paths{ SharePath: "/tmp/fortify.1971", @@ -598,6 +592,10 @@ func (s *stubNixOS) Paths() linux.Paths { } } +func (s *stubNixOS) Uid(aid int) (int, error) { + return 1000000 + 0*10000 + aid, nil +} + func (s *stubNixOS) SdBooted() bool { return true } diff --git a/internal/app/config.go b/internal/app/config.go index 96c51ce7..dd45b023 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -15,12 +15,8 @@ const fTmp = "/fortify" type Config struct { // D-Bus application ID ID string `json:"id"` - // username of the target user to switch to - User string `json:"user"` // value passed through to the child process as its argv Command []string `json:"command"` - // string representation of the child's launch method - Method string `json:"method"` // child confinement configuration Confinement ConfinementConfig `json:"confinement"` @@ -28,6 +24,14 @@ type Config struct { // ConfinementConfig defines fortified child's confinement type ConfinementConfig struct { + // numerical application id, determines uid in the init namespace + AppID int `json:"app_id"` + // list of supplementary groups to inherit + Groups []string `json:"groups"` + // passwd username in the sandbox, defaults to chronos + Username string `json:"username,omitempty"` + // home directory in sandbox + Home string `json:"home"` // bwrap sandbox confinement configuration Sandbox *SandboxConfig `json:"sandbox"` @@ -169,8 +173,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) { // Template returns a fully populated instance of Config. func Template() *Config { return &Config{ - ID: "org.chromium.Chromium", - User: "chronos", + ID: "org.chromium.Chromium", Command: []string{ "chromium", "--ignore-gpu-blocklist", @@ -178,8 +181,11 @@ func Template() *Config { "--enable-features=UseOzonePlatform", "--ozone-platform=wayland", }, - Method: "sudo", Confinement: ConfinementConfig{ + AppID: 9, + Groups: []string{"video"}, + Username: "chronos", + Home: "/var/lib/persist/home/org.chromium.Chromium", Sandbox: &SandboxConfig{ Hostname: "localhost", UserNS: true, diff --git a/internal/app/launch.machinectl.go b/internal/app/launch.machinectl.go deleted file mode 100644 index 3c196b84..00000000 --- a/internal/app/launch.machinectl.go +++ /dev/null @@ -1,57 +0,0 @@ -package app - -import ( - "strings" - - "git.ophivana.moe/security/fortify/internal/fmsg" -) - -func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) { - args = make([]string, 0, 9+len(a.seal.sys.bwrap.SetEnv)) - - // shell --uid=$USER - args = append(args, "shell", "--uid="+a.seal.sys.user.Username) - - // --quiet - if !fmsg.Verbose() { - args = append(args, "--quiet") - } - - // environ - envQ := make([]string, 0, len(a.seal.sys.bwrap.SetEnv)+1) - for k, v := range a.seal.sys.bwrap.SetEnv { - envQ = append(envQ, "-E"+k+"="+v) - } - // add shim payload to environment for shim path - envQ = append(envQ, "-E"+shimEnv) - args = append(args, envQ...) - - // -- .host - args = append(args, "--", ".host") - - // /bin/sh -c - if sh, err := a.os.LookPath("sh"); err != nil { - // hardcode /bin/sh path since it exists more often than not - args = append(args, "/bin/sh", "-c") - } else { - args = append(args, sh, "-c") - } - - // build inner command expression ran as target user - innerCommand := strings.Builder{} - - // apply custom environment variables to activation environment - innerCommand.WriteString("dbus-update-activation-environment --systemd") - for k := range a.seal.sys.bwrap.SetEnv { - innerCommand.WriteString(" " + k) - } - innerCommand.WriteString("; ") - - // launch fortify shim - innerCommand.WriteString("exec " + a.os.FshimPath()) - - // append inner command - args = append(args, innerCommand.String()) - - return -} diff --git a/internal/app/launch.sudo.go b/internal/app/launch.sudo.go deleted file mode 100644 index dbb49318..00000000 --- a/internal/app/launch.sudo.go +++ /dev/null @@ -1,30 +0,0 @@ -package app - -import ( - "git.ophivana.moe/security/fortify/internal/fmsg" -) - -const ( - sudoAskPass = "SUDO_ASKPASS" -) - -func (a *app) commandBuilderSudo(shimEnv string) (args []string) { - args = make([]string, 0, 8) - - // -Hiu $USER - args = append(args, "-Hiu", a.seal.sys.user.Username) - - // -A? - if _, ok := a.os.LookupEnv(sudoAskPass); ok { - fmsg.VPrintln(sudoAskPass, "set, adding askpass flag") - args = append(args, "-A") - } - - // shim payload - args = append(args, shimEnv) - - // -- $@ - args = append(args, "--", a.os.FshimPath()) - - return -} diff --git a/internal/app/seal.go b/internal/app/seal.go index 0846a7f8..f01061af 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -2,8 +2,8 @@ package app import ( "errors" + "fmt" "io/fs" - "os/user" "path" "strconv" @@ -15,24 +15,10 @@ import ( "git.ophivana.moe/security/fortify/internal/system" ) -const ( - LaunchMethodSudo uint8 = iota - LaunchMethodMachineCtl -) - -var method = [...]string{ - LaunchMethodSudo: "sudo", - LaunchMethodMachineCtl: "systemd", -} - var ( ErrConfig = errors.New("no configuration to seal") - ErrUser = errors.New("unknown user") - ErrLaunch = errors.New("invalid launch method") - - ErrSudo = errors.New("sudo not available") - ErrSystemd = errors.New("systemd not available") - ErrMachineCtl = errors.New("machinectl not available") + ErrUser = errors.New("invalid aid") + ErrHome = errors.New("invalid home directory") ) // appSeal seals the application with child-related information @@ -51,15 +37,11 @@ type appSeal struct { // persistent process state store store state.Store - // uint8 representation of launch method sealed from config - launchOption uint8 // process-specific share directory path share string // process-specific share directory path local to XDG_RUNTIME_DIR shareLocal string - // path to launcher program - toolPath string // pass-through enablement tracking from config et system.Enablements @@ -98,34 +80,6 @@ func (a *app) Seal(config *Config) error { seal.fid = config.ID seal.command = config.Command - // parses launch method text and looks up tool path - switch config.Method { - case method[LaunchMethodSudo]: - seal.launchOption = LaunchMethodSudo - if sudoPath, err := a.os.LookPath("sudo"); err != nil { - return fmsg.WrapError(ErrSudo, - "sudo not found") - } else { - seal.toolPath = sudoPath - } - case method[LaunchMethodMachineCtl]: - seal.launchOption = LaunchMethodMachineCtl - if !a.os.SdBooted() { - return fmsg.WrapError(ErrSystemd, - "system has not been booted with systemd as init system") - } - - if machineCtlPath, err := a.os.LookPath("machinectl"); err != nil { - return fmsg.WrapError(ErrMachineCtl, - "machinectl not found") - } else { - seal.toolPath = machineCtlPath - } - default: - return fmsg.WrapError(ErrLaunch, - "invalid launch method") - } - // create seal system component seal.sys = new(appSealSys) @@ -138,16 +92,44 @@ func (a *app) Seal(config *Config) error { seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID) seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString) - // look up user from system - if u, err := a.os.Lookup(config.User); err != nil { - if errors.As(err, new(user.UnknownUserError)) { - return fmsg.WrapError(ErrUser, "unknown user", config.User) + // validate uid and set user info + if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 { + return fmsg.WrapError(ErrUser, + fmt.Sprintf("aid %d out of range", config.Confinement.AppID)) + } else { + seal.sys.user = appUser{ + aid: config.Confinement.AppID, + as: strconv.Itoa(config.Confinement.AppID), + home: config.Confinement.Home, + username: config.Confinement.Username, + } + if seal.sys.user.username == "" { + seal.sys.user.username = "chronos" + } + if seal.sys.user.home == "" || !path.IsAbs(seal.sys.user.home) { + return fmsg.WrapError(ErrHome, + fmt.Sprintf("invalid home directory %q", seal.sys.user.home)) + } + + // invoke fsu for full uid + if u, err := a.os.Uid(seal.sys.user.aid); err != nil { + return fmsg.WrapErrorSuffix(err, + "cannot obtain uid from fsu:") } else { - // unreachable - panic(err) + seal.sys.user.uid = u + seal.sys.user.us = strconv.Itoa(u) + } + + // resolve supplementary group ids from names + seal.sys.user.supp = make([]string, len(config.Confinement.Groups)) + for i, name := range config.Confinement.Groups { + if g, err := a.os.LookupGroup(name); err != nil { + return fmsg.WrapError(err, + fmt.Sprintf("unknown group %q", name)) + } else { + seal.sys.user.supp[i] = g.Gid + } } - } else { - seal.sys.user = u } // map sandbox config to bwrap @@ -230,15 +212,10 @@ func (a *app) Seal(config *Config) error { // open process state store // the simple store only starts holding an open file after first action // store activity begins after Start is called and must end before Wait - seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.Uid) + seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.as) - // parse string UID - if u, err := strconv.Atoi(seal.sys.user.Uid); err != nil { - // unreachable unless kernel bug - panic("uid parse") - } else { - seal.sys.I = system.New(u) - } + // initialise system interface with full uid + seal.sys.I = system.New(seal.sys.user.uid) // pass through enablements seal.et = config.Confinement.Enablements @@ -249,11 +226,8 @@ func (a *app) Seal(config *Config) error { } // verbose log seal information - fmsg.VPrintln("created application seal as user", - seal.sys.user.Username, "("+seal.sys.user.Uid+"),", - "method:", config.Method+",", - "launcher:", seal.toolPath+",", - "command:", config.Command) + fmsg.VPrintf("created application seal for uid %s (%s) groups: %v, command: %s", + seal.sys.user.us, seal.sys.user.username, config.Confinement.Groups, config.Command) // seal app and release lock a.seal = seal diff --git a/internal/app/share.display.go b/internal/app/share.display.go index 7e814a83..073a29fe 100644 --- a/internal/app/share.display.go +++ b/internal/app/share.display.go @@ -58,7 +58,7 @@ func (seal *appSeal) shareDisplay(os linux.System) error { return fmsg.WrapError(ErrXDisplay, "DISPLAY is not set") } else { - seal.sys.ChangeHosts(seal.sys.user.Username) + seal.sys.ChangeHosts(seal.sys.user.us) seal.sys.bwrap.SetEnv[display] = d seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix") } diff --git a/internal/app/share.system.go b/internal/app/share.system.go index 08b006c3..7c97c48a 100644 --- a/internal/app/share.system.go +++ b/internal/app/share.system.go @@ -16,12 +16,12 @@ const ( func (seal *appSeal) shareSystem() { // ensure Share (e.g. `/tmp/fortify.%d`) // acl is unnecessary as this directory is world executable - seal.sys.Ensure(seal.SharePath, 0701) + seal.sys.Ensure(seal.SharePath, 0711) // ensure process-specific share (e.g. `/tmp/fortify.%d/%s`) // acl is unnecessary as this directory is world executable seal.share = path.Join(seal.SharePath, seal.id) - seal.sys.Ephemeral(system.Process, seal.share, 0701) + seal.sys.Ephemeral(system.Process, seal.share, 0711) // ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`) targetTmpdirParent := path.Join(seal.SharePath, "tmpdir") @@ -29,7 +29,7 @@ func (seal *appSeal) shareSystem() { seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute) // ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`) - targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid) + targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as) seal.sys.Ensure(targetTmpdir, 01700) seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute) seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true) @@ -49,15 +49,21 @@ func (seal *appSeal) sharePasswd(os linux.System) { // generate /etc/passwd passwdPath := path.Join(seal.share, "passwd") username := "chronos" - if seal.sys.user.Username != "" { - username = seal.sys.user.Username - seal.sys.bwrap.SetEnv["USER"] = seal.sys.user.Username + if seal.sys.user.username != "" { + username = seal.sys.user.username } homeDir := "/var/empty" - if seal.sys.user.HomeDir != "" { - homeDir = seal.sys.user.HomeDir - seal.sys.bwrap.SetEnv["HOME"] = seal.sys.user.HomeDir + if seal.sys.user.home != "" { + homeDir = seal.sys.user.home } + + // bind home directory + seal.sys.bwrap.Bind(homeDir, homeDir, false, true) + seal.sys.bwrap.Chdir = homeDir + + seal.sys.bwrap.SetEnv["USER"] = username + seal.sys.bwrap.SetEnv["HOME"] = homeDir + passwd := username + ":x:" + seal.sys.mappedIDString + ":" + seal.sys.mappedIDString + ":Fortify:" + homeDir + ":" + sh + "\n" seal.sys.Write(passwdPath, passwd) diff --git a/internal/app/start.go b/internal/app/start.go index 67a091c6..0feb0ae6 100644 --- a/internal/app/start.go +++ b/internal/app/start.go @@ -41,19 +41,13 @@ func (a *app) Start() error { } } - // select command builder - var commandBuilder shim.CommandBuilder - switch a.seal.launchOption { - case LaunchMethodSudo: - commandBuilder = a.commandBuilderSudo - case LaunchMethodMachineCtl: - commandBuilder = a.commandBuilderMachineCtl - default: - panic("unreachable") - } - // construct shim manager - a.shim = shim.New(a.seal.toolPath, uint32(a.seal.sys.UID()), path.Join(a.seal.share, "shim"), a.seal.wl, + a.shim = shim.New( + uint32(a.seal.sys.UID()), + a.seal.sys.user.as, + a.seal.sys.user.supp, + path.Join(a.seal.share, "shim"), + a.seal.wl, &shim0.Payload{ Argv: a.seal.command, Exec: shimExec, @@ -62,9 +56,6 @@ func (a *app) Start() error { Verbose: fmsg.Verbose(), }, - // checkPid is impossible at the moment since there is no reliable way to obtain shim's pid - // this feature is disabled here until sudo is replaced by fortify suid wrapper - false, ) // startup will go ahead, commit system setup @@ -73,7 +64,7 @@ func (a *app) Start() error { } a.seal.sys.needRevert = true - if startTime, err := a.shim.Start(commandBuilder); err != nil { + if startTime, err := a.shim.Start(); err != nil { return err } else { // shim start and setup success, create process state @@ -81,7 +72,6 @@ func (a *app) Start() error { PID: a.shim.Unwrap().Process.Pid, Command: a.seal.command, Capability: a.seal.et, - Method: method[a.seal.launchOption], Argv: a.shim.Unwrap().Args, Time: *startTime, } @@ -166,20 +156,31 @@ func (a *app) Wait() (int, error) { // failure prior to process start r = 255 } else { + wait := make(chan error, 1) + go func() { wait <- cmd.Wait() }() + + select { // wait for process and resolve exit code - if err := cmd.Wait(); err != nil { - var exitError *exec.ExitError - if !errors.As(err, &exitError) { - // should be unreachable - a.waitErr = err + case err := <-wait: + if err != nil { + var exitError *exec.ExitError + if !errors.As(err, &exitError) { + // should be unreachable + a.waitErr = err + } + + // store non-zero return code + r = exitError.ExitCode() + } else { + r = cmd.ProcessState.ExitCode() } + fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r) - // store non-zero return code - r = exitError.ExitCode() - } else { - r = cmd.ProcessState.ExitCode() + // alternative exit path when kill was unsuccessful + case err := <-a.shim.WaitFallback(): + r = 255 + fmsg.Printf("cannot terminate shim on faulted setup: %v", err) } - fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r) } // child process exited, resume output diff --git a/internal/app/system.go b/internal/app/system.go index 47bbd7ed..6a03b92f 100644 --- a/internal/app/system.go +++ b/internal/app/system.go @@ -1,8 +1,6 @@ package app import ( - "os/user" - "git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/internal/linux" @@ -18,7 +16,7 @@ type appSealSys struct { // default formatted XDG_RUNTIME_DIR of User runtime string // target user sealed from config - user *user.User + user appUser // mapped uid and gid in user namespace mappedID int @@ -32,6 +30,26 @@ type appSealSys struct { // protected by upstream mutex } +type appUser struct { + // full uid resolved by fsu + uid int + // string representation of uid + us string + + // supplementary group ids + supp []string + + // application id + aid int + // string representation of aid + as string + + // app user home directory + home string + // passwd database username + username string +} + // shareAll calls all share methods in sequence func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error { if seal.shared { diff --git a/internal/fmsg/defer.go b/internal/fmsg/defer.go index ed4a7735..0f3f5ad5 100644 --- a/internal/fmsg/defer.go +++ b/internal/fmsg/defer.go @@ -56,9 +56,10 @@ func Exit(code int) { os.Exit(code) } -func Withhold() { +func Suspend() { dequeueOnce.Do(dequeue) if wstate.CompareAndSwap(false, true) { + queueSync.Wait() withhold <- struct{}{} } } diff --git a/internal/linux/interface.go b/internal/linux/interface.go index 36f61b1a..a920492f 100644 --- a/internal/linux/interface.go +++ b/internal/linux/interface.go @@ -22,8 +22,8 @@ type System interface { LookPath(file string) (string, error) // Executable provides [os.Executable]. Executable() (string, error) - // Lookup provides [user.Lookup]. - Lookup(username string) (*user.User, error) + // LookupGroup provides [user.LookupGroup]. + LookupGroup(name string) (*user.Group, error) // ReadDir provides [os.ReadDir]. ReadDir(name string) ([]fs.DirEntry, error) // Stat provides [os.Stat]. @@ -35,10 +35,10 @@ type System interface { // Stdout provides [os.Stdout]. Stdout() io.Writer - // FshimPath returns an absolute path to the fshim binary. - FshimPath() string // Paths returns a populated [Paths] struct. Paths() Paths + // Uid invokes fsu and returns target uid. + Uid(aid int) (int, error) // SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html SdBooted() bool } diff --git a/internal/linux/std.go b/internal/linux/std.go index e0a0994c..4ea5b903 100644 --- a/internal/linux/std.go +++ b/internal/linux/std.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "os/user" + "strconv" "sync" "git.ophivana.moe/security/fortify/internal" @@ -21,41 +22,75 @@ type Std struct { sdBooted bool sdBootedOnce sync.Once - fshim string - fshimOnce sync.Once + uidOnce sync.Once + uidCopy map[int]struct { + uid int + err error + } + uidMu sync.RWMutex } -func (s *Std) Geteuid() int { return os.Geteuid() } -func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) } -func (s *Std) TempDir() string { return os.TempDir() } -func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) } -func (s *Std) Executable() (string, error) { return os.Executable() } -func (s *Std) Lookup(username string) (*user.User, error) { return user.Lookup(username) } -func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } -func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } -func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) } -func (s *Std) Exit(code int) { fmsg.Exit(code) } -func (s *Std) Stdout() io.Writer { return os.Stdout } +func (s *Std) Geteuid() int { return os.Geteuid() } +func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) } +func (s *Std) TempDir() string { return os.TempDir() } +func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) } +func (s *Std) Executable() (string, error) { return os.Executable() } +func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) } +func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } +func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } +func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) } +func (s *Std) Exit(code int) { fmsg.Exit(code) } +func (s *Std) Stdout() io.Writer { return os.Stdout } const xdgRuntimeDir = "XDG_RUNTIME_DIR" -func (s *Std) FshimPath() string { - s.fshimOnce.Do(func() { - p, ok := internal.Path(internal.Fshim) - if !ok { - fmsg.Fatal("invalid fshim path, this copy of fortify is not compiled correctly") - } - s.fshim = p - }) - - return s.fshim -} - func (s *Std) Paths() Paths { s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) }) return s.paths } +func (s *Std) Uid(aid int) (int, error) { + s.uidOnce.Do(func() { + s.uidCopy = make(map[int]struct { + uid int + err error + }) + }) + + s.uidMu.RLock() + if u, ok := s.uidCopy[aid]; ok { + s.uidMu.RUnlock() + return u.uid, u.err + } + + s.uidMu.RUnlock() + s.uidMu.Lock() + defer s.uidMu.Unlock() + + u := struct { + uid int + err error + }{} + defer func() { s.uidCopy[aid] = u }() + + u.uid = -1 + if fsu, ok := internal.Check(internal.Fsu); !ok { + fmsg.Fatal("invalid fsu path, this copy of fshim is not compiled correctly") + panic("unreachable") + } else { + cmd := exec.Command(fsu) + cmd.Path = fsu + cmd.Stderr = os.Stderr // pass through fatal messages + cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)} + cmd.Dir = "/" + var p []byte + if p, u.err = cmd.Output(); u.err == nil { + u.uid, u.err = strconv.Atoi(string(p)) + } + return u.uid, u.err + } +} + func (s *Std) SdBooted() bool { s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() }) return s.sdBooted diff --git a/internal/path.go b/internal/path.go index 3bc28216..51388536 100644 --- a/internal/path.go +++ b/internal/path.go @@ -4,7 +4,6 @@ import "path" var ( Fsu = compPoison - Fshim = compPoison Finit = compPoison ) diff --git a/internal/state/print.go b/internal/state/print.go index 8ca3529e..174f0693 100644 --- a/internal/state/print.go +++ b/internal/state/print.go @@ -67,10 +67,10 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time // write header when initialising if !fmsg.Verbose() { - _, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tMethod\tCommand") + _, _ = fmt.Fprintln(*w, "\tPID\tApp\tUptime\tEnablements\tCommand") } else { // argv is emitted in body when verbose - _, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv") + _, _ = fmt.Fprintln(*w, "\tPID\tApp\tArgv") } } @@ -96,13 +96,13 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time } if !fmsg.Verbose() { - _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n", - s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Method, + _, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\t%s\t%s\n", + state.PID, s.path[len(s.path)-1], now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Command) } else { // emit argv instead when verbose - _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\n", - s.path[len(s.path)-1], state.PID, state.Argv) + _, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\n", + state.PID, s.path[len(s.path)-1], state.Argv) } } diff --git a/internal/state/state.go b/internal/state/state.go index 81df9655..ee5a95f7 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -33,8 +33,6 @@ type State struct { // capability enablements applied to child Capability system.Enablements - // user switch method - Method string // full argv whe launching Argv []string // process start time |
