app/instance: wrap internal implementation
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m44s
Test / Fortify (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 2m59s
Test / Fpkg (push) Successful in 3m34s
Test / Fortify (race detector) (push) Successful in 4m6s
Test / Flake checks (push) Successful in 59s
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m44s
Test / Fortify (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 2m59s
Test / Fpkg (push) Successful in 3m34s
Test / Fortify (race detector) (push) Successful in 4m6s
Test / Flake checks (push) Successful in 59s
This reduces the scope of the fst package, which was growing questionably large. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
parent
0d7c1a9a43
commit
6309469e93
@ -13,7 +13,7 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/command"
|
"git.gensokyo.uk/security/fortify/command"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
"git.gensokyo.uk/security/fortify/internal/app/setuid"
|
"git.gensokyo.uk/security/fortify/internal/app/instance"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
@ -62,7 +62,7 @@ func main() {
|
|||||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||||
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
|
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
|
||||||
|
|
||||||
c.Command("shim", command.UsageInternal, func([]string) error { setuid.ShimMain(); return errSuccess })
|
c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
|
||||||
|
|
||||||
{
|
{
|
||||||
var (
|
var (
|
||||||
|
@ -5,20 +5,21 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/internal/app/setuid"
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app/instance"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
|
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
|
||||||
rs := new(fst.RunState)
|
rs := new(app.RunState)
|
||||||
a := setuid.MustNew(ctx, std)
|
a := instance.MustNew(instance.ISetuid, ctx, std)
|
||||||
|
|
||||||
var code int
|
var code int
|
||||||
if sa, err := a.Seal(config); err != nil {
|
if sa, err := a.Seal(config); err != nil {
|
||||||
fmsg.PrintBaseError(err, "cannot seal app:")
|
fmsg.PrintBaseError(err, "cannot seal app:")
|
||||||
code = 1
|
code = 1
|
||||||
} else {
|
} else {
|
||||||
code = setuid.PrintRunStateErr(rs, sa.Run(rs))
|
code = instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs))
|
||||||
}
|
}
|
||||||
|
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// Package fst exports shared fortify types.
|
||||||
package fst
|
package fst
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
192
fst/sandbox.go
192
fst/sandbox.go
@ -1,16 +1,6 @@
|
|||||||
package fst
|
package fst
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"maps"
|
|
||||||
"path"
|
|
||||||
"slices"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
|
||||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,18 +47,6 @@ type (
|
|||||||
Cover []string `json:"cover"`
|
Cover []string `json:"cover"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
|
|
||||||
SandboxSys interface {
|
|
||||||
Getuid() int
|
|
||||||
Getgid() int
|
|
||||||
Paths() Paths
|
|
||||||
ReadDir(name string) ([]fs.DirEntry, error)
|
|
||||||
EvalSymlinks(path string) (string, error)
|
|
||||||
|
|
||||||
Println(v ...any)
|
|
||||||
Printf(format string, v ...any)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilesystemConfig is a representation of [sandbox.BindMount].
|
// FilesystemConfig is a representation of [sandbox.BindMount].
|
||||||
FilesystemConfig struct {
|
FilesystemConfig struct {
|
||||||
// mount point in container, same as src if empty
|
// mount point in container, same as src if empty
|
||||||
@ -83,173 +61,3 @@ type (
|
|||||||
Must bool `json:"require,omitempty"`
|
Must bool `json:"require,omitempty"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToContainer initialises [sandbox.Params] via [SandboxConfig].
|
|
||||||
// Note that remaining container setup must be queued by the [App] implementation.
|
|
||||||
func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
|
|
||||||
if s == nil {
|
|
||||||
return nil, nil, syscall.EBADE
|
|
||||||
}
|
|
||||||
|
|
||||||
container := &sandbox.Params{
|
|
||||||
Hostname: s.Hostname,
|
|
||||||
Ops: new(sandbox.Ops),
|
|
||||||
Seccomp: s.Seccomp,
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Multiarch {
|
|
||||||
container.Seccomp |= seccomp.FilterMultiarch
|
|
||||||
}
|
|
||||||
|
|
||||||
/* this is only 4 KiB of memory on a 64-bit system,
|
|
||||||
permissive defaults on NixOS results in around 100 entries
|
|
||||||
so this capacity should eliminate copies for most setups */
|
|
||||||
*container.Ops = slices.Grow(*container.Ops, 1<<8)
|
|
||||||
|
|
||||||
if s.Devel {
|
|
||||||
container.Flags |= sandbox.FAllowDevel
|
|
||||||
}
|
|
||||||
if s.Userns {
|
|
||||||
container.Flags |= sandbox.FAllowUserns
|
|
||||||
}
|
|
||||||
if s.Net {
|
|
||||||
container.Flags |= sandbox.FAllowNet
|
|
||||||
}
|
|
||||||
if s.Tty {
|
|
||||||
container.Flags |= sandbox.FAllowTTY
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.MapRealUID {
|
|
||||||
/* some programs fail to connect to dbus session running as a different uid
|
|
||||||
so this workaround is introduced to map priv-side caller uid in container */
|
|
||||||
container.Uid = sys.Getuid()
|
|
||||||
*uid = container.Uid
|
|
||||||
container.Gid = sys.Getgid()
|
|
||||||
*gid = container.Gid
|
|
||||||
} else {
|
|
||||||
*uid = sandbox.OverflowUid()
|
|
||||||
*gid = sandbox.OverflowGid()
|
|
||||||
}
|
|
||||||
|
|
||||||
container.
|
|
||||||
Proc("/proc").
|
|
||||||
Tmpfs(Tmp, 1<<12, 0755)
|
|
||||||
|
|
||||||
if !s.Device {
|
|
||||||
container.Dev("/dev").Mqueue("/dev/mqueue")
|
|
||||||
} else {
|
|
||||||
container.Bind("/dev", "/dev", sandbox.BindWritable|sandbox.BindDevice)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* retrieve paths and hide them if they're made available in the sandbox;
|
|
||||||
this feature tries to improve user experience of permissive defaults, and
|
|
||||||
to warn about issues in custom configuration; it is NOT a security feature
|
|
||||||
and should not be treated as such, ALWAYS be careful with what you bind */
|
|
||||||
var hidePaths []string
|
|
||||||
sc := sys.Paths()
|
|
||||||
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
|
|
||||||
_, systemBusAddr := dbus.Address()
|
|
||||||
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
} else {
|
|
||||||
// there is usually only one, do not preallocate
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.Method != "unix" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, pair := range entry.Values {
|
|
||||||
if pair[0] == "path" {
|
|
||||||
if path.IsAbs(pair[1]) {
|
|
||||||
// get parent dir of socket
|
|
||||||
dir := path.Dir(pair[1])
|
|
||||||
if dir == "." || dir == "/" {
|
|
||||||
sys.Printf("dbus socket %q is in an unusual location", pair[1])
|
|
||||||
}
|
|
||||||
hidePaths = append(hidePaths, dir)
|
|
||||||
} else {
|
|
||||||
sys.Printf("dbus socket %q is not absolute", pair[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hidePathMatch := make([]bool, len(hidePaths))
|
|
||||||
for i := range hidePaths {
|
|
||||||
if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range s.Filesystem {
|
|
||||||
if c == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !path.IsAbs(c.Src) {
|
|
||||||
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
|
|
||||||
}
|
|
||||||
|
|
||||||
dest := c.Dst
|
|
||||||
if c.Dst == "" {
|
|
||||||
dest = c.Src
|
|
||||||
} else if !path.IsAbs(dest) {
|
|
||||||
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
srcH := c.Src
|
|
||||||
if err := evalSymlinks(sys, &srcH); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range hidePaths {
|
|
||||||
// skip matched entries
|
|
||||||
if hidePathMatch[i] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
} else if ok {
|
|
||||||
hidePathMatch[i] = true
|
|
||||||
sys.Printf("hiding paths from %q", c.Src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var flags int
|
|
||||||
if c.Write {
|
|
||||||
flags |= sandbox.BindWritable
|
|
||||||
}
|
|
||||||
if c.Device {
|
|
||||||
flags |= sandbox.BindDevice | sandbox.BindWritable
|
|
||||||
}
|
|
||||||
if !c.Must {
|
|
||||||
flags |= sandbox.BindOptional
|
|
||||||
}
|
|
||||||
container.Bind(c.Src, dest, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cover matched paths
|
|
||||||
for i, ok := range hidePathMatch {
|
|
||||||
if ok {
|
|
||||||
container.Tmpfs(hidePaths[i], 1<<13, 0755)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range s.Link {
|
|
||||||
container.Link(l[0], l[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return container, maps.Clone(s.Env), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func evalSymlinks(sys SandboxSys, v *string) error {
|
|
||||||
if p, err := sys.EvalSymlinks(*v); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sys.Printf("path %q does not yet exist", *v)
|
|
||||||
} else {
|
|
||||||
*v = p
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
// Package fst exports shared fortify types.
|
// Package app defines the generic [App] interface.
|
||||||
package fst
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App interface {
|
type App interface {
|
||||||
// ID returns a copy of [fst.ID] held by App.
|
// ID returns a copy of [ID] held by App.
|
||||||
ID() ID
|
ID() ID
|
||||||
|
|
||||||
// Seal determines the outcome of config as a [SealedApp].
|
// Seal determines the outcome of config as a [SealedApp].
|
||||||
// The value of config might be overwritten and must not be used again.
|
// The value of config might be overwritten and must not be used again.
|
||||||
Seal(config *Config) (SealedApp, error)
|
Seal(config *fst.Config) (SealedApp, error)
|
||||||
|
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package fst
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
@ -1,22 +1,22 @@
|
|||||||
package fst_test
|
package app_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseAppID(t *testing.T) {
|
func TestParseAppID(t *testing.T) {
|
||||||
t.Run("bad length", func(t *testing.T) {
|
t.Run("bad length", func(t *testing.T) {
|
||||||
if err := fst.ParseAppID(new(fst.ID), "meow"); !errors.Is(err, fst.ErrInvalidLength) {
|
if err := ParseAppID(new(ID), "meow"); !errors.Is(err, ErrInvalidLength) {
|
||||||
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, fst.ErrInvalidLength)
|
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, ErrInvalidLength)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bad byte", func(t *testing.T) {
|
t.Run("bad byte", func(t *testing.T) {
|
||||||
wantErr := "invalid char '\\n' at byte 15"
|
wantErr := "invalid char '\\n' at byte 15"
|
||||||
if err := fst.ParseAppID(new(fst.ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr {
|
if err := ParseAppID(new(ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr {
|
||||||
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr)
|
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -30,30 +30,30 @@ func TestParseAppID(t *testing.T) {
|
|||||||
|
|
||||||
func FuzzParseAppID(f *testing.F) {
|
func FuzzParseAppID(f *testing.F) {
|
||||||
for i := 0; i < 16; i++ {
|
for i := 0; i < 16; i++ {
|
||||||
id := new(fst.ID)
|
id := new(ID)
|
||||||
if err := fst.NewAppID(id); err != nil {
|
if err := NewAppID(id); err != nil {
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
}
|
}
|
||||||
f.Add(id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], id[12], id[13], id[14], id[15])
|
f.Add(id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], id[12], id[13], id[14], id[15])
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 byte) {
|
f.Fuzz(func(t *testing.T, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 byte) {
|
||||||
testParseAppID(t, &fst.ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15})
|
testParseAppID(t, &ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testParseAppIDWithRandom(t *testing.T) {
|
func testParseAppIDWithRandom(t *testing.T) {
|
||||||
id := new(fst.ID)
|
id := new(ID)
|
||||||
if err := fst.NewAppID(id); err != nil {
|
if err := NewAppID(id); err != nil {
|
||||||
t.Fatalf("cannot generate app ID: %v", err)
|
t.Fatalf("cannot generate app ID: %v", err)
|
||||||
}
|
}
|
||||||
testParseAppID(t, id)
|
testParseAppID(t, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testParseAppID(t *testing.T, id *fst.ID) {
|
func testParseAppID(t *testing.T, id *ID) {
|
||||||
s := id.String()
|
s := id.String()
|
||||||
got := new(fst.ID)
|
got := new(ID)
|
||||||
if err := fst.ParseAppID(got, s); err != nil {
|
if err := ParseAppID(got, s); err != nil {
|
||||||
t.Fatalf("cannot parse app ID: %v", err)
|
t.Fatalf("cannot parse app ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
187
internal/app/instance/common/container.go
Normal file
187
internal/app/instance/common/container.go
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"maps"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewContainer initialises [sandbox.Params] via [fst.SandboxConfig].
|
||||||
|
// Note that remaining container setup must be queued by the caller.
|
||||||
|
func NewContainer(s *fst.SandboxConfig, os sys.State, uid, gid *int) (*sandbox.Params, map[string]string, error) {
|
||||||
|
if s == nil {
|
||||||
|
return nil, nil, syscall.EBADE
|
||||||
|
}
|
||||||
|
|
||||||
|
container := &sandbox.Params{
|
||||||
|
Hostname: s.Hostname,
|
||||||
|
Ops: new(sandbox.Ops),
|
||||||
|
Seccomp: s.Seccomp,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Multiarch {
|
||||||
|
container.Seccomp |= seccomp.FilterMultiarch
|
||||||
|
}
|
||||||
|
|
||||||
|
/* this is only 4 KiB of memory on a 64-bit system,
|
||||||
|
permissive defaults on NixOS results in around 100 entries
|
||||||
|
so this capacity should eliminate copies for most setups */
|
||||||
|
*container.Ops = slices.Grow(*container.Ops, 1<<8)
|
||||||
|
|
||||||
|
if s.Devel {
|
||||||
|
container.Flags |= sandbox.FAllowDevel
|
||||||
|
}
|
||||||
|
if s.Userns {
|
||||||
|
container.Flags |= sandbox.FAllowUserns
|
||||||
|
}
|
||||||
|
if s.Net {
|
||||||
|
container.Flags |= sandbox.FAllowNet
|
||||||
|
}
|
||||||
|
if s.Tty {
|
||||||
|
container.Flags |= sandbox.FAllowTTY
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.MapRealUID {
|
||||||
|
/* some programs fail to connect to dbus session running as a different uid
|
||||||
|
so this workaround is introduced to map priv-side caller uid in container */
|
||||||
|
container.Uid = os.Getuid()
|
||||||
|
*uid = container.Uid
|
||||||
|
container.Gid = os.Getgid()
|
||||||
|
*gid = container.Gid
|
||||||
|
} else {
|
||||||
|
*uid = sandbox.OverflowUid()
|
||||||
|
*gid = sandbox.OverflowGid()
|
||||||
|
}
|
||||||
|
|
||||||
|
container.
|
||||||
|
Proc("/proc").
|
||||||
|
Tmpfs(fst.Tmp, 1<<12, 0755)
|
||||||
|
|
||||||
|
if !s.Device {
|
||||||
|
container.Dev("/dev").Mqueue("/dev/mqueue")
|
||||||
|
} else {
|
||||||
|
container.Bind("/dev", "/dev", sandbox.BindWritable|sandbox.BindDevice)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* retrieve paths and hide them if they're made available in the sandbox;
|
||||||
|
this feature tries to improve user experience of permissive defaults, and
|
||||||
|
to warn about issues in custom configuration; it is NOT a security feature
|
||||||
|
and should not be treated as such, ALWAYS be careful with what you bind */
|
||||||
|
var hidePaths []string
|
||||||
|
sc := os.Paths()
|
||||||
|
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
|
||||||
|
_, systemBusAddr := dbus.Address()
|
||||||
|
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else {
|
||||||
|
// there is usually only one, do not preallocate
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Method != "unix" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, pair := range entry.Values {
|
||||||
|
if pair[0] == "path" {
|
||||||
|
if path.IsAbs(pair[1]) {
|
||||||
|
// get parent dir of socket
|
||||||
|
dir := path.Dir(pair[1])
|
||||||
|
if dir == "." || dir == "/" {
|
||||||
|
os.Printf("dbus socket %q is in an unusual location", pair[1])
|
||||||
|
}
|
||||||
|
hidePaths = append(hidePaths, dir)
|
||||||
|
} else {
|
||||||
|
os.Printf("dbus socket %q is not absolute", pair[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hidePathMatch := make([]bool, len(hidePaths))
|
||||||
|
for i := range hidePaths {
|
||||||
|
if err := evalSymlinks(os, &hidePaths[i]); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range s.Filesystem {
|
||||||
|
if c == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !path.IsAbs(c.Src) {
|
||||||
|
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := c.Dst
|
||||||
|
if c.Dst == "" {
|
||||||
|
dest = c.Src
|
||||||
|
} else if !path.IsAbs(dest) {
|
||||||
|
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcH := c.Src
|
||||||
|
if err := evalSymlinks(os, &srcH); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range hidePaths {
|
||||||
|
// skip matched entries
|
||||||
|
if hidePathMatch[i] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else if ok {
|
||||||
|
hidePathMatch[i] = true
|
||||||
|
os.Printf("hiding paths from %q", c.Src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags int
|
||||||
|
if c.Write {
|
||||||
|
flags |= sandbox.BindWritable
|
||||||
|
}
|
||||||
|
if c.Device {
|
||||||
|
flags |= sandbox.BindDevice | sandbox.BindWritable
|
||||||
|
}
|
||||||
|
if !c.Must {
|
||||||
|
flags |= sandbox.BindOptional
|
||||||
|
}
|
||||||
|
container.Bind(c.Src, dest, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cover matched paths
|
||||||
|
for i, ok := range hidePathMatch {
|
||||||
|
if ok {
|
||||||
|
container.Tmpfs(hidePaths[i], 1<<13, 0755)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, l := range s.Link {
|
||||||
|
container.Link(l[0], l[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return container, maps.Clone(s.Env), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func evalSymlinks(os sys.State, v *string) error {
|
||||||
|
if p, err := os.EvalSymlinks(*v); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
os.Printf("path %q does not yet exist", *v)
|
||||||
|
} else {
|
||||||
|
*v = p
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package fst
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
@ -1,4 +1,4 @@
|
|||||||
package fst
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
17
internal/app/instance/errors.go
Normal file
17
internal/app/instance/errors.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package instance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintRunStateErr(whence int, rs *app.RunState, runErr error) (code int) {
|
||||||
|
switch whence {
|
||||||
|
case ISetuid:
|
||||||
|
return setuid.PrintRunStateErr(rs, runErr)
|
||||||
|
default:
|
||||||
|
panic(syscall.EINVAL)
|
||||||
|
}
|
||||||
|
}
|
33
internal/app/instance/new.go
Normal file
33
internal/app/instance/new.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Package instance exposes cross-package implementation details and provides constructors for builtin implementations.
|
||||||
|
package instance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ISetuid = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(whence int, ctx context.Context, os sys.State) (app.App, error) {
|
||||||
|
switch whence {
|
||||||
|
case ISetuid:
|
||||||
|
return setuid.New(ctx, os)
|
||||||
|
default:
|
||||||
|
return nil, syscall.EINVAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustNew(whence int, ctx context.Context, os sys.State) app.App {
|
||||||
|
a, err := New(whence, ctx, os)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("cannot create app: %v", err)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
6
internal/app/instance/shim.go
Normal file
6
internal/app/instance/shim.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package instance
|
||||||
|
|
||||||
|
import "git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
|
||||||
|
|
||||||
|
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
|
||||||
|
func ShimMain() { setuid.ShimMain() }
|
@ -3,36 +3,28 @@ package setuid
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(ctx context.Context, os sys.State) (fst.App, error) {
|
func New(ctx context.Context, os sys.State) (App, error) {
|
||||||
a := new(app)
|
a := new(app)
|
||||||
a.sys = os
|
a.sys = os
|
||||||
a.ctx = ctx
|
a.ctx = ctx
|
||||||
|
|
||||||
id := new(fst.ID)
|
id := new(ID)
|
||||||
err := fst.NewAppID(id)
|
err := NewAppID(id)
|
||||||
a.id = newID(id)
|
a.id = newID(id)
|
||||||
|
|
||||||
return a, err
|
return a, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustNew(ctx context.Context, os sys.State) fst.App {
|
|
||||||
a, err := New(ctx, os)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("cannot create app: %v", err)
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
type app struct {
|
type app struct {
|
||||||
id *stringPair[fst.ID]
|
id *stringPair[ID]
|
||||||
sys sys.State
|
sys sys.State
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
@ -40,7 +32,7 @@ type app struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) ID() fst.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
|
func (a *app) ID() ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
|
||||||
|
|
||||||
func (a *app) String() string {
|
func (a *app) String() string {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
@ -60,7 +52,7 @@ func (a *app) String() string {
|
|||||||
return fmt.Sprintf("(unsealed app %s)", a.id)
|
return fmt.Sprintf("(unsealed app %s)", a.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
|
func (a *app) Seal(config *fst.Config) (SealedApp, error) {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/acl"
|
"git.gensokyo.uk/security/fortify/acl"
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
"git.gensokyo.uk/security/fortify/system"
|
||||||
)
|
)
|
||||||
@ -48,7 +49,7 @@ var testCasesNixos = []sealTestCase{
|
|||||||
Enablements: system.EWayland | system.EDBus | system.EPulse,
|
Enablements: system.EWayland | system.EDBus | system.EPulse,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fst.ID{
|
app.ID{
|
||||||
0x8e, 0x2c, 0x76, 0xb0,
|
0x8e, 0x2c, 0x76, 0xb0,
|
||||||
0x66, 0xda, 0xbe, 0x57,
|
0x66, 0xda, 0xbe, 0x57,
|
||||||
0x4c, 0xf0, 0x73, 0xbd,
|
0x4c, 0xf0, 0x73, 0xbd,
|
@ -6,6 +6,7 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/acl"
|
"git.gensokyo.uk/security/fortify/acl"
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
"git.gensokyo.uk/security/fortify/system"
|
||||||
)
|
)
|
||||||
@ -20,7 +21,7 @@ var testCasesPd = []sealTestCase{
|
|||||||
Outer: "/home/chronos",
|
Outer: "/home/chronos",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fst.ID{
|
app.ID{
|
||||||
0x4a, 0x45, 0x0b, 0x65,
|
0x4a, 0x45, 0x0b, 0x65,
|
||||||
0x96, 0xd7, 0xbc, 0x15,
|
0x96, 0xd7, 0xbc, 0x15,
|
||||||
0xbd, 0x01, 0x78, 0x0e,
|
0xbd, 0x01, 0x78, 0x0e,
|
||||||
@ -117,7 +118,7 @@ var testCasesPd = []sealTestCase{
|
|||||||
Enablements: system.EWayland | system.EDBus | system.EPulse,
|
Enablements: system.EWayland | system.EDBus | system.EPulse,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fst.ID{
|
app.ID{
|
||||||
0xeb, 0xf0, 0x83, 0xd1,
|
0xeb, 0xf0, 0x83, 0xd1,
|
||||||
0xb1, 0x75, 0x91, 0x17,
|
0xb1, 0x75, 0x91, 0x17,
|
||||||
0x82, 0xd4, 0x13, 0x36,
|
0x82, 0xd4, 0x13, 0x36,
|
@ -7,7 +7,7 @@ import (
|
|||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fs methods are not implemented using a real FS
|
// fs methods are not implemented using a real FS
|
||||||
@ -125,8 +125,8 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubNixOS) Paths() fst.Paths {
|
func (s *stubNixOS) Paths() app.Paths {
|
||||||
return fst.Paths{
|
return app.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",
|
@ -8,7 +8,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/internal/app/setuid"
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
"git.gensokyo.uk/security/fortify/system"
|
||||||
@ -18,7 +19,7 @@ type sealTestCase struct {
|
|||||||
name string
|
name string
|
||||||
os sys.State
|
os sys.State
|
||||||
config *fst.Config
|
config *fst.Config
|
||||||
id fst.ID
|
id app.ID
|
||||||
wantSys *system.I
|
wantSys *system.I
|
||||||
wantContainer *sandbox.Params
|
wantContainer *sandbox.Params
|
||||||
}
|
}
|
@ -4,11 +4,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PrintRunStateErr(rs *fst.RunState, runErr error) (code int) {
|
func PrintRunStateErr(rs *RunState, runErr error) (code int) {
|
||||||
code = rs.ExitStatus()
|
code = rs.ExitStatus()
|
||||||
|
|
||||||
if runErr != nil {
|
if runErr != nil {
|
@ -1,20 +1,20 @@
|
|||||||
package setuid
|
package setuid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
"git.gensokyo.uk/security/fortify/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewWithID(id fst.ID, os sys.State) fst.App {
|
func NewWithID(id ID, os sys.State) App {
|
||||||
a := new(app)
|
a := new(app)
|
||||||
a.id = newID(&id)
|
a.id = newID(&id)
|
||||||
a.sys = os
|
a.sys = os
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) {
|
func AppIParams(a App, sa SealedApp) (*system.I, *sandbox.Params) {
|
||||||
v := a.(*app)
|
v := a.(*app)
|
||||||
seal := sa.(*outcome)
|
seal := sa.(*outcome)
|
||||||
if v.outcome != seal || v.id != seal.id {
|
if v.outcome != seal || v.id != seal.id {
|
@ -12,8 +12,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
|
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/internal/state"
|
"git.gensokyo.uk/security/fortify/internal/state"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
@ -22,7 +22,7 @@ import (
|
|||||||
|
|
||||||
const shimWaitTimeout = 5 * time.Second
|
const shimWaitTimeout = 5 * time.Second
|
||||||
|
|
||||||
func (seal *outcome) Run(rs *fst.RunState) error {
|
func (seal *outcome) Run(rs *RunState) error {
|
||||||
if !seal.f.CompareAndSwap(false, true) {
|
if !seal.f.CompareAndSwap(false, true) {
|
||||||
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
|
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
|
||||||
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
|
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
|
@ -20,6 +20,8 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
|
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app/instance/common"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
@ -64,7 +66,7 @@ var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z
|
|||||||
// outcome stores copies of various parts of [fst.Config]
|
// outcome stores copies of various parts of [fst.Config]
|
||||||
type outcome struct {
|
type outcome struct {
|
||||||
// copied from initialising [app]
|
// copied from initialising [app]
|
||||||
id *stringPair[fst.ID]
|
id *stringPair[ID]
|
||||||
// copied from [sys.State] response
|
// copied from [sys.State] response
|
||||||
runDirPath string
|
runDirPath string
|
||||||
|
|
||||||
@ -95,7 +97,7 @@ type shareHost struct {
|
|||||||
runtimeSharePath string
|
runtimeSharePath string
|
||||||
|
|
||||||
seal *outcome
|
seal *outcome
|
||||||
sc fst.Paths
|
sc Paths
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
|
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
|
||||||
@ -279,7 +281,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
|||||||
{
|
{
|
||||||
var uid, gid int
|
var uid, gid int
|
||||||
var err error
|
var err error
|
||||||
seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid)
|
seal.container, seal.env, err = common.NewContainer(config.Confinement.Sandbox, sys, &uid, &gid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmsg.WrapErrorSuffix(err,
|
return fmsg.WrapErrorSuffix(err,
|
||||||
"cannot initialise container configuration:")
|
"cannot initialise container configuration:")
|
@ -3,11 +3,11 @@ package setuid
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
|
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
|
||||||
func newID(id *fst.ID) *stringPair[fst.ID] { return &stringPair[fst.ID]{*id, id.String()} }
|
func newID(id *ID) *stringPair[ID] { return &stringPair[ID]{*id, id.String()} }
|
||||||
|
|
||||||
// stringPair stores a value and its string representation.
|
// stringPair stores a value and its string representation.
|
||||||
type stringPair[T comparable] struct {
|
type stringPair[T comparable] struct {
|
@ -14,6 +14,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ type multiBackend struct {
|
|||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *multiBackend) filename(id *fst.ID) string {
|
func (b *multiBackend) filename(id *app.ID) string {
|
||||||
return path.Join(b.path, id.String())
|
return path.Join(b.path, id.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,8 +190,8 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
|
|||||||
return nil, fmt.Errorf("unexpected directory %q in store", e.Name())
|
return nil, fmt.Errorf("unexpected directory %q in store", e.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
id := new(fst.ID)
|
id := new(app.ID)
|
||||||
if err := fst.ParseAppID(id, e.Name()); err != nil {
|
if err := app.ParseAppID(id, e.Name()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,7 +336,7 @@ func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *multiBackend) Destroy(id fst.ID) error {
|
func (b *multiBackend) Destroy(id app.ID) error {
|
||||||
b.lock.Lock()
|
b.lock.Lock()
|
||||||
defer b.lock.Unlock()
|
defer b.lock.Unlock()
|
||||||
|
|
||||||
|
@ -6,11 +6,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNoConfig = errors.New("state does not contain config")
|
var ErrNoConfig = errors.New("state does not contain config")
|
||||||
|
|
||||||
type Entries map[fst.ID]*State
|
type Entries map[app.ID]*State
|
||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
// Do calls f exactly once and ensures store exclusivity until f returns.
|
// Do calls f exactly once and ensures store exclusivity until f returns.
|
||||||
@ -29,7 +30,7 @@ type Store interface {
|
|||||||
// Cursor provides access to the store
|
// Cursor provides access to the store
|
||||||
type Cursor interface {
|
type Cursor interface {
|
||||||
Save(state *State, configWriter io.WriterTo) error
|
Save(state *State, configWriter io.WriterTo) error
|
||||||
Destroy(id fst.ID) error
|
Destroy(id app.ID) error
|
||||||
Load() (Entries, error)
|
Load() (Entries, error)
|
||||||
Len() (int, error)
|
Len() (int, error)
|
||||||
}
|
}
|
||||||
@ -37,7 +38,7 @@ type Cursor interface {
|
|||||||
// State is a fortify process's state
|
// State is a fortify process's state
|
||||||
type State struct {
|
type State struct {
|
||||||
// fortify instance id
|
// fortify instance id
|
||||||
ID fst.ID `json:"instance"`
|
ID app.ID `json:"instance"`
|
||||||
// child process PID value
|
// child process PID value
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
// sealed app configuration
|
// sealed app configuration
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/state"
|
"git.gensokyo.uk/security/fortify/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ func testStore(t *testing.T, s state.Store) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeState(t *testing.T, s *state.State, ct io.Writer) {
|
func makeState(t *testing.T, s *state.State, ct io.Writer) {
|
||||||
if err := fst.NewAppID(&s.ID); err != nil {
|
if err := app.NewAppID(&s.ID); err != nil {
|
||||||
t.Fatalf("cannot create dummy state: %v", err)
|
t.Fatalf("cannot create dummy state: %v", err)
|
||||||
}
|
}
|
||||||
if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil {
|
if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil {
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,14 +41,14 @@ type State interface {
|
|||||||
Printf(format string, v ...any)
|
Printf(format string, v ...any)
|
||||||
|
|
||||||
// Paths returns a populated [Paths] struct.
|
// Paths returns a populated [Paths] struct.
|
||||||
Paths() fst.Paths
|
Paths() app.Paths
|
||||||
// Uid invokes fsu and returns target uid.
|
// Uid invokes fsu and returns target uid.
|
||||||
// Any errors returned by Uid is already wrapped [fmsg.BaseError].
|
// Any errors returned by Uid is already wrapped [fmsg.BaseError].
|
||||||
Uid(aid int) (int, error)
|
Uid(aid int) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyPaths is a generic implementation of [fst.Paths].
|
// CopyPaths is a generic implementation of [fst.Paths].
|
||||||
func CopyPaths(os State, v *fst.Paths) {
|
func CopyPaths(os State, v *app.Paths) {
|
||||||
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
|
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
|
||||||
|
|
||||||
fmsg.Verbosef("process share directory at %q", v.SharePath)
|
fmsg.Verbosef("process share directory at %q", v.SharePath)
|
||||||
|
@ -12,15 +12,15 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Std implements System using the standard library.
|
// Std implements System using the standard library.
|
||||||
type Std struct {
|
type Std struct {
|
||||||
paths fst.Paths
|
paths app.Paths
|
||||||
pathsOnce sync.Once
|
pathsOnce sync.Once
|
||||||
|
|
||||||
uidOnce sync.Once
|
uidOnce sync.Once
|
||||||
@ -48,7 +48,7 @@ func (s *Std) Printf(format string, v ...any) { fmsg.Verbosef(form
|
|||||||
|
|
||||||
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||||
|
|
||||||
func (s *Std) Paths() fst.Paths {
|
func (s *Std) Paths() app.Paths {
|
||||||
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
|
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
|
||||||
return s.paths
|
return s.paths
|
||||||
}
|
}
|
||||||
|
15
main.go
15
main.go
@ -19,7 +19,8 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
"git.gensokyo.uk/security/fortify/internal/app/setuid"
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app/instance"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/internal/state"
|
"git.gensokyo.uk/security/fortify/internal/state"
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
@ -73,7 +74,7 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
|
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
|
||||||
|
|
||||||
c.Command("shim", command.UsageInternal, func([]string) error { setuid.ShimMain(); return errSuccess })
|
c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
|
||||||
|
|
||||||
c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
|
c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
@ -239,11 +240,11 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
|
|
||||||
case 1: // instance
|
case 1: // instance
|
||||||
name := args[0]
|
name := args[0]
|
||||||
config, instance := tryShort(name)
|
config, entry := tryShort(name)
|
||||||
if config == nil {
|
if config == nil {
|
||||||
config = tryPath(name)
|
config = tryPath(name)
|
||||||
}
|
}
|
||||||
printShowInstance(os.Stdout, time.Now().UTC(), instance, config, showFlagShort, flagJSON)
|
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Fatal("show requires 1 argument")
|
log.Fatal("show requires 1 argument")
|
||||||
@ -284,14 +285,14 @@ func runApp(config *fst.Config) {
|
|||||||
ctx, stop := signal.NotifyContext(context.Background(),
|
ctx, stop := signal.NotifyContext(context.Background(),
|
||||||
syscall.SIGINT, syscall.SIGTERM)
|
syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer stop() // unreachable
|
defer stop() // unreachable
|
||||||
a := setuid.MustNew(ctx, std)
|
a := instance.MustNew(instance.ISetuid, ctx, std)
|
||||||
|
|
||||||
rs := new(fst.RunState)
|
rs := new(app.RunState)
|
||||||
if sa, err := a.Seal(config); err != nil {
|
if sa, err := a.Seal(config); err != nil {
|
||||||
fmsg.PrintBaseError(err, "cannot seal app:")
|
fmsg.PrintBaseError(err, "cannot seal app:")
|
||||||
internal.Exit(1)
|
internal.Exit(1)
|
||||||
} else {
|
} else {
|
||||||
internal.Exit(setuid.PrintRunStateErr(rs, sa.Run(rs)))
|
internal.Exit(instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs)))
|
||||||
}
|
}
|
||||||
|
|
||||||
*(*int)(nil) = 0 // not reached
|
*(*int)(nil) = 0 // not reached
|
||||||
|
6
parse.go
6
parse.go
@ -67,7 +67,7 @@ func tryFd(name string) io.ReadCloser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryShort(name string) (config *fst.Config, instance *state.State) {
|
func tryShort(name string) (config *fst.Config, entry *state.State) {
|
||||||
likePrefix := false
|
likePrefix := false
|
||||||
if len(name) <= 32 {
|
if len(name) <= 32 {
|
||||||
likePrefix = true
|
likePrefix = true
|
||||||
@ -96,8 +96,8 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
|
|||||||
v := id.String()
|
v := id.String()
|
||||||
if strings.HasPrefix(v, name) {
|
if strings.HasPrefix(v, name) {
|
||||||
// match, use config from this state entry
|
// match, use config from this state entry
|
||||||
instance = entries[id]
|
entry = entries[id]
|
||||||
config = instance.Config
|
config = entry.Config
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,11 +7,12 @@ import (
|
|||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/state"
|
"git.gensokyo.uk/security/fortify/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testID = fst.ID{
|
testID = app.ID{
|
||||||
0x8e, 0x2c, 0x76, 0xb0,
|
0x8e, 0x2c, 0x76, 0xb0,
|
||||||
0x66, 0xda, 0xbe, 0x57,
|
0x66, 0xda, 0xbe, 0x57,
|
||||||
0x4c, 0xf0, 0x73, 0xbd,
|
0x4c, 0xf0, 0x73, 0xbd,
|
||||||
@ -457,7 +458,7 @@ func Test_printPs(t *testing.T) {
|
|||||||
{"no entries", make(state.Entries), false, false, " Instance PID Application Uptime\n"},
|
{"no entries", make(state.Entries), false, false, " Instance PID Application Uptime\n"},
|
||||||
{"no entries short", make(state.Entries), true, false, ""},
|
{"no entries short", make(state.Entries), true, false, ""},
|
||||||
{"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"},
|
{"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"},
|
||||||
{"state corruption", state.Entries{fst.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
|
{"state corruption", state.Entries{app.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
|
||||||
|
|
||||||
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(fst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
|
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(fst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
|
||||||
8e2c76b0 256 0 (uk.gensokyo.fortify.8e2c76b0) 1h2m32s
|
8e2c76b0 256 0 (uk.gensokyo.fortify.8e2c76b0) 1h2m32s
|
||||||
|
Loading…
Reference in New Issue
Block a user