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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user