Compare commits

..

11 Commits

Author SHA1 Message Date
Ophestra Umiker cfd05b10f1
release: 0.0.11
release / release (push) Successful in 28s Details
test / test (push) Successful in 19s Details
This will be the final release before major command line interface changes. This version is tagged as it contains many fixes that still impacts the permissive defaults usage pattern.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 13:46:47 +09:00
Ophestra Umiker aa067436a7
workflows: build all packages with full ldflags
test / test (push) Successful in 20s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 13:43:57 +09:00
Ophestra Umiker d7df24c999
fmsg: drop messages when msgbuf is full during withhold
test / test (push) Successful in 20s Details
Logging functions are not expected to block. This change fixes multiple hangs where more than 64 messages are produced during withhold.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 12:56:19 +09:00
Ophestra Umiker 88abcbe0b2
cmd/fsu: remove import of internal package
test / test (push) Successful in 24s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 12:32:14 +09:00
Ophestra Umiker af15b1c048
app: support mapping target uid as privileged uid in sandbox
test / test (push) Successful in 40s Details
Chromium's D-Bus client implementation refuses to work when its getuid call returns a different value than what the D-Bus server is running as. The reason behind this is not fully understood, but this workaround is implemented to support chromium and electron apps. This is not used by default since it has many side effects that break many other programs, like SSH on NixOS.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 03:15:39 +09:00
Ophestra Umiker 7962681f4a
app: format mapped uid instead of real uid
test / test (push) Successful in 19s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 00:49:32 +09:00
Ophestra Umiker bfcce3ff75
system/dbus: buffer xdg-dbus-proxy messages
test / test (push) Successful in 21s Details
Pointing xdg-dbus-proxy to stdout/stderr makes a huge mess. This change enables app to neatly print out prefixed xdg-dbus-proxy messages after output is resumed.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-03 03:07:02 +09:00
Ophestra Umiker 8cd3651bb6
cmd/fshim/ipc: friendly setup timeout message
test / test (push) Successful in 22s Details
This message eventually gets returned by the app's Start method, so they should be wrapped to provide a friendly message.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-03 02:03:30 +09:00
Ophestra Umiker 422d8e00d5
fortify: replace direct syscall with prctl wrapper
test / test (push) Successful in 20s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-02 17:00:25 +09:00
Ophestra Umiker 584732f80a
cmd: shim and init into separate binaries
test / test (push) Successful in 19s Details
This change also fixes a deadlock when shim fails to connect and complete the setup.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-02 03:13:57 +09:00
Ophestra Umiker 4b7b899bb3
add package doc comments
test / test (push) Successful in 19s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 20:57:59 +09:00
39 changed files with 492 additions and 252 deletions

View File

@ -30,8 +30,14 @@ jobs:
- name: Build for Linux - name: Build for Linux
run: >- run: >-
sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/fortify && go build -v -ldflags '-s -w
sha256sum --tag -b bin/fortify > bin/fortify.sha256" -X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }}
-X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu
-X git.ophivana.moe/security/fortify/internal.Fshim=/usr/libexec/fortify/fshim
-X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit
-X main.Fmain=/usr/bin/fortify'
-o bin/ ./... &&
(cd bin && sha512sum --tag -b * > sha512sums)
- name: Release - name: Release
id: use-go-action id: use-go-action

View File

@ -33,5 +33,11 @@ jobs:
- name: Build for Linux - name: Build for Linux
run: >- run: >-
sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/fortify && go build -v -ldflags '-s -w
sha256sum --tag -b bin/fortify > bin/fortify.sha256" -X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }}
-X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu
-X git.ophivana.moe/security/fortify/internal.Fshim=/usr/libexec/fortify/fshim
-X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit
-X main.Fmain=/usr/bin/fortify'
-o bin/ ./... &&
(cd bin && sha512sum --tag -b * > sha512sums)

View File

@ -1,3 +1,4 @@
// Package acl implements simple ACL manipulation via libacl.
package acl package acl
import "unsafe" import "unsafe"

View File

