Compare commits
No commits in common. "b39f3aeb59095aef123920d891fecf890ce0b088" and "ec5e91b8c97f02707a70a789ba4af84d51394ea5" have entirely different histories.
b39f3aeb59
...
ec5e91b8c9
@ -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"
|
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
||||||
"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 { app.ShimMain(); return errSuccess })
|
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
|
||||||
|
|
||||||
{
|
{
|
||||||
var (
|
var (
|
||||||
@ -122,7 +122,7 @@ func main() {
|
|||||||
bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
|
bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
|
||||||
pathSet := pathSetByApp(bundle.ID)
|
pathSet := pathSetByApp(bundle.ID)
|
||||||
|
|
||||||
a := bundle
|
app := bundle
|
||||||
if s, err := os.Stat(pathSet.metaPath); err != nil {
|
if s, err := os.Stat(pathSet.metaPath); err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
cleanup()
|
cleanup()
|
||||||
@ -135,39 +135,39 @@ func main() {
|
|||||||
log.Printf("metadata path %q is not a file", pathSet.metaPath)
|
log.Printf("metadata path %q is not a file", pathSet.metaPath)
|
||||||
return syscall.EBADMSG
|
return syscall.EBADMSG
|
||||||
} else {
|
} else {
|
||||||
a = loadAppInfo(pathSet.metaPath, cleanup)
|
app = loadAppInfo(pathSet.metaPath, cleanup)
|
||||||
if a.ID != bundle.ID {
|
if app.ID != bundle.ID {
|
||||||
cleanup()
|
cleanup()
|
||||||
log.Printf("app %q claims to have identifier %q",
|
log.Printf("app %q claims to have identifier %q",
|
||||||
bundle.ID, a.ID)
|
bundle.ID, app.ID)
|
||||||
return syscall.EBADE
|
return syscall.EBADE
|
||||||
}
|
}
|
||||||
// sec: should verify credentials
|
// sec: should verify credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
if a != bundle {
|
if app != bundle {
|
||||||
// do not try to re-install
|
// do not try to re-install
|
||||||
if a.NixGL == bundle.NixGL &&
|
if app.NixGL == bundle.NixGL &&
|
||||||
a.CurrentSystem == bundle.CurrentSystem &&
|
app.CurrentSystem == bundle.CurrentSystem &&
|
||||||
a.Launcher == bundle.Launcher &&
|
app.Launcher == bundle.Launcher &&
|
||||||
a.ActivationPackage == bundle.ActivationPackage {
|
app.ActivationPackage == bundle.ActivationPackage {
|
||||||
cleanup()
|
cleanup()
|
||||||
log.Printf("package %q is identical to local application %q",
|
log.Printf("package %q is identical to local application %q",
|
||||||
pkgPath, a.ID)
|
pkgPath, app.ID)
|
||||||
return errSuccess
|
return errSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppID determines uid
|
// AppID determines uid
|
||||||
if a.AppID != bundle.AppID {
|
if app.AppID != bundle.AppID {
|
||||||
cleanup()
|
cleanup()
|
||||||
log.Printf("package %q app id %d differs from installed %d",
|
log.Printf("package %q app id %d differs from installed %d",
|
||||||
pkgPath, bundle.AppID, a.AppID)
|
pkgPath, bundle.AppID, app.AppID)
|
||||||
return syscall.EBADE
|
return syscall.EBADE
|
||||||
}
|
}
|
||||||
|
|
||||||
// sec: should compare version string
|
// sec: should compare version string
|
||||||
fmsg.Verbosef("installing application %q version %q over local %q",
|
fmsg.Verbosef("installing application %q version %q over local %q",
|
||||||
bundle.ID, bundle.Version, a.Version)
|
bundle.ID, bundle.Version, app.Version)
|
||||||
} else {
|
} else {
|
||||||
fmsg.Verbosef("application %q clean installation", bundle.ID)
|
fmsg.Verbosef("application %q clean installation", bundle.ID)
|
||||||
// sec: should install credentials
|
// sec: should install credentials
|
||||||
@ -268,9 +268,9 @@ func main() {
|
|||||||
|
|
||||||
id := args[0]
|
id := args[0]
|
||||||
pathSet := pathSetByApp(id)
|
pathSet := pathSetByApp(id)
|
||||||
a := loadAppInfo(pathSet.metaPath, func() {})
|
app := loadAppInfo(pathSet.metaPath, func() {})
|
||||||
if a.ID != id {
|
if app.ID != id {
|
||||||
log.Printf("app %q claims to have identifier %q", id, a.ID)
|
log.Printf("app %q claims to have identifier %q", id, app.ID)
|
||||||
return syscall.EBADE
|
return syscall.EBADE
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +278,7 @@ func main() {
|
|||||||
Prepare nixGL.
|
Prepare nixGL.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if a.GPU && flagAutoDrivers {
|
if app.GPU && flagAutoDrivers {
|
||||||
withNixDaemon(ctx, "nix-gl", []string{
|
withNixDaemon(ctx, "nix-gl", []string{
|
||||||
"mkdir -p /nix/.nixGL/auto",
|
"mkdir -p /nix/.nixGL/auto",
|
||||||
"rm -rf /nix/.nixGL/auto",
|
"rm -rf /nix/.nixGL/auto",
|
||||||
@ -286,11 +286,11 @@ func main() {
|
|||||||
"nix build --impure " +
|
"nix build --impure " +
|
||||||
"--out-link /nix/.nixGL/auto/opengl " +
|
"--out-link /nix/.nixGL/auto/opengl " +
|
||||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||||
"path:" + a.NixGL,
|
"path:" + app.NixGL,
|
||||||
"nix build --impure " +
|
"nix build --impure " +
|
||||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||||
"path:" + a.NixGL + "#nixVulkanNvidia",
|
"path:" + app.NixGL + "#nixVulkanNvidia",
|
||||||
}, true, func(config *fst.Config) *fst.Config {
|
}, true, func(config *fst.Config) *fst.Config {
|
||||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
||||||
{Src: "/etc/resolv.conf"},
|
{Src: "/etc/resolv.conf"},
|
||||||
@ -302,7 +302,7 @@ func main() {
|
|||||||
}...)
|
}...)
|
||||||
appendGPUFilesystem(config)
|
appendGPUFilesystem(config)
|
||||||
return config
|
return config
|
||||||
}, a, pathSet, flagDropShellNixGL, func() {})
|
}, app, pathSet, flagDropShellNixGL, func() {})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -311,19 +311,19 @@ func main() {
|
|||||||
|
|
||||||
argv := make([]string, 1, len(args))
|
argv := make([]string, 1, len(args))
|
||||||
if !flagDropShell {
|
if !flagDropShell {
|
||||||
argv[0] = a.Launcher
|
argv[0] = app.Launcher
|
||||||
} else {
|
} else {
|
||||||
argv[0] = shellPath
|
argv[0] = shellPath
|
||||||
}
|
}
|
||||||
argv = append(argv, args[1:]...)
|
argv = append(argv, args[1:]...)
|
||||||
|
|
||||||
config := a.toFst(pathSet, argv, flagDropShell)
|
config := app.toFst(pathSet, argv, flagDropShell)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Expose GPU devices.
|
Expose GPU devices.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if a.GPU {
|
if app.GPU {
|
||||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
|
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
|
||||||
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
|
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
|
||||||
appendGPUFilesystem(config)
|
appendGPUFilesystem(config)
|
||||||
|
@ -116,6 +116,7 @@
|
|||||||
|
|
||||||
# appPackages
|
# appPackages
|
||||||
glibc
|
glibc
|
||||||
|
bubblewrap
|
||||||
xdg-dbus-proxy
|
xdg-dbus-proxy
|
||||||
|
|
||||||
# fpkg
|
# fpkg
|
||||||
|
72
helper/bwrap.go
Normal file
72
helper/bwrap.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BubblewrapName is the file name or path to bubblewrap.
|
||||||
|
var BubblewrapName = "bwrap"
|
||||||
|
|
||||||
|
// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
|
||||||
|
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
||||||
|
// Function argF returns an array of arguments passed directly to the child process.
|
||||||
|
func MustNewBwrap(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(cmd *exec.Cmd),
|
||||||
|
extraFiles []*os.File,
|
||||||
|
conf *bwrap.Config,
|
||||||
|
syncFd *os.File,
|
||||||
|
) Helper {
|
||||||
|
b, err := NewBwrap(ctx, name, wt, stat, argF, cmdF, extraFiles, conf, syncFd)
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
} else {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
|
||||||
|
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
||||||
|
// Function argF returns an array of arguments passed directly to the child process.
|
||||||
|
func NewBwrap(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(cmd *exec.Cmd),
|
||||||
|
extraFiles []*os.File,
|
||||||
|
conf *bwrap.Config,
|
||||||
|
syncFd *os.File,
|
||||||
|
) (Helper, error) {
|
||||||
|
b, args := newHelperCmd(ctx, BubblewrapName, wt, stat, argF, extraFiles)
|
||||||
|
|
||||||
|
var argsFd uintptr
|
||||||
|
if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
f := proc.NewWriterTo(v)
|
||||||
|
argsFd = proc.InitFile(f, b.extraFiles)
|
||||||
|
b.files = append(b.files, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Args = slices.Grow(b.Args, 4+len(args))
|
||||||
|
b.Args = append(b.Args, "--args", strconv.Itoa(int(argsFd)), "--", name)
|
||||||
|
b.Args = append(b.Args, args...)
|
||||||
|
if cmdF != nil {
|
||||||
|
cmdF(b.Cmd)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
72
helper/bwrap/arg.go
Normal file
72
helper/bwrap/arg.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package bwrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Builder interface {
|
||||||
|
Len() int
|
||||||
|
Append(args *[]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FSBuilder interface {
|
||||||
|
Path() string
|
||||||
|
Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
type FDBuilder interface {
|
||||||
|
proc.File
|
||||||
|
Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args returns a slice of bwrap args corresponding to c.
|
||||||
|
func (c *Config) Args(syncFd *os.File, extraFiles *proc.ExtraFilesPre, files *[]proc.File) (args []string) {
|
||||||
|
builders := []Builder{
|
||||||
|
c.boolArgs(),
|
||||||
|
c.intArgs(),
|
||||||
|
c.stringArgs(),
|
||||||
|
c.pairArgs(),
|
||||||
|
c.seccompArgs(),
|
||||||
|
newFile(SyncFd.String(), syncFd),
|
||||||
|
}
|
||||||
|
|
||||||
|
builders = slices.Grow(builders, len(c.Filesystem)+1)
|
||||||
|
for _, f := range c.Filesystem {
|
||||||
|
builders = append(builders, f)
|
||||||
|
}
|
||||||
|
builders = append(builders, c.Chmod)
|
||||||
|
|
||||||
|
argc := 0
|
||||||
|
fc := 0
|
||||||
|
for _, b := range builders {
|
||||||
|
l := b.Len()
|
||||||
|
if l < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
argc += l
|
||||||
|
|
||||||
|
if f, ok := b.(FDBuilder); ok {
|
||||||
|
fc++
|
||||||
|
proc.InitFile(f, extraFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fc++ // allocate extra slot for stat fd
|
||||||
|
|
||||||
|
args = make([]string, 0, argc)
|
||||||
|
*files = slices.Grow(*files, fc)
|
||||||
|
for _, b := range builders {
|
||||||
|
if b.Len() < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.Append(&args)
|
||||||
|
|
||||||
|
if f, ok := b.(FDBuilder); ok {
|
||||||
|
*files = append(*files, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
199
helper/bwrap/builder.go
Normal file
199
helper/bwrap/builder.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package bwrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Bind binds mount src on host to dest in sandbox.
|
||||||
|
|
||||||
|
Bind(src, dest) bind mount host path readonly on sandbox
|
||||||
|
(--ro-bind SRC DEST).
|
||||||
|
Bind(src, dest, true) equal to ROBind but ignores non-existent host path
|
||||||
|
(--ro-bind-try SRC DEST).
|
||||||
|
|
||||||
|
Bind(src, dest, false, true) bind mount host path on sandbox.
|
||||||
|
(--bind SRC DEST).
|
||||||
|
Bind(src, dest, true, true) equal to Bind but ignores non-existent host path
|
||||||
|
(--bind-try SRC DEST).
|
||||||
|
|
||||||
|
Bind(src, dest, false, true, true) bind mount host path on sandbox, allowing device access
|
||||||
|
(--dev-bind SRC DEST).
|
||||||
|
Bind(src, dest, true, true, true) equal to DevBind but ignores non-existent host path
|
||||||
|
(--dev-bind-try SRC DEST).
|
||||||
|
*/
|
||||||
|
func (c *Config) Bind(src, dest string, opts ...bool) *Config {
|
||||||
|
var (
|
||||||
|
try bool
|
||||||
|
write bool
|
||||||
|
dev bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(opts) > 0 {
|
||||||
|
try = opts[0]
|
||||||
|
}
|
||||||
|
if len(opts) > 1 {
|
||||||
|
write = opts[1]
|
||||||
|
}
|
||||||
|
if len(opts) > 2 {
|
||||||
|
dev = opts[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
if dev {
|
||||||
|
if try {
|
||||||
|
c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.String(), src, dest})
|
||||||
|
} else {
|
||||||
|
c.Filesystem = append(c.Filesystem, &pairF{DevBind.String(), src, dest})
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
} else if write {
|
||||||
|
if try {
|
||||||
|
c.Filesystem = append(c.Filesystem, &pairF{BindTry.String(), src, dest})
|
||||||
|
} else {
|
||||||
|
c.Filesystem = append(c.Filesystem, &pairF{Bind.String(), src, dest})
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
} else {
|
||||||
|
if try {
|
||||||
|
c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.String(), src, dest})
|
||||||
|
} else {
|
||||||
|
c.Filesystem = append(c.Filesystem, &pairF{ROBind.String(), src, dest})
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile copy from FD to destination DEST
|
||||||
|
// (--file FD DEST)
|
||||||
|
func (c *Config) WriteFile(name string, data []byte) *Config {
|
||||||
|
c.Filesystem = append(c.Filesystem, &DataConfig{Dest: name, Data: data, Type: DataWrite})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CopyBind copy from FD to file which is readonly bind-mounted on DEST
|
||||||
|
(--ro-bind-data FD DEST)
|
||||||
|
|
||||||
|
CopyBind(dest, payload, true) copy from FD to file which is bind-mounted on DEST
|
||||||
|
(--bind-data FD DEST)
|
||||||
|
*/
|
||||||
|
func (c *Config) CopyBind(dest string, payload []byte, opts ...bool) *Config {
|
||||||
|
var p *[]byte
|
||||||
|
c.CopyBindRef(dest, &p, opts...)
|
||||||
|
*p = payload
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyBindRef is the same as CopyBind but writes the address of DataConfig.Data.
|
||||||
|
func (c *Config) CopyBindRef(dest string, payloadRef **[]byte, opts ...bool) *Config {
|
||||||
|
t := DataROBind
|
||||||
|
if len(opts) > 0 && opts[0] {
|
||||||
|
t = DataBind
|
||||||
|
}
|
||||||
|
d := &DataConfig{Dest: dest, Type: t}
|
||||||
|
*payloadRef = &d.Data
|
||||||
|
|
||||||
|
c.Filesystem = append(c.Filesystem, d)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dir create dir in sandbox
|
||||||
|
// (--dir DEST)
|
||||||
|
func (c *Config) Dir(dest string) *Config {
|
||||||
|
c.Filesystem = append(c.Filesystem, &stringF{Dir.String(), dest})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemountRO remount path as readonly; does not recursively remount
|
||||||
|
// (--remount-ro DEST)
|
||||||
|
func (c *Config) RemountRO(dest string) *Config {
|
||||||
|
c.Filesystem = append(c.Filesystem, &stringF{RemountRO.String(), dest})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procfs mount new procfs in sandbox
|
||||||
|
// (--proc DEST)
|
||||||
|
func (c *Config) Procfs(dest string) *Config {
|
||||||
|
c.Filesystem = append(c.Filesystem, &stringF{Procfs.String(), dest})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// DevTmpfs mount new dev in sandbox
|
||||||
|
// (--dev DEST)
|
||||||
|
func (c *Config) DevTmpfs(dest string) *Config {
|
||||||
|
c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.String(), dest})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mqueue mount new mqueue in sandbox
|
||||||
|
// (--mqueue DEST)
|
||||||
|
func (c *Config) Mqueue(dest string) *Config {
|
||||||
|
c.Filesystem = append(c.Filesystem, &stringF{Mqueue.String(), dest})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tmpfs mount new tmpfs in sandbox
|
||||||
|
// (--tmpfs DEST)
|
||||||
|
func (c *Config) Tmpfs(dest string, size int, perm ...os.FileMode) *Config {
|
||||||
|
tmpfs := &PermConfig[*TmpfsConfig]{Inner: &TmpfsConfig{Dir: dest}}
|
||||||
|
if size >= 0 {
|
||||||
|
tmpfs.Inner.Size = size
|
||||||
|
}
|
||||||
|
if len(perm) == 1 {
|
||||||
|
tmpfs.Mode = &perm[0]
|
||||||
|
}
|
||||||
|
c.Filesystem = append(c.Filesystem, tmpfs)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay mount overlayfs on DEST, with writes going to an invisible tmpfs
|
||||||
|
// (--tmp-overlay DEST)
|
||||||
|
func (c *Config) Overlay(dest string, src ...string) *Config {
|
||||||
|
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join mount overlayfs read-only on DEST
|
||||||
|
// (--ro-overlay DEST)
|
||||||
|
func (c *Config) Join(dest string, src ...string) *Config {
|
||||||
|
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: new([2]string)})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist mount overlayfs on DEST, with RWSRC as the host path for writes and
|
||||||
|
// WORKDIR an empty directory on the same filesystem as RWSRC
|
||||||
|
// (--overlay RWSRC WORKDIR DEST)
|
||||||
|
func (c *Config) Persist(dest, rwsrc, workdir string, src ...string) *Config {
|
||||||
|
if rwsrc == "" || workdir == "" {
|
||||||
|
panic("persist called without required paths")
|
||||||
|
}
|
||||||
|
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: &[2]string{rwsrc, workdir}})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symlink create symlink within sandbox
|
||||||
|
// (--symlink SRC DEST)
|
||||||
|
func (c *Config) Symlink(src, dest string, perm ...os.FileMode) *Config {
|
||||||
|
symlink := &PermConfig[SymlinkConfig]{Inner: SymlinkConfig{src, dest}}
|
||||||
|
if len(perm) == 1 {
|
||||||
|
symlink.Mode = &perm[0]
|
||||||
|
}
|
||||||
|
c.Filesystem = append(c.Filesystem, symlink)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUID sets custom uid in the sandbox, requires new user namespace (--uid UID).
|
||||||
|
func (c *Config) SetUID(uid int) *Config {
|
||||||
|
if uid >= 0 {
|
||||||
|
c.UID = &uid
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGID sets custom gid in the sandbox, requires new user namespace (--gid GID).
|
||||||
|
func (c *Config) SetGID(gid int) *Config {
|
||||||
|
if gid >= 0 {
|
||||||
|
c.GID = &gid
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
104
helper/bwrap/config.go
Normal file
104
helper/bwrap/config.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package bwrap
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// unshare every namespace we support by default if nil
|
||||||
|
// (--unshare-all)
|
||||||
|
Unshare *UnshareConfig `json:"unshare,omitempty"`
|
||||||
|
// retain the network namespace (can only combine with nil Unshare)
|
||||||
|
// (--share-net)
|
||||||
|
Net bool `json:"net"`
|
||||||
|
|
||||||
|
// disable further use of user namespaces inside sandbox and fail unless
|
||||||
|
// further use of user namespace inside sandbox is disabled if false
|
||||||
|
// (--disable-userns) (--assert-userns-disabled)
|
||||||
|
UserNS bool `json:"userns"`
|
||||||
|
|
||||||
|
// custom uid in the sandbox, requires new user namespace
|
||||||
|
// (--uid UID)
|
||||||
|
UID *int `json:"uid,omitempty"`
|
||||||
|
// custom gid in the sandbox, requires new user namespace
|
||||||
|
// (--gid GID)
|
||||||
|
GID *int `json:"gid,omitempty"`
|
||||||
|
// custom hostname in the sandbox, requires new uts namespace
|
||||||
|
// (--hostname NAME)
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
|
||||||
|
// change directory
|
||||||
|
// (--chdir DIR)
|
||||||
|
Chdir string `json:"chdir,omitempty"`
|
||||||
|
// unset all environment variables
|
||||||
|
// (--clearenv)
|
||||||
|
Clearenv bool `json:"clearenv"`
|
||||||
|
// set environment variable
|
||||||
|
// (--setenv VAR VALUE)
|
||||||
|
SetEnv map[string]string `json:"setenv,omitempty"`
|
||||||
|
// unset environment variables
|
||||||
|
// (--unsetenv VAR)
|
||||||
|
UnsetEnv []string `json:"unsetenv,omitempty"`
|
||||||
|
|
||||||
|
// take a lock on file while sandbox is running
|
||||||
|
// (--lock-file DEST)
|
||||||
|
LockFile []string `json:"lock_file,omitempty"`
|
||||||
|
|
||||||
|
// ordered filesystem args
|
||||||
|
Filesystem []FSBuilder `json:"filesystem,omitempty"`
|
||||||
|
|
||||||
|
// change permissions (must already exist)
|
||||||
|
// (--chmod OCTAL PATH)
|
||||||
|
Chmod ChmodConfig `json:"chmod,omitempty"`
|
||||||
|
|
||||||
|
// load and use seccomp rules from FD (not repeatable)
|
||||||
|
// (--seccomp FD)
|
||||||
|
Syscall *SyscallPolicy
|
||||||
|
|
||||||
|
// create a new terminal session
|
||||||
|
// (--new-session)
|
||||||
|
NewSession bool `json:"new_session"`
|
||||||
|
// kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.
|
||||||
|
// (--die-with-parent)
|
||||||
|
DieWithParent bool `json:"die_with_parent"`
|
||||||
|
// do not install a reaper process with PID=1
|
||||||
|
// (--as-pid-1)
|
||||||
|
AsInit bool `json:"as_init"`
|
||||||
|
|
||||||
|
/* unmapped options include:
|
||||||
|
--unshare-user-try Create new user namespace if possible else continue by skipping it
|
||||||
|
--unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it
|
||||||
|
--userns FD Use this user namespace (cannot combine with --unshare-user)
|
||||||
|
--userns2 FD After setup switch to this user namespace, only useful with --userns
|
||||||
|
--pidns FD Use this pid namespace (as parent namespace if using --unshare-pid)
|
||||||
|
--bind-fd FD DEST Bind open directory or path fd on DEST
|
||||||
|
--ro-bind-fd FD DEST Bind open directory or path fd read-only on DEST
|
||||||
|
--exec-label LABEL Exec label for the sandbox
|
||||||
|
--file-label LABEL File label for temporary sandbox content
|
||||||
|
--add-seccomp-fd FD Load and use seccomp rules from FD (repeatable)
|
||||||
|
--block-fd FD Block on FD until some data to read is available
|
||||||
|
--userns-block-fd FD Block on FD until the user namespace is ready
|
||||||
|
--info-fd FD Write information about the running container to FD
|
||||||
|
--json-status-fd FD Write container status to FD as multiple JSON documents
|
||||||
|
--cap-add CAP Add cap CAP when running as privileged user
|
||||||
|
--cap-drop CAP Drop cap CAP when running as privileged user
|
||||||
|
|
||||||
|
among which --args is used internally for passing arguments */
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnshareConfig struct {
|
||||||
|
// (--unshare-user)
|
||||||
|
// create new user namespace
|
||||||
|
User bool `json:"user"`
|
||||||
|
// (--unshare-ipc)
|
||||||
|
// create new ipc namespace
|
||||||
|
IPC bool `json:"ipc"`
|
||||||
|
// (--unshare-pid)
|
||||||
|
// create new pid namespace
|
||||||
|
PID bool `json:"pid"`
|
||||||
|
// (--unshare-net)
|
||||||
|
// create new network namespace
|
||||||
|
Net bool `json:"net"`
|
||||||
|
// (--unshare-uts)
|
||||||
|
// create new uts namespace
|
||||||
|
UTS bool `json:"uts"`
|
||||||
|
// (--unshare-cgroup)
|
||||||
|
// create new cgroup namespace
|
||||||
|
CGroup bool `json:"cgroup"`
|
||||||
|
}
|
257
helper/bwrap/config_test.go
Normal file
257
helper/bwrap/config_test.go
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
package bwrap_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_Args(t *testing.T) {
|
||||||
|
oldF := seccomp.GetOutput()
|
||||||
|
seccomp.SetOutput(t.Log)
|
||||||
|
t.Cleanup(func() { seccomp.SetOutput(oldF) })
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
conf *bwrap.Config
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"bind", (new(bwrap.Config)).
|
||||||
|
Bind("/etc", "/.fortify/etc").
|
||||||
|
Bind("/etc", "/.fortify/etc", true).
|
||||||
|
Bind("/run", "/.fortify/run", false, true).
|
||||||
|
Bind("/sys/devices", "/.fortify/sys/devices", true, true).
|
||||||
|
Bind("/dev/dri", "/.fortify/dev/dri", false, true, true).
|
||||||
|
Bind("/dev/dri", "/.fortify/dev/dri", true, true, true),
|
||||||
|
[]string{
|
||||||
|
"--unshare-all", "--unshare-user",
|
||||||
|
"--disable-userns", "--assert-userns-disabled",
|
||||||
|
// Bind("/etc", "/.fortify/etc")
|
||||||
|
"--ro-bind", "/etc", "/.fortify/etc",
|
||||||
|
// Bind("/etc", "/.fortify/etc", true)
|
||||||
|
"--ro-bind-try", "/etc", "/.fortify/etc",
|
||||||
|
// Bind("/run", "/.fortify/run", false, true)
|
||||||
|
"--bind", "/run", "/.fortify/run",
|
||||||
|
// Bind("/sys/devices", "/.fortify/sys/devices", true, true)
|
||||||
|
"--bind-try", "/sys/devices", "/.fortify/sys/devices",
|
||||||
|
// Bind("/dev/dri", "/.fortify/dev/dri", false, true, true)
|
||||||
|
"--dev-bind", "/dev/dri", "/.fortify/dev/dri",
|
||||||
|
// Bind("/dev/dri", "/.fortify/dev/dri", true, true, true)
|
||||||
|
"--dev-bind-try", "/dev/dri", "/.fortify/dev/dri",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dir remount-ro proc dev mqueue", (new(bwrap.Config)).
|
||||||
|
Dir("/.fortify").
|
||||||
|
RemountRO("/home").
|
||||||
|
Procfs("/proc").
|
||||||
|
DevTmpfs("/dev").
|
||||||
|
Mqueue("/dev/mqueue"),
|
||||||
|
[]string{
|
||||||
|
"--unshare-all", "--unshare-user",
|
||||||
|
"--disable-userns", "--assert-userns-disabled",
|
||||||
|
// Dir("/.fortify")
|
||||||
|
"--dir", "/.fortify",
|
||||||
|
// RemountRO("/home")
|
||||||
|
"--remount-ro", "/home",
|
||||||
|
// Procfs("/proc")
|
||||||
|
"--proc", "/proc",
|
||||||
|
// DevTmpfs("/dev")
|
||||||
|
"--dev", "/dev",
|
||||||
|
// Mqueue("/dev/mqueue")
|
||||||
|
"--mqueue", "/dev/mqueue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tmpfs", (new(bwrap.Config)).
|
||||||
|
Tmpfs("/run/user", 8192).
|
||||||
|
Tmpfs("/run/dbus", 8192, 0755),
|
||||||
|
[]string{
|
||||||
|
"--unshare-all", "--unshare-user",
|
||||||
|
"--disable-userns", "--assert-userns-disabled",
|
||||||
|
// Tmpfs("/run/user", 8192)
|
||||||
|
"--size", "8192", "--tmpfs", "/run/user",
|
||||||
|
// Tmpfs("/run/dbus", 8192, 0755)
|
||||||
|
"--perms", "755", "--size", "8192", "--tmpfs", "/run/dbus",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symlink", (new(bwrap.Config)).
|
||||||
|
Symlink("/.fortify/sbin/init", "/sbin/init").
|
||||||
|
Symlink("/.fortify/sbin/init", "/sbin/init", 0755),
|
||||||
|
[]string{
|
||||||
|
"--unshare-all", "--unshare-user",
|
||||||
|
"--disable-userns", "--assert-userns-disabled",
|
||||||
|
// Symlink("/.fortify/sbin/init", "/sbin/init")
|
||||||
|
"--symlink", "/.fortify/sbin/init", "/sbin/init",
|
||||||
|
// Symlink("/.fortify/sbin/init", "/sbin/init", 0755)
|
||||||
|
"--perms", "755", "--symlink", "/.fortify/sbin/init", "/sbin/init",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"overlayfs", (new(bwrap.Config)).
|
||||||
|
Overlay("/etc", "/etc").
|
||||||
|
Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin").
|
||||||
|
Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix"),
|
||||||
|
[]string{
|
||||||
|
"--unshare-all", "--unshare-user",
|
||||||
|
"--disable-userns", "--assert-userns-disabled",
|
||||||
|
// Overlay("/etc", "/etc")
|
||||||
|
"--overlay-src", "/etc", "--tmp-overlay", "/etc",
|
||||||
|
// Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin")
|
||||||
|
"--overlay-src", "/bin", "--overlay-src", "/usr/bin",
|
||||||
|
"--overlay-src", "/usr/local/bin", "--ro-overlay", "/.fortify/bin",
|
||||||
|
// Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix")
|
||||||
|
"--overlay-src", "/data/app/org.chromium.Chromium/nix",
|
||||||
|
"--overlay", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/nix",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"copy", (new(bwrap.Config)).
|
||||||
|
WriteFile("/.fortify/version", make([]byte, 8)).
|
||||||
|
CopyBind("/etc/group", make([]byte, 8)).
|
||||||
|
CopyBind("/etc/passwd", make([]byte, 8), true),
|
||||||
|
[]string{
|
||||||
|
"--unshare-all", "--unshare-user",
|
||||||
|
"--disable-userns", "--assert-userns-disabled",
|
||||||
|
// Write("/.fortify/version", make([]byte, 8))
|
||||||
|
"--file", "3", "/.fortify/version",
|
||||||
|
// CopyBind("/etc/group", make([]byte, 8))
|
||||||
|
"--ro-bind-data", "4", "/etc/group",
|
||||||
|
// CopyBind("/etc/passwd", make([]byte, 8), true)
|
||||||
|
"--bind-data", "5", "/etc/passwd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unshare", &bwrap.Config{Unshare: &bwrap.UnshareConfig{
|
||||||
|
User: false,
|
||||||
|
IPC: false,
|
||||||
|
PID: false,
|
||||||
|
Net: false,
|
||||||
|
UTS: false,
|
||||||
|
CGroup: false,
|
||||||
|
}},
|
||||||
|
[]string{"--disable-userns", "--assert-userns-disabled"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid gid sync", (new(bwrap.Config)).
|
||||||
|
SetUID(1971).
|
||||||
|
SetGID(100),
|
||||||
|
[]string{
|
||||||
|
"--unshare-all", "--unshare-user",
|
||||||
|
"--disable-userns", "--assert-userns-disabled",
|
||||||
|
// SetUID(1971)
|
||||||
|
"--uid", "1971",
|
||||||
|
// SetGID(100)
|
||||||
|
"--gid", "100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hostname chdir setenv unsetenv lockfile chmod syscall", &bwrap.Config{
|
||||||
|
Hostname: "fortify",
|
||||||
|
Chdir: "/.fortify",
|
||||||
|
SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"},
|
||||||
|
UnsetEnv: []string{"HOME", "HOST"},
|
||||||
|
LockFile: []string{"/.fortify/lock"},
|
||||||
|
Syscall: new(bwrap.SyscallPolicy),
|
||||||
|
Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755},
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"--unshare-all", "--unshare-user",
|
||||||
|
"--disable-userns", "--assert-userns-disabled",
|
||||||
|
// Hostname: "fortify"
|
||||||
|
"--hostname", "fortify",
|
||||||
|
// Chdir: "/.fortify"
|
||||||
|
"--chdir", "/.fortify",
|
||||||
|
// UnsetEnv: []string{"HOME", "HOST"}
|
||||||
|
"--unsetenv", "HOME",
|
||||||
|
"--unsetenv", "HOST",
|
||||||
|
// LockFile: []string{"/.fortify/lock"},
|
||||||
|
"--lock-file", "/.fortify/lock",
|
||||||
|
// SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"}
|
||||||
|
"--setenv", "FORTIFY_INIT", "/.fortify/sbin/init",
|
||||||
|
// Syscall: new(bwrap.SyscallPolicy),
|
||||||
|
"--seccomp", "3",
|
||||||
|
// Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}
|
||||||
|
"--chmod", "755", "/.fortify/sbin/init",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"xdg-dbus-proxy constraint sample", (&bwrap.Config{Clearenv: true, DieWithParent: true}).
|
||||||
|
Symlink("usr/bin", "/bin").
|
||||||
|
Symlink("var/home", "/home").
|
||||||
|
Symlink("usr/lib", "/lib").
|
||||||
|
Symlink("usr/lib64", "/lib64").
|
||||||
|
Symlink("run/media", "/media").
|
||||||
|
Symlink("var/mnt", "/mnt").
|
||||||
|
Symlink("var/opt", "/opt").
|
||||||
|
Symlink("sysroot/ostree", "/ostree").
|
||||||
|
Symlink("var/roothome", "/root").
|
||||||
|
Symlink("usr/sbin", "/sbin").
|
||||||
|
Symlink("var/srv", "/srv").
|
||||||
|
Bind("/run", "/run", false, true).
|
||||||
|
Bind("/tmp", "/tmp", false, true).
|
||||||
|
Bind("/var", "/var", false, true).
|
||||||
|
Bind("/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", false, true).
|
||||||
|
Bind("/boot", "/boot").
|
||||||
|
Bind("/dev", "/dev").
|
||||||
|
Bind("/proc", "/proc").
|
||||||
|
Bind("/sys", "/sys").
|
||||||
|
Bind("/sysroot", "/sysroot").
|
||||||
|
Bind("/usr", "/usr").
|
||||||
|
Bind("/etc", "/etc"),
|
||||||
|
[]string{
|
||||||
|
"--unshare-all", "--unshare-user",
|
||||||
|
"--disable-userns", "--assert-userns-disabled",
|
||||||
|
"--clearenv", "--die-with-parent",
|
||||||
|
"--symlink", "usr/bin", "/bin",
|
||||||
|
"--symlink", "var/home", "/home",
|
||||||
|
"--symlink", "usr/lib", "/lib",
|
||||||
|
"--symlink", "usr/lib64", "/lib64",
|
||||||
|
"--symlink", "run/media", "/media",
|
||||||
|
"--symlink", "var/mnt", "/mnt",
|
||||||
|
"--symlink", "var/opt", "/opt",
|
||||||
|
"--symlink", "sysroot/ostree", "/ostree",
|
||||||
|
"--symlink", "var/roothome", "/root",
|
||||||
|
"--symlink", "usr/sbin", "/sbin",
|
||||||
|
"--symlink", "var/srv", "/srv",
|
||||||
|
"--bind", "/run", "/run",
|
||||||
|
"--bind", "/tmp", "/tmp",
|
||||||
|
"--bind", "/var", "/var",
|
||||||
|
"--bind", "/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/",
|
||||||
|
"--ro-bind", "/boot", "/boot",
|
||||||
|
"--ro-bind", "/dev", "/dev",
|
||||||
|
"--ro-bind", "/proc", "/proc",
|
||||||
|
"--ro-bind", "/sys", "/sys",
|
||||||
|
"--ro-bind", "/sysroot", "/sysroot",
|
||||||
|
"--ro-bind", "/usr", "/usr",
|
||||||
|
"--ro-bind", "/etc", "/etc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := tc.conf.Args(nil, new(proc.ExtraFilesPre), new([]proc.File)); !slices.Equal(got, tc.want) {
|
||||||
|
t.Errorf("Args() = %#v, want %#v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// test persist validation
|
||||||
|
t.Run("invalid persist", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
wantPanic := "persist called without required paths"
|
||||||
|
if r := recover(); r != wantPanic {
|
||||||
|
t.Errorf("Persist() panic = %v; wantPanic %v", r, wantPanic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
(new(bwrap.Config)).Persist("/run", "", "")
|
||||||
|
})
|
||||||
|
}
|
86
helper/bwrap/seccomp.go
Normal file
86
helper/bwrap/seccomp.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package bwrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SyscallPolicy struct {
|
||||||
|
// disable fortify extensions
|
||||||
|
Compat bool `json:"compat"`
|
||||||
|
// deny development syscalls
|
||||||
|
DenyDevel bool `json:"deny_devel"`
|
||||||
|
// deny multiarch/emulation syscalls
|
||||||
|
Multiarch bool `json:"multiarch"`
|
||||||
|
// allow PER_LINUX32
|
||||||
|
Linux32 bool `json:"linux32"`
|
||||||
|
// allow AF_CAN
|
||||||
|
Can bool `json:"can"`
|
||||||
|
// allow AF_BLUETOOTH
|
||||||
|
Bluetooth bool `json:"bluetooth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) seccompArgs() FDBuilder {
|
||||||
|
// explicitly disable syscall filter
|
||||||
|
if c.Syscall == nil {
|
||||||
|
// nil File skips builder
|
||||||
|
return new(seccompBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
opts seccomp.SyscallOpts
|
||||||
|
optd []string
|
||||||
|
optCond = [...]struct {
|
||||||
|
v bool
|
||||||
|
o seccomp.SyscallOpts
|
||||||
|
d string
|
||||||
|
}{
|
||||||
|
{!c.Syscall.Compat, seccomp.FlagExt, "fortify"},
|
||||||
|
{!c.UserNS, seccomp.FlagDenyNS, "denyns"},
|
||||||
|
{c.NewSession, seccomp.FlagDenyTTY, "denytty"},
|
||||||
|
{c.Syscall.DenyDevel, seccomp.FlagDenyDevel, "denydevel"},
|
||||||
|
{c.Syscall.Multiarch, seccomp.FlagMultiarch, "multiarch"},
|
||||||
|
{c.Syscall.Linux32, seccomp.FlagLinux32, "linux32"},
|
||||||
|
{c.Syscall.Can, seccomp.FlagCan, "can"},
|
||||||
|
{c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
scmpPrintln := seccomp.GetOutput()
|
||||||
|
if scmpPrintln != nil {
|
||||||
|
optd = make([]string, 1, len(optCond)+1)
|
||||||
|
optd[0] = "common"
|
||||||
|
}
|
||||||
|
for _, opt := range optCond {
|
||||||
|
if opt.v {
|
||||||
|
opts |= opt.o
|
||||||
|
if scmpPrintln != nil {
|
||||||
|
optd = append(optd, opt.d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scmpPrintln != nil {
|
||||||
|
scmpPrintln(fmt.Sprintf("seccomp flags: %s", optd))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &seccompBuilder{seccomp.NewFile(opts)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type seccompBuilder struct{ proc.File }
|
||||||
|
|
||||||
|
func (s *seccompBuilder) Len() int {
|
||||||
|
if s == nil || s.File == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *seccompBuilder) Append(args *[]string) {
|
||||||
|
if s == nil || s.File == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
*args = append(*args, Seccomp.String(), strconv.Itoa(int(s.Fd())))
|
||||||
|
}
|
273
helper/bwrap/sequential.go
Normal file
273
helper/bwrap/sequential.go
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
package bwrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(new(PermConfig[SymlinkConfig]))
|
||||||
|
gob.Register(new(PermConfig[*TmpfsConfig]))
|
||||||
|
gob.Register(new(OverlayConfig))
|
||||||
|
gob.Register(new(DataConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
type PositionalArg int
|
||||||
|
|
||||||
|
func (p PositionalArg) String() string { return positionalArgs[p] }
|
||||||
|
|
||||||
|
const (
|
||||||
|
Tmpfs PositionalArg = iota
|
||||||
|
Symlink
|
||||||
|
|
||||||
|
Bind
|
||||||
|
BindTry
|
||||||
|
DevBind
|
||||||
|
DevBindTry
|
||||||
|
ROBind
|
||||||
|
ROBindTry
|
||||||
|
|
||||||
|
Chmod
|
||||||
|
Dir
|
||||||
|
RemountRO
|
||||||
|
Procfs
|
||||||
|
DevTmpfs
|
||||||
|
Mqueue
|
||||||
|
|
||||||
|
Perms
|
||||||
|
Size
|
||||||
|
|
||||||
|
OverlaySrc
|
||||||
|
Overlay
|
||||||
|
TmpOverlay
|
||||||
|
ROOverlay
|
||||||
|
|
||||||
|
SyncFd
|
||||||
|
Seccomp
|
||||||
|
|
||||||
|
File
|
||||||
|
BindData
|
||||||
|
ROBindData
|
||||||
|
)
|
||||||
|
|
||||||
|
var positionalArgs = [...]string{
|
||||||
|
Tmpfs: "--tmpfs",
|
||||||
|
Symlink: "--symlink",
|
||||||
|
|
||||||
|
Bind: "--bind",
|
||||||
|
BindTry: "--bind-try",
|
||||||
|
DevBind: "--dev-bind",
|
||||||
|
DevBindTry: "--dev-bind-try",
|
||||||
|
ROBind: "--ro-bind",
|
||||||
|
ROBindTry: "--ro-bind-try",
|
||||||
|
|
||||||
|
Chmod: "--chmod",
|
||||||
|
Dir: "--dir",
|
||||||
|
RemountRO: "--remount-ro",
|
||||||
|
Procfs: "--proc",
|
||||||
|
DevTmpfs: "--dev",
|
||||||
|
Mqueue: "--mqueue",
|
||||||
|
|
||||||
|
Perms: "--perms",
|
||||||
|
Size: "--size",
|
||||||
|
|
||||||
|
OverlaySrc: "--overlay-src",
|
||||||
|
Overlay: "--overlay",
|
||||||
|
TmpOverlay: "--tmp-overlay",
|
||||||
|
ROOverlay: "--ro-overlay",
|
||||||
|
|
||||||
|
SyncFd: "--sync-fd",
|
||||||
|
Seccomp: "--seccomp",
|
||||||
|
|
||||||
|
File: "--file",
|
||||||
|
BindData: "--bind-data",
|
||||||
|
ROBindData: "--ro-bind-data",
|
||||||
|
}
|
||||||
|
|
||||||
|
type PermConfig[T FSBuilder] struct {
|
||||||
|
// set permissions of next argument
|
||||||
|
// (--perms OCTAL)
|
||||||
|
Mode *os.FileMode `json:"mode,omitempty"`
|
||||||
|
// path to get the new permission
|
||||||
|
// (--bind-data, --file, etc.)
|
||||||
|
Inner T `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PermConfig[T]) Path() string { return p.Inner.Path() }
|
||||||
|
|
||||||
|
func (p *PermConfig[T]) Len() int {
|
||||||
|
if p.Mode != nil {
|
||||||
|
return p.Inner.Len() + 2
|
||||||
|
} else {
|
||||||
|
return p.Inner.Len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PermConfig[T]) Append(args *[]string) {
|
||||||
|
if p.Mode != nil {
|
||||||
|
*args = append(*args, Perms.String(), strconv.FormatInt(int64(*p.Mode), 8))
|
||||||
|
}
|
||||||
|
p.Inner.Append(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TmpfsConfig struct {
|
||||||
|
// set size of tmpfs
|
||||||
|
// (--size BYTES)
|
||||||
|
Size int `json:"size,omitempty"`
|
||||||
|
// mount point of new tmpfs
|
||||||
|
// (--tmpfs DEST)
|
||||||
|
Dir string `json:"dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TmpfsConfig) Path() string { return t.Dir }
|
||||||
|
|
||||||
|
func (t *TmpfsConfig) Len() int {
|
||||||
|
if t.Size > 0 {
|
||||||
|
return 4
|
||||||
|
} else {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TmpfsConfig) Append(args *[]string) {
|
||||||
|
if t.Size > 0 {
|
||||||
|
*args = append(*args, Size.String(), strconv.Itoa(t.Size))
|
||||||
|
}
|
||||||
|
*args = append(*args, Tmpfs.String(), t.Dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverlayConfig struct {
|
||||||
|
/*
|
||||||
|
read files from SRC in the following overlay
|
||||||
|
(--overlay-src SRC)
|
||||||
|
*/
|
||||||
|
Src []string `json:"src,omitempty"`
|
||||||
|
|
||||||
|
/*
|
||||||
|
mount overlayfs on DEST, with RWSRC as the host path for writes and
|
||||||
|
WORKDIR an empty directory on the same filesystem as RWSRC
|
||||||
|
(--overlay RWSRC WORKDIR DEST)
|
||||||
|
|
||||||
|
if nil, mount overlayfs on DEST, with writes going to an invisible tmpfs
|
||||||
|
(--tmp-overlay DEST)
|
||||||
|
|
||||||
|
if either strings are empty, mount overlayfs read-only on DEST
|
||||||
|
(--ro-overlay DEST)
|
||||||
|
*/
|
||||||
|
Persist *[2]string `json:"persist,omitempty"`
|
||||||
|
|
||||||
|
/*
|
||||||
|
--overlay RWSRC WORKDIR DEST
|
||||||
|
|
||||||
|
--tmp-overlay DEST
|
||||||
|
|
||||||
|
--ro-overlay DEST
|
||||||
|
*/
|
||||||
|
Dest string `json:"dest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OverlayConfig) Path() string { return o.Dest }
|
||||||
|
|
||||||
|
func (o *OverlayConfig) Len() int {
|
||||||
|
// (--tmp-overlay DEST) or (--ro-overlay DEST)
|
||||||
|
p := 2
|
||||||
|
// (--overlay RWSRC WORKDIR DEST)
|
||||||
|
if o.Persist != nil && o.Persist[0] != "" && o.Persist[1] != "" {
|
||||||
|
p = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
return p + len(o.Src)*2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OverlayConfig) Append(args *[]string) {
|
||||||
|
// --overlay-src SRC
|
||||||
|
for _, src := range o.Src {
|
||||||
|
*args = append(*args, OverlaySrc.String(), src)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Persist != nil {
|
||||||
|
if o.Persist[0] != "" && o.Persist[1] != "" {
|
||||||
|
// --overlay RWSRC WORKDIR
|
||||||
|
*args = append(*args, Overlay.String(), o.Persist[0], o.Persist[1])
|
||||||
|
} else {
|
||||||
|
// --ro-overlay
|
||||||
|
*args = append(*args, ROOverlay.String())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// --tmp-overlay
|
||||||
|
*args = append(*args, TmpOverlay.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEST
|
||||||
|
*args = append(*args, o.Dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymlinkConfig [2]string
|
||||||
|
|
||||||
|
func (s SymlinkConfig) Path() string { return s[1] }
|
||||||
|
func (s SymlinkConfig) Len() int { return 3 }
|
||||||
|
func (s SymlinkConfig) Append(args *[]string) { *args = append(*args, Symlink.String(), s[0], s[1]) }
|
||||||
|
|
||||||
|
type ChmodConfig map[string]os.FileMode
|
||||||
|
|
||||||
|
func (c ChmodConfig) Len() int { return len(c) }
|
||||||
|
func (c ChmodConfig) Append(args *[]string) {
|
||||||
|
for path, mode := range c {
|
||||||
|
*args = append(*args, Chmod.String(), strconv.FormatInt(int64(mode), 8), path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DataWrite = iota
|
||||||
|
DataBind
|
||||||
|
DataROBind
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataConfig struct {
|
||||||
|
Dest string `json:"dest"`
|
||||||
|
Data []byte `json:"data,omitempty"`
|
||||||
|
Type int `json:"type"`
|
||||||
|
proc.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataConfig) Path() string { return d.Dest }
|
||||||
|
func (d *DataConfig) Len() int {
|
||||||
|
if d == nil || d.Data == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
func (d *DataConfig) Init(fd uintptr, v **os.File) uintptr {
|
||||||
|
if d.File != nil {
|
||||||
|
panic("file initialised twice")
|
||||||
|
}
|
||||||
|
d.File = proc.NewWriterTo(d)
|
||||||
|
return d.File.Init(fd, v)
|
||||||
|
}
|
||||||
|
func (d *DataConfig) WriteTo(w io.Writer) (int64, error) {
|
||||||
|
n, err := w.Write(d.Data)
|
||||||
|
return int64(n), err
|
||||||
|
}
|
||||||
|
func (d *DataConfig) Append(args *[]string) {
|
||||||
|
if d == nil || d.Data == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var a PositionalArg
|
||||||
|
switch d.Type {
|
||||||
|
case DataWrite:
|
||||||
|
a = File
|
||||||
|
case DataBind:
|
||||||
|
a = BindData
|
||||||
|
case DataROBind:
|
||||||
|
a = ROBindData
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("invalid type %d", a))
|
||||||
|
}
|
||||||
|
|
||||||
|
*args = append(*args, a.String(), strconv.Itoa(int(d.Fd())), d.Dest)
|
||||||
|
}
|
249
helper/bwrap/static.go
Normal file
249
helper/bwrap/static.go
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
package bwrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
static boolean args
|
||||||
|
*/
|
||||||
|
|
||||||
|
type BoolArg int
|
||||||
|
|
||||||
|
func (b BoolArg) Unwrap() []string {
|
||||||
|
return boolArgs[b]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
UnshareAll BoolArg = iota
|
||||||
|
UnshareUser
|
||||||
|
UnshareIPC
|
||||||
|
UnsharePID
|
||||||
|
UnshareNet
|
||||||
|
UnshareUTS
|
||||||
|
UnshareCGroup
|
||||||
|
ShareNet
|
||||||
|
|
||||||
|
UserNS
|
||||||
|
Clearenv
|
||||||
|
|
||||||
|
NewSession
|
||||||
|
DieWithParent
|
||||||
|
AsInit
|
||||||
|
)
|
||||||
|
|
||||||
|
var boolArgs = [...][]string{
|
||||||
|
UnshareAll: {"--unshare-all", "--unshare-user"},
|
||||||
|
UnshareUser: {"--unshare-user"},
|
||||||
|
UnshareIPC: {"--unshare-ipc"},
|
||||||
|
UnsharePID: {"--unshare-pid"},
|
||||||
|
UnshareNet: {"--unshare-net"},
|
||||||
|
UnshareUTS: {"--unshare-uts"},
|
||||||
|
UnshareCGroup: {"--unshare-cgroup"},
|
||||||
|
ShareNet: {"--share-net"},
|
||||||
|
|
||||||
|
UserNS: {"--disable-userns", "--assert-userns-disabled"},
|
||||||
|
Clearenv: {"--clearenv"},
|
||||||
|
|
||||||
|
NewSession: {"--new-session"},
|
||||||
|
DieWithParent: {"--die-with-parent"},
|
||||||
|
AsInit: {"--as-pid-1"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) boolArgs() Builder {
|
||||||
|
b := boolArg{
|
||||||
|
UserNS: !c.UserNS,
|
||||||
|
Clearenv: c.Clearenv,
|
||||||
|
|
||||||
|
NewSession: c.NewSession,
|
||||||
|
DieWithParent: c.DieWithParent,
|
||||||
|
AsInit: c.AsInit,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Unshare == nil {
|
||||||
|
b[UnshareAll] = true
|
||||||
|
b[ShareNet] = c.Net
|
||||||
|
} else {
|
||||||
|
b[UnshareUser] = c.Unshare.User
|
||||||
|
b[UnshareIPC] = c.Unshare.IPC
|
||||||
|
b[UnsharePID] = c.Unshare.PID
|
||||||
|
b[UnshareNet] = c.Unshare.Net
|
||||||
|
b[UnshareUTS] = c.Unshare.UTS
|
||||||
|
b[UnshareCGroup] = c.Unshare.CGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
|
type boolArg [len(boolArgs)]bool
|
||||||
|
|
||||||
|
func (b *boolArg) Len() (l int) {
|
||||||
|
for i, v := range b {
|
||||||
|
if v {
|
||||||
|
l += len(boolArgs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolArg) Append(args *[]string) {
|
||||||
|
for i, v := range b {
|
||||||
|
if v {
|
||||||
|
*args = append(*args, BoolArg(i).Unwrap()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
static integer args
|
||||||
|
*/
|
||||||
|
|
||||||
|
type IntArg int
|
||||||
|
|
||||||
|
func (i IntArg) Unwrap() string {
|
||||||
|
return intArgs[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
UID IntArg = iota
|
||||||
|
GID
|
||||||
|
)
|
||||||
|
|
||||||
|
var intArgs = [...]string{
|
||||||
|
UID: "--uid",
|
||||||
|
GID: "--gid",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) intArgs() Builder {
|
||||||
|
return &intArg{
|
||||||
|
UID: c.UID,
|
||||||
|
GID: c.GID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type intArg [len(intArgs)]*int
|
||||||
|
|
||||||
|
func (n *intArg) Len() (l int) {
|
||||||
|
for _, v := range n {
|
||||||
|
if v != nil {
|
||||||
|
l += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *intArg) Append(args *[]string) {
|
||||||
|
for i, v := range n {
|
||||||
|
if v != nil {
|
||||||
|
*args = append(*args, IntArg(i).Unwrap(), strconv.Itoa(*v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
static string args
|
||||||
|
*/
|
||||||
|
|
||||||
|
type StringArg int
|
||||||
|
|
||||||
|
func (s StringArg) Unwrap() string {
|
||||||
|
return stringArgs[s]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
Hostname StringArg = iota
|
||||||
|
Chdir
|
||||||
|
UnsetEnv
|
||||||
|
LockFile
|
||||||
|
)
|
||||||
|
|
||||||
|
var stringArgs = [...]string{
|
||||||
|
Hostname: "--hostname",
|
||||||
|
Chdir: "--chdir",
|
||||||
|
UnsetEnv: "--unsetenv",
|
||||||
|
LockFile: "--lock-file",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) stringArgs() Builder {
|
||||||
|
n := stringArg{
|
||||||
|
UnsetEnv: c.UnsetEnv,
|
||||||
|
LockFile: c.LockFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Hostname != "" {
|
||||||
|
n[Hostname] = []string{c.Hostname}
|
||||||
|
}
|
||||||
|
if c.Chdir != "" {
|
||||||
|
n[Chdir] = []string{c.Chdir}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &n
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringArg [len(stringArgs)][]string
|
||||||
|
|
||||||
|
func (s *stringArg) Len() (l int) {
|
||||||
|
for _, arg := range s {
|
||||||
|
l += len(arg) * 2
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stringArg) Append(args *[]string) {
|
||||||
|
for i, arg := range s {
|
||||||
|
for _, v := range arg {
|
||||||
|
*args = append(*args, StringArg(i).Unwrap(), v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
static pair args
|
||||||
|
*/
|
||||||
|
|
||||||
|
type PairArg int
|
||||||
|
|
||||||
|
func (p PairArg) Unwrap() string {
|
||||||
|
return pairArgs[p]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SetEnv PairArg = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
var pairArgs = [...]string{
|
||||||
|
SetEnv: "--setenv",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) pairArgs() Builder {
|
||||||
|
var n pairArg
|
||||||
|
n[SetEnv] = make([][2]string, len(c.SetEnv))
|
||||||
|
keys := make([]string, 0, len(c.SetEnv))
|
||||||
|
for k := range c.SetEnv {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
slices.Sort(keys)
|
||||||
|
for i, k := range keys {
|
||||||
|
n[SetEnv][i] = [2]string{k, c.SetEnv[k]}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &n
|
||||||
|
}
|
||||||
|
|
||||||
|
type pairArg [len(pairArgs)][][2]string
|
||||||
|
|
||||||
|
func (p *pairArg) Len() (l int) {
|
||||||
|
for _, v := range p {
|
||||||
|
l += len(v) * 3
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pairArg) Append(args *[]string) {
|
||||||
|
for i, arg := range p {
|
||||||
|
for _, v := range arg {
|
||||||
|
*args = append(*args, PairArg(i).Unwrap(), v[0], v[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
helper/bwrap/trivial.go
Normal file
52
helper/bwrap/trivial.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package bwrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(new(pairF))
|
||||||
|
gob.Register(new(stringF))
|
||||||
|
}
|
||||||
|
|
||||||
|
type pairF [3]string
|
||||||
|
|
||||||
|
func (p *pairF) Path() string { return p[2] }
|
||||||
|
func (p *pairF) Len() int { return len(p) }
|
||||||
|
func (p *pairF) Append(args *[]string) { *args = append(*args, p[0], p[1], p[2]) }
|
||||||
|
|
||||||
|
type stringF [2]string
|
||||||
|
|
||||||
|
func (s stringF) Path() string { return s[1] }
|
||||||
|
func (s stringF) Len() int { return len(s) /* compiler replaces this with 2 */ }
|
||||||
|
func (s stringF) Append(args *[]string) { *args = append(*args, s[0], s[1]) }
|
||||||
|
|
||||||
|
func newFile(name string, f *os.File) FDBuilder { return &fileF{name: name, file: f} }
|
||||||
|
|
||||||
|
type fileF struct {
|
||||||
|
name string
|
||||||
|
file *os.File
|
||||||
|
proc.BaseFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileF) ErrCount() int { return 0 }
|
||||||
|
func (f *fileF) Fulfill(_ context.Context, _ func(error)) error { f.Set(f.file); return nil }
|
||||||
|
|
||||||
|
func (f *fileF) Len() int {
|
||||||
|
if f.file == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileF) Append(args *[]string) {
|
||||||
|
if f.file == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*args = append(*args, f.name, strconv.Itoa(int(f.Fd())))
|
||||||
|
}
|
127
helper/bwrap_test.go
Normal file
127
helper/bwrap_test.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package helper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBwrap(t *testing.T) {
|
||||||
|
sc := &bwrap.Config{
|
||||||
|
Net: true,
|
||||||
|
Hostname: "localhost",
|
||||||
|
Chdir: "/proc/nonexistent",
|
||||||
|
Clearenv: true,
|
||||||
|
NewSession: true,
|
||||||
|
DieWithParent: true,
|
||||||
|
AsInit: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("nonexistent bwrap name", func(t *testing.T) {
|
||||||
|
bubblewrapName := helper.BubblewrapName
|
||||||
|
helper.BubblewrapName = "/proc/nonexistent"
|
||||||
|
t.Cleanup(func() { helper.BubblewrapName = bubblewrapName })
|
||||||
|
|
||||||
|
h := helper.MustNewBwrap(
|
||||||
|
context.Background(),
|
||||||
|
"false",
|
||||||
|
argsWt, false,
|
||||||
|
argF, nil,
|
||||||
|
nil,
|
||||||
|
sc, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("Start: error = %v, wantErr %v",
|
||||||
|
err, os.ErrNotExist)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||||
|
if got := helper.MustNewBwrap(
|
||||||
|
context.TODO(),
|
||||||
|
"false",
|
||||||
|
argsWt, false,
|
||||||
|
argF, nil,
|
||||||
|
nil,
|
||||||
|
sc, nil,
|
||||||
|
); got == nil {
|
||||||
|
t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
|
||||||
|
sc, argsWt, "false")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid bwrap config new helper panic", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
wantPanic := "argument contains null character"
|
||||||
|
if r := recover(); r != wantPanic {
|
||||||
|
t.Errorf("MustNewBwrap: panic = %q, want %q",
|
||||||
|
r, wantPanic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
helper.MustNewBwrap(
|
||||||
|
context.TODO(),
|
||||||
|
"false",
|
||||||
|
argsWt, false,
|
||||||
|
argF, nil,
|
||||||
|
nil,
|
||||||
|
&bwrap.Config{Hostname: "\x00"}, nil,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("start without pipes", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
||||||
|
h := helper.MustNewBwrap(
|
||||||
|
ctx, os.Args[0],
|
||||||
|
nil, false,
|
||||||
|
argFChecked, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr; hijackBwrap(cmd) },
|
||||||
|
nil,
|
||||||
|
sc, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := h.Start(); err != nil {
|
||||||
|
t.Errorf("Start: error = %v",
|
||||||
|
err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.Wait(); err != nil {
|
||||||
|
t.Errorf("Wait() err = %v stderr = %s",
|
||||||
|
err, stderr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("implementation compliance", func(t *testing.T) {
|
||||||
|
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||||
|
return helper.MustNewBwrap(
|
||||||
|
ctx, os.Args[0],
|
||||||
|
argsWt, stat,
|
||||||
|
argF, func(cmd *exec.Cmd) { setOutput(&cmd.Stdout, &cmd.Stderr); hijackBwrap(cmd) },
|
||||||
|
nil,
|
||||||
|
sc, nil,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func hijackBwrap(cmd *exec.Cmd) {
|
||||||
|
if cmd.Args[0] != "bwrap" {
|
||||||
|
panic(fmt.Sprintf("unexpected argv0 %q", cmd.Args[0]))
|
||||||
|
}
|
||||||
|
cmd.Err = nil
|
||||||
|
cmd.Path = os.Args[0]
|
||||||
|
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args...)
|
||||||
|
}
|
@ -6,7 +6,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InternalHelperStub is an internal function but exported because it is cross-package;
|
// InternalHelperStub is an internal function but exported because it is cross-package;
|
||||||
@ -25,7 +29,11 @@ func InternalHelperStub() {
|
|||||||
sp = v
|
sp = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(os.Args) > 3 && os.Args[3] == "bwrap" {
|
||||||
|
bwrapStub()
|
||||||
|
} else {
|
||||||
genericStub(flagRestoreFiles(3, ap, sp))
|
genericStub(flagRestoreFiles(3, ap, sp))
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
@ -104,3 +112,43 @@ func genericStub(argsFile, statFile *os.File) {
|
|||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bwrapStub() {
|
||||||
|
// the bwrap launcher does not launch with a typical sync fd
|
||||||
|
argsFile, _ := flagRestoreFiles(4, "1", "0")
|
||||||
|
|
||||||
|
// test args pipe behaviour
|
||||||
|
func() {
|
||||||
|
got, want := new(strings.Builder), new(strings.Builder)
|
||||||
|
if _, err := io.Copy(got, argsFile); err != nil {
|
||||||
|
panic("cannot read bwrap args: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// hardcoded bwrap configuration used by test
|
||||||
|
sc := &bwrap.Config{
|
||||||
|
Net: true,
|
||||||
|
Hostname: "localhost",
|
||||||
|
Chdir: "/proc/nonexistent",
|
||||||
|
Clearenv: true,
|
||||||
|
NewSession: true,
|
||||||
|
DieWithParent: true,
|
||||||
|
AsInit: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))).
|
||||||
|
WriteTo(want); err != nil {
|
||||||
|
panic("cannot read want: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.String() != want.String() {
|
||||||
|
panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := syscall.Exec(
|
||||||
|
flag.CommandLine.Args()[0],
|
||||||
|
flag.CommandLine.Args(),
|
||||||
|
os.Environ()); err != nil {
|
||||||
|
panic("cannot start helper stub: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"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/shim"
|
||||||
"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/system"
|
"git.gensokyo.uk/security/fortify/system"
|
||||||
@ -94,7 +95,7 @@ func (seal *outcome) Run(rs *fst.RunState) error {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
waitErr := make(chan error, 1)
|
waitErr := make(chan error, 1)
|
||||||
cmd := new(shimProcess)
|
cmd := new(shim.Shim)
|
||||||
if startTime, err := cmd.Start(
|
if startTime, err := cmd.Start(
|
||||||
seal.user.aid.String(),
|
seal.user.aid.String(),
|
||||||
seal.user.supp,
|
seal.user.supp,
|
||||||
@ -114,7 +115,7 @@ func (seal *outcome) Run(rs *fst.RunState) error {
|
|||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := cmd.Serve(ctx, &shimParams{
|
if err := cmd.Serve(ctx, &shim.Params{
|
||||||
Container: seal.container,
|
Container: seal.container,
|
||||||
Home: seal.user.data,
|
Home: seal.user.data,
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ import (
|
|||||||
"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"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox/wl"
|
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
"git.gensokyo.uk/security/fortify/system"
|
||||||
|
"git.gensokyo.uk/security/fortify/wl"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -1,212 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/gob"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
||||||
"git.gensokyo.uk/security/fortify/sandbox"
|
|
||||||
)
|
|
||||||
|
|
||||||
const shimEnv = "FORTIFY_SHIM"
|
|
||||||
|
|
||||||
type shimParams struct {
|
|
||||||
// finalised container params
|
|
||||||
Container *sandbox.Params
|
|
||||||
// path to outer home directory
|
|
||||||
Home string
|
|
||||||
|
|
||||||
// verbosity pass through
|
|
||||||
Verbose bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
|
|
||||||
func ShimMain() {
|
|
||||||
fmsg.Prepare("shim")
|
|
||||||
|
|
||||||
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
|
|
||||||
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
params shimParams
|
|
||||||
closeSetup func() error
|
|
||||||
)
|
|
||||||
if f, err := sandbox.Receive(shimEnv, ¶ms, nil); err != nil {
|
|
||||||
if errors.Is(err, sandbox.ErrInvalid) {
|
|
||||||
log.Fatal("invalid config descriptor")
|
|
||||||
}
|
|
||||||
if errors.Is(err, sandbox.ErrNotSet) {
|
|
||||||
log.Fatal("FORTIFY_SHIM not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Fatalf("cannot receive shim setup params: %v", err)
|
|
||||||
} else {
|
|
||||||
internal.InstallFmsg(params.Verbose)
|
|
||||||
closeSetup = f
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.Container == nil || params.Container.Ops == nil {
|
|
||||||
log.Fatal("invalid container params")
|
|
||||||
}
|
|
||||||
|
|
||||||
// close setup socket
|
|
||||||
if err := closeSetup(); err != nil {
|
|
||||||
log.Printf("cannot close setup pipe: %v", err)
|
|
||||||
// not fatal
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure home directory as target user
|
|
||||||
if s, err := os.Stat(params.Home); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
if err = os.Mkdir(params.Home, 0700); err != nil {
|
|
||||||
log.Fatalf("cannot create home directory: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Fatalf("cannot access home directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// home directory is created, proceed
|
|
||||||
} else if !s.IsDir() {
|
|
||||||
log.Fatalf("path %q is not a directory", params.Home)
|
|
||||||
}
|
|
||||||
|
|
||||||
var name string
|
|
||||||
if len(params.Container.Args) > 0 {
|
|
||||||
name = params.Container.Args[0]
|
|
||||||
}
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
||||||
defer stop() // unreachable
|
|
||||||
container := sandbox.New(ctx, name)
|
|
||||||
container.Params = *params.Container
|
|
||||||
container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
||||||
container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
|
|
||||||
container.WaitDelay = 2 * time.Second
|
|
||||||
|
|
||||||
if err := container.Start(); err != nil {
|
|
||||||
fmsg.PrintBaseError(err, "cannot start container:")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if err := container.Serve(); err != nil {
|
|
||||||
fmsg.PrintBaseError(err, "cannot configure container:")
|
|
||||||
}
|
|
||||||
if err := container.Wait(); err != nil {
|
|
||||||
var exitError *exec.ExitError
|
|
||||||
if !errors.As(err, &exitError) {
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
log.Printf("wait: %v", err)
|
|
||||||
os.Exit(127)
|
|
||||||
}
|
|
||||||
os.Exit(exitError.ExitCode())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type shimProcess struct {
|
|
||||||
// user switcher process
|
|
||||||
cmd *exec.Cmd
|
|
||||||
// fallback exit notifier with error returned killing the process
|
|
||||||
killFallback chan error
|
|
||||||
// monitor to shim encoder
|
|
||||||
encoder *gob.Encoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *shimProcess) Unwrap() *exec.Cmd { return s.cmd }
|
|
||||||
func (s *shimProcess) Fallback() chan error { return s.killFallback }
|
|
||||||
|
|
||||||
func (s *shimProcess) String() string {
|
|
||||||
if s.cmd == nil {
|
|
||||||
return "(unused shim manager)"
|
|
||||||
}
|
|
||||||
return s.cmd.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *shimProcess) Start(
|
|
||||||
aid string,
|
|
||||||
supp []string,
|
|
||||||
) (*time.Time, error) {
|
|
||||||
// prepare user switcher invocation
|
|
||||||
fsuPath := internal.MustFsuPath()
|
|
||||||
s.cmd = exec.Command(fsuPath)
|
|
||||||
|
|
||||||
// pass shim setup pipe
|
|
||||||
if fd, e, err := sandbox.Setup(&s.cmd.ExtraFiles); err != nil {
|
|
||||||
return nil, fmsg.WrapErrorSuffix(err,
|
|
||||||
"cannot create shim setup pipe:")
|
|
||||||
} else {
|
|
||||||
s.encoder = e
|
|
||||||
s.cmd.Env = []string{
|
|
||||||
shimEnv + "=" + strconv.Itoa(fd),
|
|
||||||
"FORTIFY_APP_ID=" + aid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// format fsu supplementary groups
|
|
||||||
if len(supp) > 0 {
|
|
||||||
fmsg.Verbosef("attaching supplementary group ids %s", supp)
|
|
||||||
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
|
|
||||||
}
|
|
||||||
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
||||||
s.cmd.Dir = "/"
|
|
||||||
|
|
||||||
fmsg.Verbose("starting shim via fsu:", s.cmd)
|
|
||||||
// withhold messages to stderr
|
|
||||||
fmsg.Suspend()
|
|
||||||
if err := s.cmd.Start(); err != nil {
|
|
||||||
return nil, fmsg.WrapErrorSuffix(err,
|
|
||||||
"cannot start fsu:")
|
|
||||||
}
|
|
||||||
startTime := time.Now().UTC()
|
|
||||||
|
|
||||||
return &startTime, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *shimProcess) Serve(ctx context.Context, params *shimParams) error {
|
|
||||||
// kill shim if something goes wrong and an error is returned
|
|
||||||
s.killFallback = make(chan error, 1)
|
|
||||||
killShim := func() {
|
|
||||||
if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
|
|
||||||
s.killFallback <- err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer func() { killShim() }()
|
|
||||||
|
|
||||||
encodeErr := make(chan error)
|
|
||||||
go func() { encodeErr <- s.encoder.Encode(params) }()
|
|
||||||
|
|
||||||
select {
|
|
||||||
// encode return indicates setup completion
|
|
||||||
case err := <-encodeErr:
|
|
||||||
if err != nil {
|
|
||||||
return fmsg.WrapErrorSuffix(err,
|
|
||||||
"cannot transmit shim config:")
|
|
||||||
}
|
|
||||||
killShim = func() {}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
// setup canceled before payload was accepted
|
|
||||||
case <-ctx.Done():
|
|
||||||
err := ctx.Err()
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
return fmsg.WrapError(syscall.ECANCELED,
|
|
||||||
"shim setup canceled")
|
|
||||||
}
|
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
return fmsg.WrapError(syscall.ETIMEDOUT,
|
|
||||||
"deadline exceeded waiting for shim")
|
|
||||||
}
|
|
||||||
// unreachable
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
115
internal/app/shim/main.go
Normal file
115
internal/app/shim/main.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package shim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Env = "FORTIFY_SHIM"
|
||||||
|
|
||||||
|
type Params struct {
|
||||||
|
// finalised container params
|
||||||
|
Container *sandbox.Params
|
||||||
|
// path to outer home directory
|
||||||
|
Home string
|
||||||
|
|
||||||
|
// verbosity pass through
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything beyond this point runs as unconstrained target user
|
||||||
|
// proceed with caution!
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
// sharing stdout with fortify
|
||||||
|
// USE WITH CAUTION
|
||||||
|
fmsg.Prepare("shim")
|
||||||
|
|
||||||
|
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
|
||||||
|
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
params Params
|
||||||
|
closeSetup func() error
|
||||||
|
)
|
||||||
|
if f, err := sandbox.Receive(Env, ¶ms, nil); err != nil {
|
||||||
|
if errors.Is(err, sandbox.ErrInvalid) {
|
||||||
|
log.Fatal("invalid config descriptor")
|
||||||
|
}
|
||||||
|
if errors.Is(err, sandbox.ErrNotSet) {
|
||||||
|
log.Fatal("FORTIFY_SHIM not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatalf("cannot receive shim setup params: %v", err)
|
||||||
|
} else {
|
||||||
|
internal.InstallFmsg(params.Verbose)
|
||||||
|
closeSetup = f
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Container == nil || params.Container.Ops == nil {
|
||||||
|
log.Fatal("invalid container params")
|
||||||
|
}
|
||||||
|
|
||||||
|
// close setup socket
|
||||||
|
if err := closeSetup(); err != nil {
|
||||||
|
log.Printf("cannot close setup pipe: %v", err)
|
||||||
|
// not fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure home directory as target user
|
||||||
|
if s, err := os.Stat(params.Home); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if err = os.Mkdir(params.Home, 0700); err != nil {
|
||||||
|
log.Fatalf("cannot create home directory: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Fatalf("cannot access home directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// home directory is created, proceed
|
||||||
|
} else if !s.IsDir() {
|
||||||
|
log.Fatalf("path %q is not a directory", params.Home)
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if len(params.Container.Args) > 0 {
|
||||||
|
name = params.Container.Args[0]
|
||||||
|
}
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop() // unreachable
|
||||||
|
container := sandbox.New(ctx, name)
|
||||||
|
container.Params = *params.Container
|
||||||
|
container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
|
||||||
|
container.WaitDelay = 2 * time.Second
|
||||||
|
|
||||||
|
if err := container.Start(); err != nil {
|
||||||
|
fmsg.PrintBaseError(err, "cannot start container:")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := container.Serve(); err != nil {
|
||||||
|
fmsg.PrintBaseError(err, "cannot configure container:")
|
||||||
|
}
|
||||||
|
if err := container.Wait(); err != nil {
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if !errors.As(err, &exitError) {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
log.Printf("wait: %v", err)
|
||||||
|
os.Exit(127)
|
||||||
|
}
|
||||||
|
os.Exit(exitError.ExitCode())
|
||||||
|
}
|
||||||
|
}
|
117
internal/app/shim/proc.go
Normal file
117
internal/app/shim/proc.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package shim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
// used by the parent process
|
||||||
|
|
||||||
|
type Shim struct {
|
||||||
|
// user switcher process
|
||||||
|
cmd *exec.Cmd
|
||||||
|
// fallback exit notifier with error returned killing the process
|
||||||
|
killFallback chan error
|
||||||
|
// monitor to shim encoder
|
||||||
|
encoder *gob.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shim) Unwrap() *exec.Cmd { return s.cmd }
|
||||||
|
func (s *Shim) Fallback() chan error { return s.killFallback }
|
||||||
|
|
||||||
|
func (s *Shim) String() string {
|
||||||
|
if s.cmd == nil {
|
||||||
|
return "(unused shim manager)"
|
||||||
|
}
|
||||||
|
return s.cmd.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shim) Start(
|
||||||
|
aid string,
|
||||||
|
supp []string,
|
||||||
|
) (*time.Time, error) {
|
||||||
|
// prepare user switcher invocation
|
||||||
|
fsuPath := internal.MustFsuPath()
|
||||||
|
s.cmd = exec.Command(fsuPath)
|
||||||
|
|
||||||
|
// pass shim setup pipe
|
||||||
|
if fd, e, err := sandbox.Setup(&s.cmd.ExtraFiles); err != nil {
|
||||||
|
return nil, fmsg.WrapErrorSuffix(err,
|
||||||
|
"cannot create shim setup pipe:")
|
||||||
|
} else {
|
||||||
|
s.encoder = e
|
||||||
|
s.cmd.Env = []string{
|
||||||
|
Env + "=" + strconv.Itoa(fd),
|
||||||
|
"FORTIFY_APP_ID=" + aid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// format fsu supplementary groups
|
||||||
|
if len(supp) > 0 {
|
||||||
|
fmsg.Verbosef("attaching supplementary group ids %s", supp)
|
||||||
|
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
|
||||||
|
}
|
||||||
|
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
s.cmd.Dir = "/"
|
||||||
|
|
||||||
|
fmsg.Verbose("starting shim via fsu:", s.cmd)
|
||||||
|
// withhold messages to stderr
|
||||||
|
fmsg.Suspend()
|
||||||
|
if err := s.cmd.Start(); err != nil {
|
||||||
|
return nil, fmsg.WrapErrorSuffix(err,
|
||||||
|
"cannot start fsu:")
|
||||||
|
}
|
||||||
|
startTime := time.Now().UTC()
|
||||||
|
|
||||||
|
return &startTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shim) Serve(ctx context.Context, params *Params) error {
|
||||||
|
// kill shim if something goes wrong and an error is returned
|
||||||
|
s.killFallback = make(chan error, 1)
|
||||||
|
killShim := func() {
|
||||||
|
if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
|
||||||
|
s.killFallback <- err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() { killShim() }()
|
||||||
|
|
||||||
|
encodeErr := make(chan error)
|
||||||
|
go func() { encodeErr <- s.encoder.Encode(params) }()
|
||||||
|
|
||||||
|
select {
|
||||||
|
// encode return indicates setup completion
|
||||||
|
case err := <-encodeErr:
|
||||||
|
if err != nil {
|
||||||
|
return fmsg.WrapErrorSuffix(err,
|
||||||
|
"cannot transmit shim config:")
|
||||||
|
}
|
||||||
|
killShim = func() {}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
// setup canceled before payload was accepted
|
||||||
|
case <-ctx.Done():
|
||||||
|
err := ctx.Err()
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return fmsg.WrapError(syscall.ECANCELED,
|
||||||
|
"shim setup canceled")
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return fmsg.WrapError(syscall.ETIMEDOUT,
|
||||||
|
"deadline exceeded waiting for shim")
|
||||||
|
}
|
||||||
|
// unreachable
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
3
main.go
3
main.go
@ -20,6 +20,7 @@ import (
|
|||||||
"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"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
||||||
"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 { app.ShimMain(); return errSuccess })
|
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); 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 {
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
buildGoModule,
|
buildGoModule,
|
||||||
makeBinaryWrapper,
|
makeBinaryWrapper,
|
||||||
xdg-dbus-proxy,
|
xdg-dbus-proxy,
|
||||||
|
bubblewrap,
|
||||||
pkg-config,
|
pkg-config,
|
||||||
libffi,
|
libffi,
|
||||||
libseccomp,
|
libseccomp,
|
||||||
@ -89,6 +90,7 @@ buildGoModule rec {
|
|||||||
let
|
let
|
||||||
appPackages = [
|
appPackages = [
|
||||||
glibc
|
glibc
|
||||||
|
bubblewrap
|
||||||
xdg-dbus-proxy
|
xdg-dbus-proxy
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// Package sandbox implements unprivileged Linux container with hardening options useful for creating application sandboxes.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
"git.gensokyo.uk/security/fortify/acl"
|
||||||
"git.gensokyo.uk/security/fortify/sandbox/wl"
|
"git.gensokyo.uk/security/fortify/wl"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wayland sets up a wayland socket with a security context attached.
|
// Wayland sets up a wayland socket with a security context attached.
|
||||||
|
Loading…
Reference in New Issue
Block a user