@ -1,6 +1,6 @@
package init0 package init0
const EnvInit = "FORTIFY_INIT" const Env = "FORTIFY_INIT"
type Payload struct { type Payload struct {
// target full exec path // target full exec path

View File

@ -1,9 +1,8 @@
package init0 package main
import ( import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"flag"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@ -12,58 +11,80 @@ import (
"syscall" "syscall"
"time" "time"
init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const ( const (
// time to wait for linger processes after death initial process // time to wait for linger processes after death of initial process
residualProcessTimeout = 5 * time.Second residualProcessTimeout = 5 * time.Second
) )
// everything beyond this point runs within pid namespace // everything beyond this point runs within pid namespace
// proceed with caution! // proceed with caution!
func doInit(fd uintptr) { func main() {
// sharing stdout with shim
// USE WITH CAUTION
fmsg.SetPrefix("init") fmsg.SetPrefix("init")
// setting this prevents ptrace
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
fmsg.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
panic("unreachable")
}
if os.Getpid() != 1 {
fmsg.Fatal("this process must run as pid 1")
panic("unreachable")
}
// re-exec // re-exec
if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) { if len(os.Args) > 0 && (os.Args[0] != "finit" || len(os.Args) != 1) && path.IsAbs(os.Args[0]) {
if err := syscall.Exec(os.Args[0], []string{"fortify", "init"}, os.Environ()); err != nil { if err := syscall.Exec(os.Args[0], []string{"finit"}, os.Environ()); err != nil {
fmsg.Println("cannot re-exec self:", err) fmsg.Println("cannot re-exec self:", err)
// continue anyway // continue anyway
} }
} }
var payload Payload // setup pipe fd from environment
p := os.NewFile(fd, "config-stream") var setup *os.File
if p == nil { if s, ok := os.LookupEnv(init0.Env); !ok {
fmsg.Fatal("invalid config descriptor") fmsg.Fatal("FORTIFY_INIT not set")
} panic("unreachable")
if err := gob.NewDecoder(p).Decode(&payload); err != nil { } else {
fmsg.Fatal("cannot decode init payload:", err) if fd, err := strconv.Atoi(s); err != nil {
fmsg.Fatalf("cannot parse %q: %v", s, err)
panic("unreachable")
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
fmsg.Fatal("invalid config descriptor")
panic("unreachable")
}
}
}
var payload init0.Payload
if err := gob.NewDecoder(setup).Decode(&payload); err != nil {
fmsg.Fatal("cannot decode init setup payload:", err)
panic("unreachable")
} else { } else {
// sharing stdout with parent
// USE WITH CAUTION
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
// child does not need to see this // child does not need to see this
if err = os.Unsetenv(EnvInit); err != nil { if err = os.Unsetenv(init0.Env); err != nil {
fmsg.Println("cannot unset", EnvInit+":", err) fmsg.Printf("cannot unset %s: %v", init0.Env, err)
// not fatal // not fatal
} else { } else {
fmsg.VPrintln("received configuration") fmsg.VPrintln("received configuration")
} }
} }
// close config fd
if err := p.Close(); err != nil {
fmsg.Println("cannot close config fd:", err)
// not fatal
}
// die with parent // die with parent
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 { if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil {
fmsg.Fatal("prctl(PR_SET_PDEATHSIG, SIGKILL):", errno.Error()) fmsg.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
} }
cmd := exec.Command(payload.Argv0) cmd := exec.Command(payload.Argv0)
@ -82,6 +103,13 @@ func doInit(fd uintptr) {
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err) fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
} }
fmsg.Withhold()
// close setup pipe as setup is now complete
if err := setup.Close(); err != nil {
fmsg.Println("cannot close setup pipe:", err)
// not fatal
}
sig := make(chan os.Signal, 2) sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
@ -122,6 +150,7 @@ func doInit(fd uintptr) {
close(done) close(done)
}() }()
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{}) timeout := make(chan struct{})
r := 2 r := 2
@ -129,9 +158,13 @@ func doInit(fd uintptr) {
select { select {
case s := <-sig: case s := <-sig:
fmsg.VPrintln("received", s.String()) fmsg.VPrintln("received", s.String())
fmsg.Resume() // output could still be withheld at this point, so resume is called
fmsg.Exit(0) fmsg.Exit(0)
case w := <-info: case w := <-info:
if w.wpid == cmd.Process.Pid { if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
fmsg.Resume()
switch { switch {
case w.wstatus.Exited(): case w.wstatus.Exited():
r = w.wstatus.ExitStatus() r = w.wstatus.ExitStatus()
@ -154,21 +187,3 @@ func doInit(fd uintptr) {
} }
} }
} }
// Try runs init and stops execution if FORTIFY_INIT is set.
func Try() {
if os.Getpid() != 1 {
return
}
if args := flag.Args(); len(args) == 1 && args[0] == "init" {
if s, ok := os.LookupEnv(EnvInit); ok {
if fd, err := strconv.Atoi(s); err != nil {
fmsg.Fatalf("cannot parse %q: %v", s, err)
} else {
doInit(uintptr(fd))
}
panic("unreachable")
}
}
}

View File

@ -1,4 +1,4 @@
package shim package shim0
import ( import (
"encoding/gob" "encoding/gob"
@ -9,13 +9,13 @@ import (
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const EnvShim = "FORTIFY_SHIM" const Env = "FORTIFY_SHIM"
type Payload struct { type Payload struct {
// child full argv // child full argv
Argv []string Argv []string
// fortify, bwrap, target full exec path // bwrap, target full exec path
Exec [3]string Exec [2]string
// bwrap config // bwrap config
Bwrap *bwrap.Config Bwrap *bwrap.Config
// whether to pass wayland fd // whether to pass wayland fd
@ -25,7 +25,7 @@ type Payload struct {
Verbose bool Verbose bool
} }
func (p *Payload) serve(conn *net.UnixConn, wl *Wayland) error { func (p *Payload) Serve(conn *net.UnixConn, wl *Wayland) error {
if err := gob.NewEncoder(conn).Encode(*p); err != nil { if err := gob.NewEncoder(conn).Encode(*p); err != nil {
return fmsg.WrapErrorSuffix(err, return fmsg.WrapErrorSuffix(err,
"cannot stream shim payload:") "cannot stream shim payload:")

View File

@ -11,9 +11,12 @@ import (
"time" "time"
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const shimSetupTimeout = 5 * time.Second
// used by the parent process // used by the parent process
type Shim struct { type Shim struct {
@ -32,12 +35,12 @@ type Shim struct {
abortErr atomic.Pointer[error] abortErr atomic.Pointer[error]
abortOnce sync.Once abortOnce sync.Once
// wayland mediation, nil if disabled // wayland mediation, nil if disabled
wl *Wayland wl *shim0.Wayland
// shim setup payload // shim setup payload
payload *Payload payload *shim0.Payload
} }
func New(executable string, uid uint32, socket string, wl *Wayland, payload *Payload, checkPid bool) *Shim { func New(executable string, uid uint32, socket string, wl *shim0.Wayland, payload *shim0.Payload, checkPid bool) *Shim {
return &Shim{uid: uid, executable: executable, socket: socket, wl: wl, payload: payload, checkPid: checkPid} return &Shim{uid: uid, executable: executable, socket: socket, wl: wl, payload: payload, checkPid: checkPid}
} }
@ -84,7 +87,7 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
} }
// start user switcher process and save time // start user switcher process and save time
s.cmd = exec.Command(s.executable, f(EnvShim+"="+s.socket)...) s.cmd = exec.Command(s.executable, f(shim0.Env+"="+s.socket)...)
s.cmd.Env = []string{} s.cmd.Env = []string{}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/" s.cmd.Dir = "/"
@ -105,9 +108,19 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
defer func() { killShim() }() defer func() { killShim() }()
accept() accept()
conn := <-cf var conn *net.UnixConn
if conn == nil { select {
case c := <-cf:
if c == nil {
return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:") return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:")
} else {
conn = c
}
case <-time.After(shimSetupTimeout):
err := fmsg.WrapError(errors.New("timed out waiting for shim"),
"timed out waiting for shim to connect")
s.AbortWait(err)
return &startTime, err
} }
// authenticate against called provided uid and shim pid // authenticate against called provided uid and shim pid
@ -129,7 +142,7 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
// serve payload and wayland fd if enabled // serve payload and wayland fd if enabled
// this also closes the connection // this also closes the connection
err := s.payload.serve(conn, s.wl) err := s.payload.Serve(conn, s.wl)
if err == nil { if err == nil {
killShim = func() {} killShim = func() {}
} }
@ -158,6 +171,7 @@ func (s *Shim) serve() (chan *net.UnixConn, func(), error) {
} }
go func() { go func() {
cfWg := new(sync.WaitGroup)
for { for {
select { select {
case err = <-s.abort: case err = <-s.abort:
@ -168,15 +182,24 @@ func (s *Shim) serve() (chan *net.UnixConn, func(), error) {
fmsg.Println("cannot close setup socket:", err) fmsg.Println("cannot close setup socket:", err)
} }
close(s.abort) close(s.abort)
go func() {
cfWg.Wait()
close(cf) close(cf)
}()
return return
case <-accept: case <-accept:
cfWg.Add(1)
go func() {
defer cfWg.Done()
if conn, err0 := l.AcceptUnix(); err0 != nil { if conn, err0 := l.AcceptUnix(); err0 != nil {
s.Abort(err0) // does not block, breaks loop // breaks loop
cf <- nil // receiver sees nil value and loads err0 stored during abort s.Abort(err0)
// receiver sees nil value and loads err0 stored during abort
cf <- nil
} else { } else {
cf <- conn cf <- conn
} }
}()
} }
} }
}() }()

View File

@ -1,4 +1,4 @@
package shim package shim0
import ( import (
"fmt" "fmt"

View File

@ -1,37 +1,63 @@
package shim package main
import ( import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"flag"
"net" "net"
"os" "os"
"path" "path"
"strconv" "strconv"
"syscall" "syscall"
init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc"
shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
init0 "git.ophivana.moe/security/fortify/internal/init"
) )
// everything beyond this point runs as target user // everything beyond this point runs as unconstrained target user
// proceed with caution! // proceed with caution!
func doShim(socket string) { func main() {
// sharing stdout with fortify
// USE WITH CAUTION
fmsg.SetPrefix("shim") fmsg.SetPrefix("shim")
// setting this prevents ptrace
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
fmsg.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
panic("unreachable")
}
// re-exec // re-exec
if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) { if len(os.Args) > 0 && (os.Args[0] != "fshim" || len(os.Args) != 1) && path.IsAbs(os.Args[0]) {
if err := syscall.Exec(os.Args[0], []string{"fortify", "shim"}, os.Environ()); err != nil { if err := syscall.Exec(os.Args[0], []string{"fshim"}, os.Environ()); err != nil {
fmsg.Println("cannot re-exec self:", err) fmsg.Println("cannot re-exec self:", err)
// continue anyway // continue anyway
} }
} }
// lookup socket path from environment
var socketPath string
if s, ok := os.LookupEnv(shim.Env); !ok {
fmsg.Fatal("FORTIFY_SHIM not set")
panic("unreachable")
} else {
socketPath = s
}
// check path to finit
var finitPath string
if p, ok := internal.Path(internal.Finit); !ok {
fmsg.Fatal("invalid finit path, this copy of fshim is not compiled correctly")
} else {
finitPath = p
}
// dial setup socket // dial setup socket
var conn *net.UnixConn var conn *net.UnixConn
if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socket, Net: "unix"}); err != nil { if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socketPath, Net: "unix"}); err != nil {
fmsg.Fatal("cannot dial setup socket:", err) fmsg.Fatal("cannot dial setup socket:", err)
panic("unreachable") panic("unreachable")
} else { } else {
@ -39,12 +65,10 @@ func doShim(socket string) {
} }
// decode payload gob stream // decode payload gob stream
var payload Payload var payload shim.Payload
if err := gob.NewDecoder(conn).Decode(&payload); err != nil { if err := gob.NewDecoder(conn).Decode(&payload); err != nil {
fmsg.Fatal("cannot decode shim payload:", err) fmsg.Fatal("cannot decode shim payload:", err)
} else { } else {
// sharing stdout with parent
// USE WITH CAUTION
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
} }
@ -74,7 +98,7 @@ func doShim(socket string) {
ic.Argv = payload.Argv ic.Argv = payload.Argv
if len(ic.Argv) > 0 { if len(ic.Argv) > 0 {
// looked up from $PATH by parent // looked up from $PATH by parent
ic.Argv0 = payload.Exec[2] ic.Argv0 = payload.Exec[1]
} else { } else {
// no argv, look up shell instead // no argv, look up shell instead
var ok bool var ok bool
@ -103,7 +127,7 @@ func doShim(socket string) {
if r, w, err := os.Pipe(); err != nil { if r, w, err := os.Pipe(); err != nil {
fmsg.Fatal("cannot pipe:", err) fmsg.Fatal("cannot pipe:", err)
} else { } else {
conf.SetEnv[init0.EnvInit] = strconv.Itoa(3 + len(extraFiles)) conf.SetEnv[init0.Env] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r) extraFiles = append(extraFiles, r)
fmsg.VPrintln("transmitting config to init") fmsg.VPrintln("transmitting config to init")
@ -115,8 +139,9 @@ func doShim(socket string) {
}() }()
} }
helper.BubblewrapName = payload.Exec[1] // resolved bwrap path by parent helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if b, err := helper.NewBwrap(conf, nil, payload.Exec[0], func(int, int) []string { return []string{"init"} }); err != nil { if b, err := helper.NewBwrap(conf, nil, finitPath,
func(int, int) []string { return make([]string, 0) }); err != nil {
fmsg.Fatal("malformed sandbox config:", err) fmsg.Fatal("malformed sandbox config:", err)
} else { } else {
cmd := b.Unwrap() cmd := b.Unwrap()
@ -167,13 +192,3 @@ func receiveWLfd(conn *net.UnixConn) (int, error) {
return fds[0], nil return fds[0], nil
} }
} }
// Try runs shim and stops execution if FORTIFY_SHIM is set.
func Try() {
if args := flag.Args(); len(args) == 1 && args[0] == "shim" {
if s, ok := os.LookupEnv(EnvShim); ok {
doShim(s)
panic("unreachable")
}
}
}

View File

@ -11,15 +11,13 @@ import (
) )
const ( const (
compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
fsuConfFile = "/etc/fsurc" fsuConfFile = "/etc/fsurc"
envShim = "FORTIFY_SHIM" envShim = "FORTIFY_SHIM"
envAID = "FORTIFY_APP_ID" envAID = "FORTIFY_APP_ID"
fpPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
) )
// FortifyPath is the path to fortify, set at compile time. var Fmain = compPoison
var FortifyPath = fpPoison
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
@ -35,9 +33,11 @@ func main() {
log.Fatal("this program must not be started by root") log.Fatal("this program must not be started by root")
} }
// validate compiled in fortify path var fmain string
if FortifyPath == fpPoison || !path.IsAbs(FortifyPath) { if p, ok := checkPath(Fmain); !ok {
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly") log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
} else {
fmain = p
} }
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe") pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
@ -45,7 +45,7 @@ func main() {
log.Fatalf("cannot read parent executable path: %v", err) log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") { } else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("fortify executable has been deleted") log.Fatal("fortify executable has been deleted")
} else if p != FortifyPath { } else if p != fmain {
log.Fatal("this program must be started by fortify") log.Fatal("this program must be started by fortify")
} }
@ -86,7 +86,7 @@ func main() {
if err := syscall.Setresuid(uid, uid, uid); err != nil { if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err) log.Fatalf("cannot set uid: %v", err)
} }
if err := syscall.Exec(FortifyPath, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupPath}); err != nil { if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupPath}); err != nil {
log.Fatalf("cannot start shim: %v", err) log.Fatalf("cannot start shim: %v", err)
} }
@ -138,3 +138,7 @@ func parseConfig(p string, puid int) (fid int, ok bool) {
return -1, false return -1, false
} }
} }
func checkPath(p string) (string, bool) {
return p, p != compPoison && p != "" && path.IsAbs(p)
}

View File

@ -1,3 +1,4 @@
// Package dbus wraps xdg-dbus-proxy and implements configuration and sandboxing of the underlying helper process.
package dbus package dbus
import ( import (

View File

@ -1,6 +1,4 @@
/* // Package helper runs external helpers with optional sandboxing and manages their status/args pipes.
Package helper runs external helpers and manages their status and args FDs.
*/
package helper package helper
import ( import (

View File

@ -3,8 +3,8 @@ package app
import ( import (
"sync" "sync"
"git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim"
"git.ophivana.moe/security/fortify/internal/shim" "git.ophivana.moe/security/fortify/internal/linux"
) )
type App interface { type App interface {
@ -25,7 +25,7 @@ type app struct {
// application unique identifier // application unique identifier
id *ID id *ID
// operating system interface // operating system interface
os internal.System os linux.System
// shim process manager // shim process manager
shim *shim.Shim shim *shim.Shim
// child process related information // child process related information
@ -63,7 +63,7 @@ func (a *app) WaitErr() error {
return a.waitErr return a.waitErr
} }
func New(os internal.System) (App, error) { func New(os linux.System) (App, error) {
a := new(app) a := new(app)
a.id = new(ID) a.id = new(ID)
a.os = os a.os = os

View File

@ -9,8 +9,8 @@ import (
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -47,7 +47,7 @@ var testCasesNixos = []sealTestCase{
"SHELL": "/run/current-system/sw/bin/zsh", "SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color", "TERM": "xterm-256color",
"USER": "chronos", "USER": "chronos",
"XDG_RUNTIME_DIR": "/run/user/150", "XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user", "XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty"}, "XDG_SESSION_TYPE": "tty"},
Chmod: make(bwrap.ChmodConfig), Chmod: make(bwrap.ChmodConfig),
@ -183,7 +183,7 @@ var testCasesNixos = []sealTestCase{
Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true). Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576). Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576). Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/150", 8388608). Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd"). Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group"). Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group").
Tmpfs("/var/run/nscd", 8192), Tmpfs("/var/run/nscd", 8192),
@ -287,16 +287,16 @@ var testCasesNixos = []sealTestCase{
UserNS: true, UserNS: true,
Clearenv: true, Clearenv: true,
SetEnv: map[string]string{ SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/150/bus", "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket", "DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"HOME": "/home/chronos", "HOME": "/home/chronos",
"PULSE_COOKIE": "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "PULSE_COOKIE": "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie",
"PULSE_SERVER": "unix:/run/user/150/pulse/native", "PULSE_SERVER": "unix:/run/user/65534/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh", "SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color", "TERM": "xterm-256color",
"USER": "chronos", "USER": "chronos",
"WAYLAND_DISPLAY": "/run/user/150/wayland-0", "WAYLAND_DISPLAY": "/run/user/65534/wayland-0",
"XDG_RUNTIME_DIR": "/run/user/150", "XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user", "XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty", "XDG_SESSION_TYPE": "tty",
}, },
@ -434,13 +434,13 @@ var testCasesNixos = []sealTestCase{
Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true). Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576). Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576). Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/150", 8388608). Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/150/wayland-0"). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/150/pulse/native"). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/150/bus"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192), Tmpfs("/var/run/nscd", 8192),
}, },
@ -579,8 +579,12 @@ func (s *stubNixOS) Exit(code int) {
panic("called exit on stub with code " + strconv.Itoa(code)) panic("called exit on stub with code " + strconv.Itoa(code))
} }
func (s *stubNixOS) Paths() internal.Paths { func (s *stubNixOS) FshimPath() string {
return internal.Paths{ return "/nix/store/00000000000000000000000000000000-fortify-0.0.10/bin/.fshim"
}
func (s *stubNixOS) Paths() linux.Paths {
return linux.Paths{
SharePath: "/tmp/fortify.1971", SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971", RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify", RunDirPath: "/run/user/1971/fortify",

View File

@ -7,14 +7,14 @@ import (
"time" "time"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
type sealTestCase struct { type sealTestCase struct {
name string name string
os internal.System os linux.System
config *app.Config config *app.Config
id app.ID id app.ID
wantSys *system.I wantSys *system.I

View File

@ -49,6 +49,8 @@ type SandboxConfig struct {
Net bool `json:"net,omitempty"` Net bool `json:"net,omitempty"`
// do not run in new session // do not run in new session
NoNewSession bool `json:"no_new_session,omitempty"` NoNewSession bool `json:"no_new_session,omitempty"`
// map target user uid to privileged user uid in the user namespace
UseRealUID bool `json:"use_real_uid"`
// mediated access to wayland socket // mediated access to wayland socket
Wayland bool `json:"wayland,omitempty"` Wayland bool `json:"wayland,omitempty"`
@ -77,11 +79,15 @@ type FilesystemConfig struct {
// Bwrap returns the address of the corresponding bwrap.Config to s. // Bwrap returns the address of the corresponding bwrap.Config to s.
// Note that remaining tmpfs entries must be queued by the caller prior to launch. // Note that remaining tmpfs entries must be queued by the caller prior to launch.
func (s *SandboxConfig) Bwrap() *bwrap.Config { func (s *SandboxConfig) Bwrap(uid int) *bwrap.Config {
if s == nil { if s == nil {
return nil return nil
} }
if !s.UseRealUID {
uid = 65534
}
conf := (&bwrap.Config{ conf := (&bwrap.Config{
Net: s.Net, Net: s.Net,
UserNS: s.UserNS, UserNS: s.UserNS,
@ -95,7 +101,7 @@ func (s *SandboxConfig) Bwrap() *bwrap.Config {
// initialise map // initialise map
Chmod: make(map[string]os.FileMode), Chmod: make(map[string]os.FileMode),
}). }).
SetUID(65534).SetGID(65534). SetUID(uid).SetGID(uid).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue"). Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs("/dev/fortify", 4*1024) Tmpfs("/dev/fortify", 4*1024)

View File

@ -2,11 +2,11 @@ package app
import ( import (
"git.ophivana.moe/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
func NewWithID(id ID, os internal.System) App { func NewWithID(id ID, os linux.System) App {
a := new(app) a := new(app)
a.id = &id a.id = &id
a.os = os a.os = os

View File

@ -47,8 +47,8 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
} }
innerCommand.WriteString("; ") innerCommand.WriteString("; ")
// launch fortify as shim // launch fortify shim
innerCommand.WriteString("exec " + a.seal.sys.executable + " shim") innerCommand.WriteString("exec " + a.os.FshimPath())
// append inner command // append inner command
args = append(args, innerCommand.String()) args = append(args, innerCommand.String())

View File

@ -24,7 +24,7 @@ func (a *app) commandBuilderSudo(shimEnv string) (args []string) {
args = append(args, shimEnv) args = append(args, shimEnv)
// -- $@ // -- $@
args = append(args, "--", a.seal.sys.executable, "shim") args = append(args, "--", a.os.FshimPath())
return return
} }

View File

@ -7,10 +7,10 @@ import (
"path" "path"
"strconv" "strconv"
shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/shim" "git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -41,6 +41,8 @@ type appSeal struct {
id string id string
// wayland mediation, disabled if nil // wayland mediation, disabled if nil
wl *shim.Wayland wl *shim.Wayland
// dbus proxy message buffer retriever
dbusMsg func(f func(msgbuf []string))
// freedesktop application ID // freedesktop application ID
fid string fid string
@ -66,7 +68,7 @@ type appSeal struct {
// seal system-level component // seal system-level component
sys *appSealSys sys *appSealSys
internal.Paths linux.Paths
// protected by upstream mutex // protected by upstream mutex
} }
@ -127,12 +129,14 @@ func (a *app) Seal(config *Config) error {
// create seal system component // create seal system component
seal.sys = new(appSealSys) seal.sys = new(appSealSys)
// look up fortify executable path // mapped uid
if p, err := a.os.Executable(); err != nil { if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.UseRealUID {
return fmsg.WrapErrorSuffix(err, "cannot look up fortify executable path:") seal.sys.mappedID = a.os.Geteuid()
} else { } else {
seal.sys.executable = p seal.sys.mappedID = 65534
} }
seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID)
seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString)
// look up user from system // look up user from system
if u, err := a.os.Lookup(config.User); err != nil { if u, err := a.os.Lookup(config.User); err != nil {
@ -144,7 +148,6 @@ func (a *app) Seal(config *Config) error {
} }
} else { } else {
seal.sys.user = u seal.sys.user = u
seal.sys.runtime = path.Join("/run/user", u.Uid)
} }
// map sandbox config to bwrap // map sandbox config to bwrap
@ -233,7 +236,7 @@ func (a *app) Seal(config *Config) error {
config.Confinement.Sandbox = conf config.Confinement.Sandbox = conf
} }
seal.sys.bwrap = config.Confinement.Sandbox.Bwrap() seal.sys.bwrap = config.Confinement.Sandbox.Bwrap(a.os.Geteuid())
seal.sys.override = config.Confinement.Sandbox.Override seal.sys.override = config.Confinement.Sandbox.Override
if seal.sys.bwrap.SetEnv == nil { if seal.sys.bwrap.SetEnv == nil {
seal.sys.bwrap.SetEnv = make(map[string]string) seal.sys.bwrap.SetEnv = make(map[string]string)

View File

@ -22,8 +22,10 @@ func (seal *appSeal) shareDBus(config [2]*dbus.Config) error {
sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket") sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket")
// configure dbus proxy // configure dbus proxy
if err := seal.sys.ProxyDBus(config[0], config[1], sessionPath, systemPath); err != nil { if f, err := seal.sys.ProxyDBus(config[0], config[1], sessionPath, systemPath); err != nil {
return err return err
} else {
seal.dbusMsg = f
} }
// share proxy sockets // share proxy sockets

View File

@ -5,8 +5,8 @@ import (
"path" "path"
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -23,7 +23,7 @@ var (
ErrXDisplay = errors.New(display + " unset") ErrXDisplay = errors.New(display + " unset")
) )
func (seal *appSeal) shareDisplay(os internal.System) error { func (seal *appSeal) shareDisplay(os linux.System) error {
// pass $TERM to launcher // pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok { if t, ok := os.LookupEnv(term); ok {
seal.sys.bwrap.SetEnv[term] = t seal.sys.bwrap.SetEnv[term] = t

View File

@ -6,8 +6,8 @@ import (
"io/fs" "io/fs"
"path" "path"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -25,7 +25,7 @@ var (
ErrPulseMode = errors.New("unexpected pulse socket mode") ErrPulseMode = errors.New("unexpected pulse socket mode")
) )
func (seal *appSeal) sharePulse(os internal.System) error { func (seal *appSeal) sharePulse(os linux.System) error {
if !seal.et.Has(system.EPulse) { if !seal.et.Has(system.EPulse) {
return nil return nil
} }
@ -78,7 +78,7 @@ func (seal *appSeal) sharePulse(os internal.System) error {
} }
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie // discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(os internal.System) (string, error) { func discoverPulseCookie(os linux.System) (string, error) {
if p, ok := os.LookupEnv(pulseCookie); ok { if p, ok := os.LookupEnv(pulseCookie); ok {
return p, nil return p, nil
} }

View File

@ -4,7 +4,7 @@ import (
"path" "path"
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -38,7 +38,7 @@ func (seal *appSeal) shareSystem() {
seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024) seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024)
} }
func (seal *appSeal) sharePasswd(os internal.System) { func (seal *appSeal) sharePasswd(os linux.System) {
// look up shell // look up shell
sh := "/bin/sh" sh := "/bin/sh"
if s, ok := os.LookupEnv(shell); ok { if s, ok := os.LookupEnv(shell); ok {
@ -58,12 +58,12 @@ func (seal *appSeal) sharePasswd(os internal.System) {
homeDir = seal.sys.user.HomeDir homeDir = seal.sys.user.HomeDir
seal.sys.bwrap.SetEnv["HOME"] = seal.sys.user.HomeDir seal.sys.bwrap.SetEnv["HOME"] = seal.sys.user.HomeDir
} }
passwd := username + ":x:65534:65534:Fortify:" + homeDir + ":" + sh + "\n" passwd := username + ":x:" + seal.sys.mappedIDString + ":" + seal.sys.mappedIDString + ":Fortify:" + homeDir + ":" + sh + "\n"
seal.sys.Write(passwdPath, passwd) seal.sys.Write(passwdPath, passwd)
// write /etc/group // write /etc/group
groupPath := path.Join(seal.share, "group") groupPath := path.Join(seal.share, "group")
seal.sys.Write(groupPath, "fortify:x:65534:\n") seal.sys.Write(groupPath, "fortify:x:"+seal.sys.mappedIDString+":\n")
// bind /etc/passwd and /etc/group // bind /etc/passwd and /etc/group
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd") seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")

View File

@ -8,9 +8,10 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim"
"git.ophivana.moe/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/shim"
"git.ophivana.moe/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -22,9 +23,9 @@ func (a *app) Start() error {
defer a.lock.Unlock() defer a.lock.Unlock()
// resolve exec paths // resolve exec paths
shimExec := [3]string{a.seal.sys.executable, helper.BubblewrapName} shimExec := [2]string{helper.BubblewrapName}
if len(a.seal.command) > 0 { if len(a.seal.command) > 0 {
shimExec[2] = a.seal.command[0] shimExec[1] = a.seal.command[0]
} }
for i, n := range shimExec { for i, n := range shimExec {
if len(n) == 0 { if len(n) == 0 {
@ -53,7 +54,7 @@ func (a *app) Start() error {
// construct shim manager // 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(a.seal.toolPath, uint32(a.seal.sys.UID()), path.Join(a.seal.share, "shim"), a.seal.wl,
&shim.Payload{ &shim0.Payload{
Argv: a.seal.command, Argv: a.seal.command,
Exec: shimExec, Exec: shimExec,
Bwrap: a.seal.sys.bwrap, Bwrap: a.seal.sys.bwrap,
@ -184,6 +185,15 @@ func (a *app) Wait() (int, error) {
// child process exited, resume output // child process exited, resume output
fmsg.Resume() fmsg.Resume()
// print queued up dbus messages
if a.seal.dbusMsg != nil {
a.seal.dbusMsg(func(msgbuf []string) {
for _, msg := range msgbuf {
fmsg.Println(msg)
}
})
}
// close wayland connection // close wayland connection
if a.seal.wl != nil { if a.seal.wl != nil {
if err := a.seal.wl.Close(); err != nil { if err := a.seal.wl.Close(); err != nil {

View File

@ -5,7 +5,7 @@ import (
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -17,11 +17,14 @@ type appSealSys struct {
// default formatted XDG_RUNTIME_DIR of User // default formatted XDG_RUNTIME_DIR of User
runtime string runtime string
// sealed path to fortify executable, used by shim
executable string
// target user sealed from config // target user sealed from config
user *user.User user *user.User
// mapped uid and gid in user namespace
mappedID int
// string representation of mappedID
mappedIDString string
needRevert bool needRevert bool
saveState bool saveState bool
*system.I *system.I
@ -30,7 +33,7 @@ type appSealSys struct {
} }
// shareAll calls all share methods in sequence // shareAll calls all share methods in sequence
func (seal *appSeal) shareAll(bus [2]*dbus.Config, os internal.System) error { func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error {
if seal.shared { if seal.shared {
panic("seal shared twice") panic("seal shared twice")
} }

12
internal/comp.go Normal file
View File

@ -0,0 +1,12 @@
package internal
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
var (
Version = compPoison
)
// Check validates string value set at compile time.
func Check(s string) (string, bool) {
return s, s != compPoison && s != ""
}

View File

@ -8,6 +8,7 @@ import (
var ( var (
wstate atomic.Bool wstate atomic.Bool
dropped atomic.Uint64
withhold = make(chan struct{}, 1) withhold = make(chan struct{}, 1)
msgbuf = make(chan dOp, 64) // these ops are tiny so a large buffer is allocated for withholding output msgbuf = make(chan dOp, 64) // these ops are tiny so a large buffer is allocated for withholding output
@ -29,6 +30,25 @@ func dequeue() {
}() }()
} }
// queue submits ops to msgbuf but drops messages
// when the buffer is full and dequeue is withholding
func queue(op dOp) {
select {
case msgbuf <- op:
queueSync.Add(1)
default:
// send the op anyway if not withholding
// as dequeue will get to it eventually
if !wstate.Load() {
queueSync.Add(1)
msgbuf <- op
} else {
// increment dropped message count
dropped.Add(1)
}
}
}
type dOp interface{ Do() } type dOp interface{ Do() }
func Exit(code int) { func Exit(code int) {
@ -47,6 +67,9 @@ func Resume() {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(true, false) { if wstate.CompareAndSwap(true, false) {
withhold <- struct{}{} withhold <- struct{}{}
if d := dropped.Swap(0); d != 0 {
Printf("dropped %d messages during withhold", d)
}
} }
} }

View File

@ -16,20 +16,17 @@ func SetPrefix(prefix string) {
func Print(v ...any) { func Print(v ...any) {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
queueSync.Add(1) queue(dPrint(v))
msgbuf <- dPrint(v)
} }
func Printf(format string, v ...any) { func Printf(format string, v ...any) {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
queueSync.Add(1) queue(&dPrintf{format, v})
msgbuf <- &dPrintf{format, v}
} }
func Println(v ...any) { func Println(v ...any) {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
queueSync.Add(1) queue(dPrintln(v))
msgbuf <- dPrintln(v)
} }
func Fatal(v ...any) { func Fatal(v ...any) {

View File

@ -1,14 +1,10 @@
package internal package linux
import ( import (
"errors"
"io/fs" "io/fs"
"os"
"os/exec"
"os/user" "os/user"
"path" "path"
"strconv" "strconv"
"sync"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
@ -36,6 +32,8 @@ type System interface {
// Exit provides [os.Exit]. // Exit provides [os.Exit].
Exit(code int) Exit(code int)
// FshimPath returns an absolute path to the fshim binary.
FshimPath() string
// Paths returns a populated [Paths] struct. // Paths returns a populated [Paths] struct.
Paths() Paths Paths() Paths
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html // SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
@ -69,58 +67,3 @@ func CopyPaths(os System, v *Paths) {
fmsg.VPrintf("runtime directory at %q", v.RunDirPath) fmsg.VPrintf("runtime directory at %q", v.RunDirPath)
} }
// Std implements System using the standard library.
type Std struct {
paths Paths
pathsOnce sync.Once
sdBooted bool
sdBootedOnce sync.Once
}
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) }
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths
}
func (s *Std) SdBooted() bool {
s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() })
return s.sdBooted
}
const systemdCheckPath = "/run/systemd/system"
func copySdBooted() bool {
if v, err := sdBooted(); err != nil {
fmsg.Println("cannot read systemd marker:", err)
return false
} else {
return v
}
}
func sdBooted() (bool, error) {
_, err := os.Stat(systemdCheckPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return false, err
}
return true, nil
}

83
internal/linux/std.go Normal file
View File

@ -0,0 +1,83 @@
package linux
import (
"errors"
"io/fs"
"os"
"os/exec"
"os/user"
"sync"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// Std implements System using the standard library.
type Std struct {
paths Paths
pathsOnce sync.Once
sdBooted bool
sdBootedOnce sync.Once
fshim string
fshimOnce sync.Once
}
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) }
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) SdBooted() bool {
s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() })
return s.sdBooted
}
const systemdCheckPath = "/run/systemd/system"
func copySdBooted() bool {
if v, err := sdBooted(); err != nil {
fmsg.Println("cannot read systemd marker:", err)
return false
} else {
return v
}
}
func sdBooted() (bool, error) {
_, err := os.Stat(systemdCheckPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return false, err
}
return true, nil
}

13
internal/path.go Normal file
View File

@ -0,0 +1,13 @@
package internal
import "path"
var (
Fsu = compPoison
Fshim = compPoison
Finit = compPoison
)
func Path(p string) (string, bool) {
return p, p != compPoison && p != "" && path.IsAbs(p)
}

20
internal/prctl.go Normal file
View File

@ -0,0 +1,20 @@
package internal
import "syscall"
func PR_SET_DUMPABLE__SUID_DUMP_DISABLE() error {
// linux/sched/coredump.h
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
return errno
}
return nil
}
func PR_SET_PDEATHSIG__SIGKILL() error {
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
return errno
}
return nil
}

View File

@ -1,8 +1,11 @@
package system package system
import ( import (
"bytes"
"errors" "errors"
"os" "os"
"strings"
"sync"
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
@ -13,14 +16,14 @@ var (
) )
func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath string, system *dbus.Config) *I { func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath string, system *dbus.Config) *I {
if err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil { if _, err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil {
panic(err.Error()) panic(err.Error())
} else { } else {
return sys return sys
} }
} }
func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) error { func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(f func(msgbuf []string)), error) {
d := new(DBus) d := new(DBus)
// used by waiting goroutine to notify process exit // used by waiting goroutine to notify process exit
@ -28,7 +31,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
// session bus is mandatory // session bus is mandatory
if session == nil { if session == nil {
return fmsg.WrapError(ErrDBusConfig, return nil, fmsg.WrapError(ErrDBusConfig,
"attempted to seal message bus proxy without session bus config") "attempted to seal message bus proxy without session bus config")
} }
@ -61,13 +64,15 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
sys.ops = append(sys.ops, d) sys.ops = append(sys.ops, d)
// seal dbus proxy // seal dbus proxy
return fmsg.WrapErrorSuffix(d.proxy.Seal(session, system), d.out = &scanToFmsg{msg: new(strings.Builder)}
return d.out.F, fmsg.WrapErrorSuffix(d.proxy.Seal(session, system),
"cannot seal message bus proxy:") "cannot seal message bus proxy:")
} }
type DBus struct { type DBus struct {
proxy *dbus.Proxy proxy *dbus.Proxy
out *scanToFmsg
// whether system bus proxy is enabled // whether system bus proxy is enabled
system bool system bool
// notification from goroutine waiting for dbus.Proxy // notification from goroutine waiting for dbus.Proxy
@ -88,7 +93,7 @@ func (d *DBus) apply(_ *I) error {
ready := make(chan error, 1) ready := make(chan error, 1)
// background dbus proxy start // background dbus proxy start
if err := d.proxy.Start(ready, os.Stderr, true); err != nil { if err := d.proxy.Start(ready, d.out, true); err != nil {
return fmsg.WrapErrorSuffix(err, return fmsg.WrapErrorSuffix(err,
"cannot start message bus proxy:") "cannot start message bus proxy:")
} }
@ -164,3 +169,34 @@ func (d *DBus) Path() string {
func (d *DBus) String() string { func (d *DBus) String() string {
return d.proxy.String() return d.proxy.String()
} }
type scanToFmsg struct {
msg *strings.Builder
msgbuf []string
mu sync.RWMutex
}
func (s *scanToFmsg) Write(p []byte) (n int, err error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.write(p, 0)
}
func (s *scanToFmsg) write(p []byte, a int) (int, error) {
if i := bytes.IndexByte(p, '\n'); i == -1 {
n, _ := s.msg.Write(p)
return a + n, nil
} else {
n, _ := s.msg.Write(p[:i])
s.msgbuf = append(s.msgbuf, s.msg.String())
s.msg.Reset()
return s.write(p[i+1:], a+n+1)
}
}
func (s *scanToFmsg) F(f func(msgbuf []string)) {
s.mu.RLock()
f(s.msgbuf)
s.mu.RUnlock()
}

View File

@ -1,3 +1,4 @@
// Package ldd retrieves linker information by invoking ldd from glibc or musl and parsing its output.
package ldd package ldd
import ( import (

16
main.go
View File

@ -2,13 +2,11 @@ package main
import ( import (
"flag" "flag"
"syscall"
"git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
init0 "git.ophivana.moe/security/fortify/internal/init" "git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/shim"
) )
var ( var (
@ -19,12 +17,12 @@ func init() {
flag.BoolVar(&flagVerbose, "v", false, "Verbose output") flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
} }
var os = new(internal.Std) var os = new(linux.Std)
func main() { func main() {
// linux/sched/coredump.h if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 { fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
fmsg.Printf("fortify: cannot set SUID_DUMP_DISABLE: %s", errno.Error()) // not fatal: this program runs as the privileged user
} }
flag.Parse() flag.Parse()
@ -34,10 +32,6 @@ func main() {
fmsg.VPrintln("system booted with systemd as init system") fmsg.VPrintln("system booted with systemd as init system")
} }
// shim/init early exit
init0.Try()
shim.Try()
// root check // root check
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
fmsg.Fatal("this program must not run as root") fmsg.Fatal("this program must not run as root")

View File

@ -10,19 +10,33 @@
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.0.10"; version = "0.0.11";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
ldflags = [ ldflags =
lib.attrsets.foldlAttrs
(
ldflags: name: value:
ldflags
++ [
"-X"
"git.ophivana.moe/security/fortify/internal.${name}=${value}"
]
)
[
"-s" "-s"
"-w" "-w"
"-X" "-X"
"main.Version=v${version}" "main.Fmain=${placeholder "out"}/bin/.fortify-wrapped"
"-X" ]
"main.FortifyPath=${placeholder "out"}/bin/.fortify-wrapped" {
]; Version = "v${version}";
Fsu = "/run/wrappers/bin/fsu";
Fshim = "${placeholder "out"}/bin/.fshim";
Finit = "${placeholder "out"}/bin/.finit";
};
buildInputs = [ buildInputs = [
acl acl
@ -40,5 +54,7 @@ buildGoModule rec {
} }
mv $out/bin/fsu $out/bin/.fsu mv $out/bin/fsu $out/bin/.fsu
mv $out/bin/fshim $out/bin/.fshim
mv $out/bin/finit $out/bin/.finit
''; '';
} }

View File

@ -3,11 +3,11 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"git.ophivana.moe/security/fortify/internal"
) )
var ( var (
Version = "impure"
printVersion bool printVersion bool
) )
@ -17,7 +17,11 @@ func init() {
func tryVersion() { func tryVersion() {
if printVersion { if printVersion {
fmt.Println(Version) if v, ok := internal.Check(internal.Version); ok {
fmt.Println(v)
} else {
fmt.Println("impure")
}
os.Exit(0) os.Exit(0)
} }
} }

View File

@ -1,3 +1,4 @@
// Package xcb implements X11 ChangeHosts via libxcb.
package xcb package xcb
//#include <stdlib.h> //#include <stdlib.h>