├── examples ├── profiles │ ├── docker.mod │ ├── README.md │ └── docker-26.1.3.mod ├── poisoned │ ├── go.mod │ └── poisoned.go ├── README.md └── victim │ ├── main.go │ └── go.mod ├── pkg ├── unwinder │ ├── unwinder_amd64.go │ ├── unwinder_arm64.go │ ├── unwinder_darwin.go │ ├── unwinder_linux.go │ └── unwinder.go ├── env │ └── env.go ├── tracer │ ├── regs │ │ ├── regs_linux.go │ │ ├── regs_linux_arm64.go │ │ └── regs_linux_amd64.go │ ├── tracer.go │ ├── tracer_darwin.go │ └── tracer_linux.go ├── child │ ├── child_others.go │ └── child_linux.go ├── osargs │ └── osargs.go ├── parent │ ├── parent_darwin.go │ ├── parent.go │ └── parent_linux.go ├── cp │ └── cp.go ├── envutil │ └── envutil.go ├── cache │ └── cache.go ├── profile │ ├── profile_test.go │ ├── profile.go │ ├── fromgomod │ │ ├── fromgomod.go │ │ └── fromgomod_test.go │ └── seccompprofile │ │ └── seccompprofile_linux.go ├── procutil │ └── procutil_linux.go └── ziputil │ └── ziputil.go ├── .gitignore ├── go.mod ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── main.yml ├── docs └── syntax.md ├── libgomodjail_hook_darwin ├── offset.sh └── libgomodjail_hook_darwin.c ├── cmd └── gomodjail │ ├── version │ └── version.go │ ├── commands │ ├── run │ │ └── run.go │ └── pack │ │ └── pack.go │ └── main.go ├── go.sum ├── Makefile ├── README.md └── LICENSE /examples/profiles/docker.mod: -------------------------------------------------------------------------------- 1 | docker-26.1.3.mod -------------------------------------------------------------------------------- /pkg/unwinder/unwinder_amd64.go: -------------------------------------------------------------------------------- 1 | package unwinder 2 | 3 | const wordSize = 8 4 | -------------------------------------------------------------------------------- /pkg/unwinder/unwinder_arm64.go: -------------------------------------------------------------------------------- 1 | package unwinder 2 | 3 | const wordSize = 8 4 | -------------------------------------------------------------------------------- /examples/poisoned/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AkihiroSuda/gomodjail/examples/poisoned 2 | 3 | go 1.23 4 | -------------------------------------------------------------------------------- /pkg/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | const ( 4 | PrivateChild = "_GOMODJAIL_PRIVATE_CHILD" // no value 5 | CacheHome = "GOMODJAIL_CACHE_HOME" 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/tracer/regs/regs_linux.go: -------------------------------------------------------------------------------- 1 | package regs 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | type Regs struct { 6 | unix.PtraceRegs 7 | Modified bool 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_artifacts 2 | /_output 3 | /gomodjail 4 | /victim 5 | /victim.* 6 | /examples/victim/victim 7 | /examples/victim/victim.* 8 | *.o 9 | *.dylib 10 | *.gomodjail 11 | -------------------------------------------------------------------------------- /pkg/child/child_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package child 4 | 5 | import "errors" 6 | 7 | func Main(_ []string) error { 8 | return errors.New("unexpected code path") 9 | } 10 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | - [`victim`](./victim): An example application 2 | - [`poisoned`](./poisoned): An example module depended by `victim` 3 | 4 | - - - 5 | 6 | - [`profiles`](./profiles): Example profiles 7 | -------------------------------------------------------------------------------- /examples/victim/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | p "github.com/AkihiroSuda/gomodjail/examples/poisoned" 7 | ) 8 | 9 | func main() { 10 | const x, y = 42, 43 11 | fmt.Printf("%d + %d = %d\n", x, y, p.Add(x, y)) 12 | } 13 | -------------------------------------------------------------------------------- /examples/victim/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AkihiroSuda/gomodjail/examples/victim 2 | 3 | go 1.23 4 | 5 | require github.com/AkihiroSuda/gomodjail/examples/poisoned v0.0.0-00010101000000-000000000000 // gomodjail:confined 6 | 7 | replace github.com/AkihiroSuda/gomodjail/examples/poisoned => ../poisoned 8 | -------------------------------------------------------------------------------- /pkg/osargs/osargs.go: -------------------------------------------------------------------------------- 1 | package osargs 2 | 3 | import "os" 4 | 5 | var overridden []string 6 | 7 | func OSArgs() []string { 8 | if overridden != nil { 9 | return overridden 10 | } 11 | return os.Args 12 | } 13 | 14 | func SetOSArgs(override []string) { 15 | overridden = override 16 | } 17 | -------------------------------------------------------------------------------- /pkg/parent/parent_darwin.go: -------------------------------------------------------------------------------- 1 | package parent 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | ) 7 | 8 | func createCmd(args []string) (*exec.Cmd, error) { 9 | cmd := exec.Command(args[0], args[1:]...) 10 | cmd.Stdin = os.Stdin 11 | cmd.Stdout = os.Stdout 12 | cmd.Stderr = os.Stderr 13 | return cmd, nil 14 | } 15 | -------------------------------------------------------------------------------- /pkg/tracer/tracer.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import "fmt" 4 | 5 | type Tracer interface { 6 | // Trace traces the process. 7 | // Trace may return [ExitError]. 8 | Trace() error 9 | } 10 | 11 | type ExitError struct { 12 | ExitCode int 13 | } 14 | 15 | func (e *ExitError) Error() string { 16 | return fmt.Sprintf("exit code %d", e.ExitCode) 17 | } 18 | -------------------------------------------------------------------------------- /examples/profiles/README.md: -------------------------------------------------------------------------------- 1 | # Example profiles of gomodjail 2 | 3 | See the top-level [`README.md`](../../README.md) for usage. 4 | 5 | - [`docker-26.1.3.mod`](./docker-26.1.3.mod): profile for [`docker`](https://github.com/docker/cli) (Docker CLI, not Docker daemon) 6 | 7 | ## Projects using gomodjail 8 | - https://github.com/lima-vm/lima/blob/master/go.mod 9 | - https://github.com/containerd/nerdctl/blob/main/go.mod 10 | - https://github.com/AkihiroSuda/alcless/blob/master/go.mod 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AkihiroSuda/gomodjail 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/elastic/go-seccomp-bpf v1.6.0 7 | github.com/spf13/cobra v1.10.2 8 | golang.org/x/mod v0.31.0 9 | golang.org/x/sys v0.39.0 10 | gotest.tools/v3 v3.5.2 11 | ) 12 | 13 | require ( 14 | github.com/google/go-cmp v0.5.9 // indirect 15 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 16 | github.com/spf13/pflag v1.0.9 // indirect 17 | golang.org/x/net v0.41.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/tracer/regs/regs_linux_arm64.go: -------------------------------------------------------------------------------- 1 | package regs 2 | 3 | func (regs *Regs) Syscall() uint64 { 4 | return regs.Regs[8] 5 | } 6 | 7 | func (regs *Regs) Args() []uint64 { 8 | return regs.Regs[0:5] 9 | } 10 | 11 | func (regs *Regs) SetSyscall(v uint64) { 12 | regs.Regs[8] = v 13 | regs.Modified = true 14 | } 15 | 16 | func (regs *Regs) SetRet(v uint64) { 17 | regs.Regs[0] = v 18 | regs.Modified = true 19 | } 20 | 21 | func (regs *Regs) FramePointer() uint64 { 22 | return regs.Regs[29] 23 | } 24 | -------------------------------------------------------------------------------- /pkg/cp/cp.go: -------------------------------------------------------------------------------- 1 | package cp 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | func CopyFile(dst, src string, perm os.FileMode) error { 9 | srcF, err := os.Open(src) 10 | if err != nil { 11 | return err 12 | } 13 | defer srcF.Close() //nolint:errcheck 14 | dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_RDWR, perm) 15 | if err != nil { 16 | return err 17 | } 18 | defer dstF.Close() // nolint:errcheck 19 | if _, err = io.Copy(dstF, srcF); err != nil { 20 | return err 21 | } 22 | return dstF.Close() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/parent/parent.go: -------------------------------------------------------------------------------- 1 | package parent 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/AkihiroSuda/gomodjail/pkg/profile" 7 | "github.com/AkihiroSuda/gomodjail/pkg/tracer" 8 | ) 9 | 10 | func Main(profile *profile.Profile, args []string) error { 11 | cmd, err := createCmd(args) 12 | if err != nil { 13 | return err 14 | } 15 | tr, err := tracer.New(cmd, profile) 16 | if err != nil { 17 | return err 18 | } 19 | if trC, ok := tr.(io.Closer); ok { 20 | defer trC.Close() //nolint:errcheck 21 | } 22 | return tr.Trace() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/tracer/regs/regs_linux_amd64.go: -------------------------------------------------------------------------------- 1 | package regs 2 | 3 | func (regs *Regs) Syscall() uint64 { 4 | return regs.Orig_rax 5 | } 6 | 7 | func (regs *Regs) Args() []uint64 { 8 | return []uint64{regs.Rdi, regs.Rsi, regs.Rdx, regs.R10, regs.R8, regs.R9} 9 | } 10 | 11 | func (regs *Regs) SetSyscall(v uint64) { 12 | regs.Orig_rax = v 13 | regs.Modified = true 14 | } 15 | 16 | func (regs *Regs) SetRet(v uint64) { 17 | regs.Rax = v 18 | regs.Modified = true 19 | } 20 | 21 | func (regs *Regs) FramePointer() uint64 { 22 | return regs.Rbp 23 | } 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - AkihiroSuda 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: weekly 14 | open-pull-requests-limit: 10 15 | reviewers: 16 | - AkihiroSuda 17 | - package-ecosystem: docker 18 | directory: "/" 19 | schedule: 20 | interval: weekly 21 | open-pull-requests-limit: 10 22 | reviewers: 23 | - AkihiroSuda 24 | -------------------------------------------------------------------------------- /pkg/envutil/envutil.go: -------------------------------------------------------------------------------- 1 | // Package envutil is from https://github.com/reproducible-containers/repro-get/blob/v0.4.0/pkg/envutil/envutil.go 2 | package envutil 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "strconv" 8 | 9 | "log/slog" 10 | ) 11 | 12 | func Bool(envName string, defaultValue bool) bool { 13 | v, ok := os.LookupEnv(envName) 14 | if !ok { 15 | return defaultValue 16 | } 17 | b, err := strconv.ParseBool(v) 18 | if err != nil { 19 | slog.Warn(fmt.Sprintf("Failed to parse %q ($%s) as a boolean: %v", v, envName, err)) 20 | return defaultValue 21 | } 22 | return b 23 | } 24 | -------------------------------------------------------------------------------- /examples/poisoned/poisoned.go: -------------------------------------------------------------------------------- 1 | package poisoned 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | // Add returns x + y . 10 | func Add(x, y int) int { 11 | const msg = `*** ARBITRARY SHELL CODE EXECUTION *** 12 | 13 | This 'vi' command was executed by the 'github.com/AkihiroSuda/gomodjail/examples/poisoned' module. 14 | 15 | This example is harmless, of course, but suppose that this was a malicious code. 16 | 17 | Type ':q!' to leave this screen. 18 | ` 19 | cmd := exec.Command("vi", "-") 20 | cmd.Stdin = strings.NewReader(msg) 21 | cmd.Stdout = os.Stdout 22 | cmd.Stderr = os.Stderr 23 | _ = cmd.Run() 24 | return x + y 25 | } 26 | -------------------------------------------------------------------------------- /pkg/parent/parent_linux.go: -------------------------------------------------------------------------------- 1 | package parent 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/AkihiroSuda/gomodjail/pkg/env" 8 | "github.com/AkihiroSuda/gomodjail/pkg/osargs" 9 | ) 10 | 11 | func createCmd(_ []string) (*exec.Cmd, error) { 12 | self, err := os.Executable() 13 | if err != nil { 14 | return nil, err 15 | } 16 | // osargs.OSArgs is basically os.Args but is overridden on self-extract mode 17 | cmd := exec.Command(self, osargs.OSArgs()[1:]...) 18 | cmd.Stdin = os.Stdin 19 | cmd.Stdout = os.Stdout 20 | cmd.Stderr = os.Stderr 21 | cmd.Env = append(os.Environ(), env.PrivateChild+"=") // no value 22 | return cmd, nil 23 | } 24 | -------------------------------------------------------------------------------- /docs/syntax.md: -------------------------------------------------------------------------------- 1 | # Syntax of `// gomodjail:...` comments 2 | 3 | e.g., 4 | ```go-module 5 | // gomodjail:confined 6 | module example.com/foo 7 | 8 | go 1.23 9 | 10 | require ( 11 | example.com/mod100 v1.2.3 12 | example.com/mod101 v1.2.3 // gomodjail:unconfined 13 | example.com/mod102 v1.2.3 14 | // gomodjail:unconfined 15 | example.com/mod103 v1.2.3 16 | ) 17 | 18 | require ( 19 | // gomodjail:unconfined 20 | example.com/mod200 v1.2.3 // indirect 21 | example.com/mod201 v1.2.3 // indirect 22 | ) 23 | 24 | // policy cannot be specified here because the parser ignores 25 | // the comment lines here 26 | require ( 27 | ) 28 | ``` 29 | 30 | This makes the following modules confined: `mod100`, `mod102`, and `mod201`. 31 | 32 | The version numbers are ignored. 33 | -------------------------------------------------------------------------------- /pkg/unwinder/unwinder_darwin.go: -------------------------------------------------------------------------------- 1 | // Package unwider provides unwinder for Go call stacks. 2 | // 3 | // https://www.grant.pizza/blog/go-stack-traces-bpf/ 4 | package unwinder 5 | 6 | import ( 7 | "debug/macho" 8 | "errors" 9 | ) 10 | 11 | const ( 12 | gopclntabSectionName = "__gopclntab" 13 | textSectionName = "__text" 14 | gosymtabSectionName = "__gosymtab" 15 | ) 16 | 17 | type machoObjectFile struct { 18 | *macho.File 19 | } 20 | 21 | func (e *machoObjectFile) Section(name string) Section { 22 | section := e.File.Section(name) 23 | if section == nil { 24 | return nil 25 | } 26 | return &machoSection{Section: section} 27 | } 28 | 29 | type machoSection struct { 30 | *macho.Section 31 | } 32 | 33 | func (s *machoSection) Addr() uint64 { 34 | return s.Section.Addr 35 | } 36 | 37 | func openObjectFile(path string) (ObjectFile, error) { 38 | f, err := macho.Open(path) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return &machoObjectFile{File: f}, nil 43 | } 44 | 45 | func (u *unwinder) unwind(_ int, _, _ uintptr) ([]Entry, error) { 46 | _ = wordSize 47 | return nil, errors.New("unwinder for Darwin is not implemented yet") 48 | } 49 | -------------------------------------------------------------------------------- /libgomodjail_hook_darwin/offset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Usage: 4 | # GO111MODULE=off GO="$(go env GOPATH)"/src/go.googlesource.com/go/bin/go ./offset.sh 5 | 6 | set -eux -o pipefail 7 | 8 | : "${GO:=go}" 9 | 10 | GOROOT="$("$GO" env GOROOT)" 11 | 12 | cat <"${GOROOT}"/src/runtime/gomodjail_offset.go 13 | package runtime 14 | 15 | import "unsafe" 16 | 17 | var ( 18 | GOMODJAIL_OFFSET_G_M= unsafe.Offsetof(g{}.m) 19 | GOMODJAIL_OFFSET_M_LIBCALLPC = unsafe.Offsetof(m{}.libcallpc) 20 | GOMODJAIL_OFFSET_M_LIBCALLSP = unsafe.Offsetof(m{}.libcallsp) 21 | ) 22 | EOF 23 | 24 | cat <gomodjail_offset_print.go 25 | package main 26 | 27 | import ( 28 | "fmt" 29 | "runtime" 30 | ) 31 | 32 | func main() { 33 | fmt.Printf("/* %s */\n", runtime.Version()) 34 | fmt.Printf("#define GOMODJAIL_OFFSET_G_M %d\n", runtime.GOMODJAIL_OFFSET_G_M) 35 | fmt.Printf("#define GOMODJAIL_OFFSET_M_LIBCALLPC %d\n", runtime.GOMODJAIL_OFFSET_M_LIBCALLPC) 36 | fmt.Printf("#define GOMODJAIL_OFFSET_M_LIBCALLSP %d\n", runtime.GOMODJAIL_OFFSET_M_LIBCALLSP) 37 | } 38 | EOF 39 | 40 | "$GO" run ./gomodjail_offset_print.go 41 | 42 | rm -f "${GOROOT}"/src/runtime/gomodjail_offset.go gomodjail_offset_print.go 43 | -------------------------------------------------------------------------------- /pkg/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/AkihiroSuda/gomodjail/pkg/env" 10 | ) 11 | 12 | func ExecutableDir() (string, error) { 13 | selfPath, err := os.Executable() 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | cacheHome, err := Home() 19 | if err != nil { 20 | return "", fmt.Errorf("failed to resolve GOMODJAIL_CACHE_HOME: %w", err) 21 | } 22 | 23 | selfPathDigest := sha256sum([]byte(selfPath)) 24 | selfPathDigestPartial := selfPathDigest[0:16] 25 | 26 | dir := filepath.Join(cacheHome, selfPathDigestPartial) 27 | return dir, nil 28 | } 29 | 30 | func sha256sum(b []byte) string { 31 | h := sha256.New() 32 | if _, err := h.Write(b); err != nil { 33 | panic(err) 34 | } 35 | return fmt.Sprintf("%x", h.Sum(nil)) 36 | } 37 | 38 | // Home candidates are: 39 | // - $GOMODJAIL_CACHE_HOME 40 | // - $XDG_CACHE_HOME/gomodjail 41 | func Home() (string, error) { 42 | if cacheHome := os.Getenv(env.CacheHome); cacheHome != "" { 43 | return cacheHome, nil 44 | } 45 | osCacheHome, err := os.UserCacheDir() 46 | if err != nil { 47 | return "", err 48 | } 49 | cacheHome := filepath.Join(osCacheHome, "gomodjail") 50 | return cacheHome, nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/profile/profile_test.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestProfile(t *testing.T) { 10 | prof := New() 11 | const mainMod = "example.com/blah/v2" 12 | prof.Module = mainMod 13 | prof.Modules["example.com/foo"] = PolicyConfined 14 | prof.Modules["example.com/foobaz"] = PolicyConfined 15 | assert.NilError(t, prof.Validate()) 16 | assert.DeepEqual(t, &Confinment{ 17 | Module: "example.com/foo", 18 | Policy: PolicyConfined, 19 | }, prof.Confined(mainMod, "example.com/foo")) 20 | assert.DeepEqual(t, &Confinment{ 21 | Module: "example.com/foo", 22 | Policy: PolicyConfined, 23 | }, prof.Confined(mainMod, "example.com/foo/bar")) 24 | assert.DeepEqual(t, &Confinment{ 25 | Module: "example.com/foo", 26 | Policy: PolicyConfined, 27 | }, prof.Confined(mainMod, "example.com/foo.fn")) 28 | assert.DeepEqual(t, &Confinment{ 29 | Module: "example.com/foo", 30 | Policy: PolicyConfined, 31 | }, prof.Confined(mainMod, "example.com/foo/bar.fn")) 32 | assert.Assert(t, prof.Confined(mainMod, "example.com/foobar.fn") == nil) 33 | assert.DeepEqual(t, &Confinment{ 34 | Module: "example.com/foobaz", 35 | Policy: PolicyConfined, 36 | }, prof.Confined(mainMod, "example.com/foobaz.fn")) 37 | assert.Assert(t, prof.Confined(mainMod, "example.com/baz.fn") == nil) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/procutil/procutil_linux.go: -------------------------------------------------------------------------------- 1 | package procutil 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "strings" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func ReadUint64(pid int, addr uintptr) (uint64, error) { 12 | buf := make([]byte, 8) 13 | if _, err := unix.PtracePeekData(pid, addr, buf); err != nil { 14 | return 0, fmt.Errorf("failed to read 0x%x (%d bytes) from PID %d", addr, len(buf), pid) 15 | } 16 | return binary.NativeEndian.Uint64(buf), nil 17 | } 18 | 19 | func ReadString(pid int, addr uintptr, bufSize int) (string, error) { 20 | if addr == 0 { 21 | return "", nil 22 | } 23 | buf := make([]byte, bufSize) 24 | c, err := unix.PtracePeekData(pid, addr, buf) 25 | if err != nil { 26 | return "", fmt.Errorf("failed to read 0x%x (%d bytes) from PID %d", addr, bufSize, pid) 27 | } 28 | buf = buf[:c] 29 | nilIdx := strings.Index(string(buf), "\x00") 30 | if nilIdx < 0 { 31 | return "", fmt.Errorf("nil byte was not found in the %d bytes", c) 32 | } 33 | return string(buf[:nilIdx]), nil 34 | } 35 | 36 | func WaitForStopSignal(pid int) (int, unix.Signal, error) { 37 | var ws unix.WaitStatus 38 | wPid, err := unix.Wait4(pid, &ws, unix.WALL, nil) 39 | if err != nil { 40 | return 0, 0, err 41 | } 42 | if !ws.Stopped() { 43 | return 0, 0, fmt.Errorf("expected to be stopped (wPid=%d, ws=0x%x)", wPid, ws) 44 | } 45 | return wPid, ws.StopSignal(), nil 46 | } 47 | -------------------------------------------------------------------------------- /cmd/gomodjail/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "runtime/debug" 5 | "strconv" 6 | ) 7 | 8 | // Version can be fulfilled on compilation time: -ldflags="-X github.com/AkihiroSuda/gomodjail/cmd/gomodjail/version.Version=v0.1.2" 9 | var Version string 10 | 11 | func GetVersion() string { 12 | if Version != "" { 13 | return Version 14 | } 15 | const unknown = "(unknown)" 16 | bi, ok := debug.ReadBuildInfo() 17 | if !ok { 18 | return unknown 19 | } 20 | 21 | /* 22 | * go install example.com/cmd/foo@vX.Y.Z: bi.Main.Version="vX.Y.Z", vcs.revision is unset 23 | * go install example.com/cmd/foo@latest: bi.Main.Version="vX.Y.Z", vcs.revision is unset 24 | * go install example.com/cmd/foo@master: bi.Main.Version="vX.Y.Z-N.yyyyMMddhhmmss-gggggggggggg", vcs.revision is unset 25 | * go install ./cmd/foo: bi.Main.Version="(devel)", vcs.revision="gggggggggggggggggggggggggggggggggggggggg" 26 | * vcs.time="yyyy-MM-ddThh:mm:ssZ", vcs.modified=("false"|"true") 27 | */ 28 | if bi.Main.Version != "" && bi.Main.Version != "(devel)" { 29 | return bi.Main.Version 30 | } 31 | var ( 32 | vcsRevision string 33 | vcsModified bool 34 | ) 35 | for _, f := range bi.Settings { 36 | switch f.Key { 37 | case "vcs.revision": 38 | vcsRevision = f.Value 39 | case "vcs.modified": 40 | vcsModified, _ = strconv.ParseBool(f.Value) 41 | } 42 | } 43 | if vcsRevision == "" { 44 | return unknown 45 | } 46 | v := vcsRevision 47 | if vcsModified { 48 | v += ".m" 49 | } 50 | return v 51 | } 52 | -------------------------------------------------------------------------------- /pkg/unwinder/unwinder_linux.go: -------------------------------------------------------------------------------- 1 | // Package unwider provides unwinder for Go call stacks. 2 | // 3 | // https://www.grant.pizza/blog/go-stack-traces-bpf/ 4 | package unwinder 5 | 6 | import ( 7 | "debug/elf" 8 | 9 | "github.com/AkihiroSuda/gomodjail/pkg/procutil" 10 | ) 11 | 12 | const ( 13 | gopclntabSectionName = ".gopclntab" 14 | textSectionName = ".text" 15 | gosymtabSectionName = ".gosymtab" 16 | ) 17 | 18 | type elfObjectFile struct { 19 | *elf.File 20 | } 21 | 22 | func (e *elfObjectFile) Section(name string) Section { 23 | section := e.File.Section(name) 24 | if section == nil { 25 | return nil 26 | } 27 | return &elfSection{Section: section} 28 | } 29 | 30 | type elfSection struct { 31 | *elf.Section 32 | } 33 | 34 | func (s *elfSection) Addr() uint64 { 35 | return s.Section.Addr 36 | } 37 | 38 | func openObjectFile(path string) (ObjectFile, error) { 39 | f, err := elf.Open(path) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return &elfObjectFile{File: f}, nil 44 | } 45 | 46 | func (u *unwinder) unwind(pid int, pc, bp uintptr) ([]Entry, error) { 47 | var res []Entry 48 | frameCount := 0 49 | const maxFrames = 1024 50 | for bp != 0 && frameCount < maxFrames { 51 | // FIXME: read the memory regions at once 52 | savedBp, err := procutil.ReadUint64(pid, bp) 53 | if err != nil { 54 | return res, err 55 | } 56 | retAddr, err := procutil.ReadUint64(pid, bp+wordSize) 57 | if err != nil { 58 | return res, err 59 | } 60 | var ent Entry 61 | ent.File, ent.Line, ent.Func = u.symtab.PCToLine(uint64(pc)) 62 | if ent.Func != nil { 63 | res = append(res, ent) 64 | } 65 | pc = uintptr(retAddr) 66 | bp = uintptr(savedBp) 67 | frameCount++ 68 | } 69 | return res, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/child/child_linux.go: -------------------------------------------------------------------------------- 1 | package child 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | 10 | "github.com/AkihiroSuda/gomodjail/pkg/env" 11 | "github.com/AkihiroSuda/gomodjail/pkg/profile/seccompprofile" 12 | seccomp "github.com/elastic/go-seccomp-bpf" 13 | "github.com/elastic/go-seccomp-bpf/arch" 14 | ) 15 | 16 | func withoutUnknownSyscalls(ss []string) []string { 17 | archInfo, err := arch.GetInfo("") 18 | if err != nil { 19 | panic(err) 20 | } 21 | var res []string 22 | for _, s := range ss { 23 | if _, ok := archInfo.SyscallNames[s]; ok { 24 | res = append(res, s) 25 | } else { 26 | slog.Debug("Ignoring syscall not supported on this arch", 27 | "syscallName", s, "arch", archInfo.ID.String()) 28 | } 29 | } 30 | return res 31 | } 32 | 33 | func Main(args []string) error { 34 | if os.Geteuid() == 0 { 35 | // seccompprofile is not ready to cover privileged syscalls yet. 36 | slog.Warn("gomodjail should not be executed as the root (yet)") 37 | } 38 | arg0, err := exec.LookPath(args[0]) 39 | if err != nil { 40 | return err 41 | } 42 | _ = os.Unsetenv(env.PrivateChild) 43 | 44 | seccompPolicy := seccomp.Policy{ 45 | DefaultAction: seccomp.ActionTrace, 46 | Syscalls: []seccomp.SyscallGroup{ 47 | { 48 | Names: withoutUnknownSyscalls(seccompprofile.AlwaysAllowed), 49 | Action: seccomp.ActionAllow, 50 | }, 51 | }, 52 | } 53 | seccompFilter := seccomp.Filter{ 54 | NoNewPrivs: true, 55 | Flag: seccomp.FilterFlagTSync, 56 | Policy: seccompPolicy, 57 | } 58 | if err := seccomp.LoadFilter(seccompFilter); err != nil { 59 | return fmt.Errorf("failed to load the seccomp filter: %w", err) 60 | } 61 | 62 | if err := syscall.Exec(arg0, args, os.Environ()); err != nil { 63 | return fmt.Errorf("failed to execute %q %v: %w", arg0, args, err) 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/profile/profile.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "slices" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | type Policy = string 12 | 13 | const ( 14 | PolicyUnconfined = "unconfined" 15 | PolicyConfined = "confined" 16 | ) 17 | 18 | var KnownPolicies = []Policy{ 19 | PolicyUnconfined, 20 | PolicyConfined, 21 | } 22 | 23 | func New() *Profile { 24 | return &Profile{ 25 | Modules: make(map[string]Policy), 26 | moduleMismatchWarnOnce: make(map[string]struct{}), 27 | } 28 | } 29 | 30 | type Profile struct { 31 | Module string // the "module" line of go.mod 32 | Modules map[string]Policy // the "require" lines of go.mod TODO: rename to "Requires"? "Dependencies"? 33 | 34 | moduleMismatchWarnOnce map[string]struct{} 35 | moduleMismatchWarnOnceMu sync.RWMutex 36 | } 37 | 38 | func (p *Profile) Validate() error { 39 | if p.Module == "" { 40 | slog.Warn("No module was specified") 41 | } 42 | if len(p.Modules) == 0 { 43 | slog.Warn("No policy was specified") 44 | } 45 | 46 | for k, v := range p.Modules { 47 | if !slices.Contains(KnownPolicies, v) { 48 | return fmt.Errorf("unknown policy %q was specified for module %q", v, k) 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | type Confinment struct { 55 | Module string 56 | Policy Policy 57 | } 58 | 59 | func (p *Profile) Confined(mainMod, sym string) *Confinment { 60 | if mainMod != p.Module { 61 | k := mainMod + "," + p.Module 62 | p.moduleMismatchWarnOnceMu.RLock() 63 | _, warned := p.moduleMismatchWarnOnce[k] 64 | p.moduleMismatchWarnOnceMu.RUnlock() 65 | if !warned { 66 | slog.Warn("module mismatch", "a", mainMod, "b", p.Module) 67 | p.moduleMismatchWarnOnceMu.Lock() 68 | p.moduleMismatchWarnOnce[k] = struct{}{} 69 | p.moduleMismatchWarnOnceMu.Unlock() 70 | } 71 | return nil 72 | } 73 | for module, policy := range p.Modules { 74 | switch policy { 75 | case PolicyConfined: 76 | if sym == module || strings.HasPrefix(sym, module+"/") || strings.HasPrefix(sym, module+".") { 77 | return &Confinment{ 78 | Module: module, 79 | Policy: policy, 80 | } 81 | } 82 | } 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /cmd/gomodjail/commands/run/run.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | 9 | "github.com/AkihiroSuda/gomodjail/pkg/child" 10 | "github.com/AkihiroSuda/gomodjail/pkg/env" 11 | "github.com/AkihiroSuda/gomodjail/pkg/parent" 12 | "github.com/AkihiroSuda/gomodjail/pkg/profile" 13 | "github.com/AkihiroSuda/gomodjail/pkg/profile/fromgomod" 14 | "github.com/spf13/cobra" 15 | "golang.org/x/mod/modfile" 16 | ) 17 | 18 | func Example() string { 19 | return "TBD" 20 | } 21 | 22 | func New() *cobra.Command { 23 | cmd := &cobra.Command{ 24 | Use: "run COMMAND...", 25 | Short: "Run a Go program with confinement", 26 | Example: Example(), 27 | Args: cobra.MinimumNArgs(1), 28 | RunE: action, 29 | DisableFlagsInUseLine: true, 30 | } 31 | flags := cmd.Flags() 32 | flags.String("go-mod", "", "go.mod file with comment lines like `gomodjail:confined`") 33 | flags.StringToString("policy", nil, "e.g., example.com/module=confined") 34 | flags.Bool("no-policy", false, "Allow running without any policy (useful only for debugging)") 35 | return cmd 36 | } 37 | 38 | func action(cmd *cobra.Command, args []string) error { 39 | flags := cmd.Flags() 40 | if _, ok := os.LookupEnv(env.PrivateChild); ok { 41 | return child.Main(args) 42 | } 43 | prof := profile.New() 44 | flagGoMod, err := flags.GetString("go-mod") 45 | if err != nil { 46 | return err 47 | } 48 | if flagGoMod != "" { 49 | goModB, err := os.ReadFile(flagGoMod) 50 | if err != nil { 51 | return err 52 | } 53 | goModFile, err := modfile.Parse(flagGoMod, goModB, nil) 54 | if err != nil { 55 | return err 56 | } 57 | if err = fromgomod.FromGoMod(goModFile, prof); err != nil { 58 | return fmt.Errorf("failed to read profile from %q: %w", flagGoMod, err) 59 | } 60 | } 61 | flagPolicy, err := flags.GetStringToString("policy") 62 | if err != nil { 63 | return err 64 | } 65 | for k, v := range flagPolicy { 66 | if oldV, ok := prof.Modules[k]; ok && oldV != v { 67 | slog.Warn("Overwriting policy", "module", k, "old", oldV, "new", v) 68 | } 69 | prof.Modules[k] = v 70 | } 71 | flagNoPolicy, err := flags.GetBool("no-policy") 72 | if err != nil { 73 | return err 74 | } 75 | if !flagNoPolicy && len(prof.Modules) == 0 { 76 | return errors.New("no policy was specified (Hint: specify --go-mod=FILE, or --policy=MODULE=POLICY)") 77 | } 78 | if err = prof.Validate(); err != nil { 79 | return err 80 | } 81 | return parent.Main(prof, args) 82 | } 83 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Forked from https://github.com/containerd/nerdctl/blob/v0.8.1/.github/workflows/release.yml 2 | # Apache License 2.0 3 | 4 | name: Release 5 | on: 6 | push: 7 | branches: 8 | - 'master' 9 | tags: 10 | - 'v*' 11 | pull_request: 12 | branches: 13 | - 'master' 14 | jobs: 15 | release: 16 | env: 17 | GOTOOLCHAIN: local 18 | # The macOS runner can cross-compile Linux binaries, but not vice versa. 19 | runs-on: macos-26 20 | timeout-minutes: 20 21 | # The maximum access is "read" for PRs from public forked repos 22 | # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token 23 | permissions: 24 | contents: write # for releases 25 | id-token: write # for provenances 26 | attestations: write # for provenances 27 | steps: 28 | - uses: actions/checkout@v6 29 | with: 30 | # https://github.com/reproducible-containers/repro-get/issues/3 31 | fetch-depth: 0 32 | ref: ${{ github.event.pull_request.head.sha }} 33 | - uses: actions/setup-go@v6 34 | with: 35 | go-version: 1.24.x 36 | - name: "Make artifacts" 37 | run: make artifacts 38 | - name: "SHA256SUMS" 39 | run: | 40 | cat _artifacts/SHA256SUMS 41 | - name: "The sha256sum of the SHA256SUMS file" 42 | run: | 43 | (cd _artifacts; sha256sum SHA256SUMS) 44 | - name: "Prepare the release note" 45 | run: | 46 | shasha=$(sha256sum _artifacts/SHA256SUMS | awk '{print $1}') 47 | cat <<-EOF | tee /tmp/release-note.txt 48 | (Changes to be documented) 49 | 50 | - - - 51 | The binaries were built automatically on GitHub Actions. 52 | The build log is available for 90 days: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 53 | 54 | The sha256sum of the SHA256SUMS file itself is \`${shasha}\` . 55 | EOF 56 | - uses: actions/attest-build-provenance@v3 57 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 58 | with: 59 | subject-path: _artifacts/* 60 | - name: "Create release" 61 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | run: | 65 | tag="${GITHUB_REF##*/}" 66 | gh release create -F /tmp/release-note.txt --draft --title "${tag}" "${tag}" _artifacts/* 67 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/elastic/go-seccomp-bpf v1.6.0 h1:NYduiYxRJ0ZkIyQVwlSskcqPPSg6ynu5pK0/d7SQATs= 5 | github.com/elastic/go-seccomp-bpf v1.6.0/go.mod h1:5tFsTvH4NtWGfpjsOQD53H8HdVQ+zSZFRUDSGevC0Kc= 6 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 7 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 9 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 13 | github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 14 | github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 15 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 16 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 17 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 18 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 19 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 20 | golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= 21 | golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 22 | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= 23 | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 24 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 25 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= 30 | gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 31 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - 'release/**' 7 | pull_request: 8 | jobs: 9 | main: 10 | env: 11 | GOTOOLCHAIN: local 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - runner: ubuntu-24.04 # Intel 17 | go: 1.25.x 18 | build_mode: "" 19 | - runner: ubuntu-24.04 # Intel 20 | go: 1.25.x 21 | build_mode: "strip" 22 | - runner: macos-15-intel # Intel 23 | go: 1.25.x 24 | build_mode: "" 25 | - runner: macos-26 # ARM 26 | # libgomodjail_hook_darwin is sensitive to Go version 27 | go: 1.24.x 28 | build_mode: "" 29 | - runner: macos-26 # ARM 30 | go: 1.25.x 31 | build_mode: "" 32 | runs-on: ${{ matrix.runner }} 33 | timeout-minutes: 10 34 | steps: 35 | - uses: actions/checkout@v6 36 | - uses: actions/setup-go@v6 37 | with: 38 | go-version: ${{ matrix.go }} 39 | - name: Install 40 | run: | 41 | set -eux 42 | make 43 | sudo make install 44 | - name: Unit tests 45 | run: go test -v ./... 46 | - name: Run golangci-lint 47 | uses: golangci/golangci-lint-action@v9 48 | - name: Smoke test 49 | timeout-minutes: 5 50 | env: 51 | BUILD_MODE: ${{ matrix.build_mode }} 52 | run: | 53 | set -eux 54 | cd examples/victim 55 | if [ "${BUILD_MODE}" = "strip" ]; then 56 | go build -ldflags="-s -w" 57 | else 58 | go build 59 | fi 60 | # Unpacked mode 61 | gomodjail run --go-mod=go.mod -- ./victim 62 | # Packed mode 63 | gomodjail pack --go-mod=go.mod ./victim 64 | ./victim.gomodjail 65 | if [ "$(find /tmp -maxdepth 1 -type d -name 'gomodjail*' | awk 'END{print NR}')" != "0" ]; then 66 | echo >&2 "tmp files are leaked" 67 | exit 1 68 | fi 69 | - name: "Smoke test: docker (not dockerd)" 70 | if: runner.os == 'Linux' 71 | timeout-minutes: 5 72 | run: | 73 | set -eux 74 | DOCKER="gomodjail run --go-mod=./examples/profiles/docker.mod -- docker" 75 | $DOCKER buildx create --name foo --use 76 | cat <.gomodjail)") 35 | return cmd 36 | } 37 | 38 | func action(cmd *cobra.Command, args []string) error { 39 | flags := cmd.Flags() 40 | flagGoMod, err := flags.GetString("go-mod") 41 | if err != nil { 42 | return err 43 | } 44 | if flagGoMod == "" { 45 | return errors.New("needs --go-mod") 46 | } 47 | prog := args[0] 48 | flagOutput, err := flags.GetString("output") 49 | if err != nil { 50 | return err 51 | } 52 | if flagOutput == "" { 53 | flagOutput = filepath.Base(prog) + ".gomodjail" 54 | } 55 | 56 | slog.Info("Creating a self-extract archive", "file", flagOutput) 57 | out, err := os.OpenFile(flagOutput, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) 58 | if err != nil { 59 | return err 60 | } 61 | defer out.Close() //nolint:errcheck 62 | 63 | selfExe, err := os.Executable() 64 | if err != nil { 65 | return err 66 | } 67 | selfExeF, err := os.Open(selfExe) 68 | if err != nil { 69 | return err 70 | } 71 | defer selfExeF.Close() //nolint:errcheck 72 | if _, err := io.Copy(out, selfExeF); err != nil { 73 | return err 74 | } 75 | 76 | zw := zip.NewWriter(out) 77 | defer zw.Close() //nolint:errcheck 78 | if runtime.GOOS == "darwin" { 79 | libgomodjailHook, err := tracer.LibgomodjailHook() 80 | if err != nil { 81 | return err 82 | } 83 | if err := ziputil.WriteFileWithPath(zw, libgomodjailHook, "libgomodjail_hook_darwin.dylib"); err != nil { 84 | return err 85 | } 86 | } 87 | if err := ziputil.WriteFileWithPath(zw, prog, filepath.Base(prog)); err != nil { 88 | return err 89 | } 90 | if err := ziputil.WriteFileWithPath(zw, flagGoMod, "go.mod"); err != nil { 91 | return err 92 | } 93 | if err := zw.SetComment(ziputil.SelfExtractArchiveComment); err != nil { 94 | return err 95 | } 96 | if err := zw.Close(); err != nil { 97 | return err 98 | } 99 | 100 | return out.Close() 101 | } 102 | -------------------------------------------------------------------------------- /pkg/profile/seccompprofile/seccompprofile_linux.go: -------------------------------------------------------------------------------- 1 | package seccompprofile 2 | 3 | // AlwaysAllowed is the list of the syscalls that do not need to be traced. 4 | // For the performance reason, FD operations like read and write are always allowed. 5 | // 6 | // https://filippo.io/linux-syscall-table/ 7 | var AlwaysAllowed = []string{ 8 | // ===== 000 ===== 9 | "read", 10 | "write", 11 | "close", 12 | "lstat", 13 | "poll", 14 | "lseek", 15 | "mmap", 16 | "mprotect", 17 | "munmap", 18 | "brk", 19 | "rt_sigaction", 20 | "rt_sigprocmask", 21 | "rt_sigreturn", 22 | "pread64", 23 | "pwrite64", 24 | "readv", 25 | "writev", 26 | "access", 27 | "pipe", 28 | "select", 29 | "sched_yield", 30 | "mremap", 31 | "msync", 32 | "mincore", 33 | "madvise", 34 | "dup", 35 | "dup2", 36 | "pause", 37 | "nanosleep", 38 | "getitimer", 39 | "alarm", 40 | "setitimer", 41 | "getpid", 42 | "sendfile", 43 | "clone", 44 | "exit", 45 | "wait4", 46 | "uname", 47 | "fsync", 48 | "ftruncate", 49 | "getdents", 50 | "getcwd", 51 | "chdir", 52 | "fchdir", 53 | "gettimeofday", 54 | "getrlimit", 55 | "getrusage", 56 | "sysinfo", 57 | // ===== 100 ===== 58 | "times", 59 | "getuid", 60 | "syslog", 61 | "getgid", 62 | "setuid", 63 | "setgid", 64 | "geteuid", 65 | "getegid", 66 | "setpgid", 67 | "getppid", 68 | "getpgrp", 69 | "setsid", 70 | "setreuid", 71 | "setregid", 72 | "getgroups", 73 | "setgroups", 74 | "setresuid", 75 | "getresuid", 76 | "setresgid", 77 | "getresgid", 78 | "getpgid", 79 | "setfsuid", 80 | "setfsgid", 81 | "getsid", 82 | "capget", 83 | "capset", 84 | "rt_sigpending", 85 | "rt_sigtimedwait", 86 | "rt_sigqueueinfo", 87 | "rt_sigsuspend", 88 | "sigaltstack", 89 | "utime", 90 | "ustat", 91 | "statfs", 92 | "fstatfs", 93 | "sysfs", 94 | "getpriority", 95 | "setpriority", 96 | "sched_setparam", 97 | "sched_getparam", 98 | "sched_setscheduler", 99 | "sched_getscheduler", 100 | "sched_get_priority_max", 101 | "sched_get_priority_min", 102 | "sched_rr_get_interval", 103 | "mlock", 104 | "munlock", 105 | "mlockall", 106 | "munlockall", 107 | "sync", 108 | "acct", 109 | "gettid", 110 | "readahead", 111 | // ===== 200 ===== 112 | "tkill", 113 | "time", 114 | "futex", 115 | "sched_setaffinity", 116 | "sched_getaffinity", 117 | "set_thread_area", 118 | "get_thread_area", 119 | "epoll_create", 120 | "timer_create", 121 | "timer_gettime", 122 | "timer_getoverrun", 123 | "timer_delete", 124 | "clock_gettime", 125 | "clock_getres", 126 | "clock_nanosleep", 127 | "exit_group", 128 | "epoll_wait", 129 | "epoll_ctl", 130 | "tgkill", 131 | "newfstatat", 132 | "faccessat", 133 | "epoll_pwait", 134 | "eventfd", 135 | "eventfd2", 136 | "epoll_create1", 137 | "dup3", 138 | "pipe2", 139 | "preadv", 140 | "pwritev", 141 | // ===== 300 ===== 142 | "sched_setattr", 143 | "sched_getattr", 144 | "seccomp", 145 | "getrandom", 146 | "memfd_create", 147 | "preadv2", 148 | "pwritev2", 149 | // ===== 400 ===== 150 | "clone3", 151 | "faccessat2", 152 | "epoll_pwait2", 153 | "landlock_create_ruleset", 154 | "landlock_add_rule", 155 | "landlock_restrict_self", 156 | "memfd_secret", 157 | "futex_waitv", 158 | "futex_wake", 159 | "futex_wait", 160 | "futex_requeue", 161 | } 162 | 163 | // TODO: more 164 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Files are installed under $(DESTDIR)/$(PREFIX) 2 | PREFIX ?= /usr/local 3 | DEST := $(shell echo "$(DESTDIR)/$(PREFIX)" | sed 's:///*:/:g; s://*$$::') 4 | 5 | VERSION ?=$(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags) 6 | VERSION_SYMBOL := github.com/AkihiroSuda/gomodjail/cmd/gomodjail/version.Version 7 | 8 | export SOURCE_DATE_EPOCH ?= $(shell git log -1 --pretty=%ct) 9 | SOURCE_DATE_EPOCH_TOUCH := $(shell date -r $(SOURCE_DATE_EPOCH) +%Y%m%d%H%M.%S) 10 | 11 | GO ?= go 12 | GO_LDFLAGS ?= -s -w -X $(VERSION_SYMBOL)=$(VERSION) 13 | GO_BUILD ?= $(GO) build -trimpath -ldflags="$(GO_LDFLAGS)" 14 | GOOS ?= $(shell $(GO) env GOOS) 15 | GOARCH ?= $(shell $(GO) env GOARCH) 16 | 17 | STATIC ?= 18 | ifeq ($(STATIC),1) 19 | GO_LDFLAGS += -extldflags -static 20 | export CGO_ENABLED=0 21 | endif 22 | 23 | BINARIES := _output/bin/gomodjail 24 | ifeq ($(GOOS),darwin) 25 | BINARIES += _output/lib/libgomodjail_hook_darwin.dylib 26 | ifneq (,$(findstring arm64,$(GOARCH))) 27 | CFLAGS += -arch arm64 28 | else ifneq (,$(findstring amd64,$(GOARCH))) 29 | CFLAGS += -arch x86_64 30 | endif 31 | endif 32 | 33 | TAR ?= tar 34 | 35 | .PHONY: all 36 | all: binaries 37 | 38 | .PHONY: binaries 39 | binaries: $(BINARIES) 40 | 41 | .PHONY: _output/bin/gomodjail 42 | _output/bin/gomodjail: 43 | $(GO_BUILD) -o $@ ./cmd/gomodjail 44 | 45 | %.o: %.c *.h 46 | $(CC) $(CFLAGS) -c $< -o $@ 47 | 48 | _output/lib/libgomodjail_hook_darwin.dylib: $(patsubst %.c, %.o, $(wildcard libgomodjail_hook_darwin/*.c)) 49 | mkdir -p _output/lib 50 | $(CC) $(CFLAGS) -o $@ $(LDFLAGS) -ldl -dynamiclib $^ 51 | 52 | .PHONY: install 53 | install: uninstall 54 | mkdir -p "$(DEST)/bin" 55 | cp -a _output/bin/gomodjail "$(DEST)/bin/gomodjail" 56 | ifeq ($(GOOS),darwin) 57 | mkdir -p "$(DEST)/lib" 58 | cp -a _output/lib/libgomodjail_hook_darwin.dylib "$(DEST)/lib/libgomodjail_hook_darwin.dylib" 59 | endif 60 | 61 | .PHONY: uninstall 62 | uninstall: 63 | rm -f "$(DEST)/bin/gomodjail" 64 | ifeq ($(GOOS),darwin) 65 | rm -f "$(DEST)/lib/libgomodjail_hook_darwin.dylib" 66 | endif 67 | 68 | # clean does not remove _artifacts 69 | .PHONY: clean 70 | clean: 71 | rm -rf _output libgomodjail_hook_darwin/*.o 72 | 73 | define touch_recursive 74 | find "$(1)" -exec touch -t $(SOURCE_DATE_EPOCH_TOUCH) {} + 75 | endef 76 | 77 | define make_artifact 78 | make clean 79 | GOOS=$(1) GOARCH=$(2) make 80 | $(call touch_recursive,_output) 81 | $(TAR) -C _output/ --no-xattrs --numeric-owner --uid 0 --gid 0 --option !timestamp -czvf _artifacts/gomodjail-$(VERSION).$(1)-$(2).tar.gz ./ 82 | endef 83 | 84 | # Needs to be executed on macOS 85 | .PHONY: artifacts 86 | artifacts: 87 | rm -rf _artifacts 88 | mkdir -p _artifacts 89 | $(call make_artifact,linux,amd64) 90 | $(call make_artifact,linux,arm64) 91 | $(call make_artifact,darwin,amd64) 92 | $(call make_artifact,darwin,arm64) 93 | make clean 94 | go version | tee _artifacts/build-env.txt 95 | echo --- >> _artifacts/build-env.txt 96 | sw_vers | tee -a _artifacts/build-env.txt 97 | echo --- >> _artifacts/build-env.txt 98 | pkgutil --pkg-info=com.apple.pkg.CLTools_Executables | tee -a _artifacts/build-env.txt 99 | echo --- >> _artifacts/build-env.txt 100 | $(CC) --version | tee -a _artifacts/build-env.txt 101 | (cd _artifacts ; sha256sum *) > SHA256SUMS 102 | mv SHA256SUMS _artifacts/SHA256SUMS 103 | $(call touch_recursive,_artifacts) 104 | -------------------------------------------------------------------------------- /pkg/ziputil/ziputil.go: -------------------------------------------------------------------------------- 1 | package ziputil 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/fs" 9 | "log/slog" 10 | "math" 11 | "os" 12 | "path/filepath" 13 | 14 | "golang.org/x/sys/unix" 15 | ) 16 | 17 | // SelfExtractArchiveComment is the comment present in the End Of Central Directory Record. 18 | const SelfExtractArchiveComment = "gomodjail-self-extract-archive" 19 | 20 | func WriteFileWithPath(zw *zip.Writer, filePath, name string) error { 21 | f, err := os.Open(filePath) 22 | if err != nil { 23 | return err 24 | } 25 | defer f.Close() //nolint:errcheck 26 | if err = WriteFile(zw, f, name); err != nil { 27 | return err 28 | } 29 | return f.Close() 30 | } 31 | 32 | func WriteFile(zw *zip.Writer, f fs.File, name string) error { 33 | st, err := f.Stat() 34 | if err != nil { 35 | return err 36 | } 37 | if sz := st.Size(); sz > math.MaxUint32 { 38 | return fmt.Errorf("file size must not exceed MaxUint32, got %d", sz) 39 | } 40 | fh, err := zip.FileInfoHeader(st) 41 | if err != nil { 42 | return err 43 | } 44 | if name != "" { 45 | fh.Name = name 46 | } 47 | w, err := zw.CreateHeader(fh) 48 | if err != nil { 49 | return err 50 | } 51 | if _, err := io.Copy(w, f); err != nil { 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | func FindSelfExtractArchive() (*zip.ReadCloser, error) { 58 | selfExe, err := os.Executable() 59 | if err != nil { 60 | return nil, err 61 | } 62 | zr, err := zip.OpenReader(selfExe) 63 | if err != nil { 64 | if errors.Is(err, zip.ErrFormat) { 65 | return nil, nil 66 | } 67 | return nil, err 68 | } 69 | if zr.Comment != SelfExtractArchiveComment { 70 | return nil, fmt.Errorf("expected comment %q, got %q", SelfExtractArchiveComment, zr.Comment) 71 | } 72 | return zr, nil 73 | } 74 | 75 | func Unzip(dir string, zr *zip.ReadCloser) ([]fs.FileInfo, error) { 76 | if err := os.MkdirAll(dir, 0o755); err != nil { 77 | return nil, err 78 | } 79 | 80 | dirFile, err := os.Open(dir) 81 | if err != nil { 82 | return nil, err 83 | } 84 | defer dirFile.Close() //nolint:errcheck 85 | 86 | if err = flock(dirFile, unix.LOCK_EX); err != nil { 87 | slog.Warn("failed to lock dir", "path", dir, "error", err) 88 | } else { 89 | defer func() { 90 | if err = flock(dirFile, unix.LOCK_UN); err != nil { 91 | slog.Warn("failed to unlock dir", "path", dir, "error", err) 92 | } 93 | }() 94 | } 95 | 96 | res := make([]fs.FileInfo, len(zr.File)) 97 | for i, f := range zr.File { 98 | if err := unzip1(dir, f); err != nil { 99 | return res, err 100 | } 101 | res[i] = f.FileInfo() 102 | } 103 | return res, nil 104 | } 105 | 106 | func unzip1(dir string, f *zip.File) error { 107 | fi := f.FileInfo() 108 | if !fi.Mode().IsRegular() { 109 | // No need to support directories 110 | return fmt.Errorf("unexpected non-regular file: %q", fs.FormatFileInfo(fi)) 111 | } 112 | r, err := f.Open() 113 | if err != nil { 114 | return err 115 | } 116 | defer r.Close() //nolint:errcheck 117 | baseName := filepath.Base(f.Name) 118 | if baseName != f.Name { 119 | return fmt.Errorf("unexpected file: %q", fs.FormatFileInfo(fi)) 120 | } 121 | wPath := filepath.Join(dir, baseName) 122 | modTime := fi.ModTime() 123 | if st, err := os.Stat(wPath); err == nil && st.ModTime().Equal(modTime) && modTime.UnixNano() != 0 { 124 | // TODO: compare digest too (via xattr? fs-verity?) 125 | slog.Debug("already exists", "path", wPath, "modTime", modTime) 126 | return nil 127 | } 128 | 129 | // for atomicity 130 | wPathTmp := fmt.Sprintf("%s.pid-%d", wPath, os.Getpid()) 131 | defer os.RemoveAll(wPathTmp) //nolint:errcheck 132 | 133 | w, err := os.OpenFile(wPathTmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) 134 | if err != nil { 135 | return err 136 | } 137 | defer w.Close() //nolint:errcheck 138 | if _, err = io.Copy(w, r); err != nil { 139 | return err 140 | } 141 | for _, x := range []io.Closer{w, r} { 142 | if err = x.Close(); err != nil { 143 | return err 144 | } 145 | } 146 | if err = os.Chtimes(wPathTmp, modTime, modTime); err != nil { 147 | return err 148 | } 149 | if err = os.RemoveAll(wPath); err != nil { 150 | return err 151 | } 152 | if err = os.Rename(wPathTmp, wPath); err != nil { 153 | return err 154 | } 155 | return nil 156 | } 157 | 158 | func flock(f *os.File, flags int) error { 159 | fd := int(f.Fd()) 160 | for { 161 | err := unix.Flock(fd, flags) 162 | if err == nil || err != unix.EINTR { 163 | return err 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /pkg/tracer/tracer_darwin.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "debug/buildinfo" 5 | "encoding/binary" 6 | "encoding/json" 7 | "fmt" 8 | "log/slog" 9 | "net" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "sync" 14 | 15 | "github.com/AkihiroSuda/gomodjail/pkg/profile" 16 | ) 17 | 18 | func LibgomodjailHook() (string, error) { 19 | hookDylib := os.Getenv("LIBGOMODJAIL_HOOK") 20 | if hookDylib == "" { 21 | self, err := os.Executable() 22 | if err != nil { 23 | return "", err 24 | } 25 | binDir := filepath.Dir(self) // /usr/local/bin 26 | localDir := filepath.Dir(binDir) // /usr/local 27 | libDir := filepath.Join(localDir, "lib") // /usr/local/lib 28 | hookDylib = filepath.Join(libDir, "libgomodjail_hook_darwin.dylib") 29 | } 30 | if _, err := os.Stat(hookDylib); err != nil { 31 | return "", err 32 | } 33 | return hookDylib, nil 34 | } 35 | 36 | func New(cmd *exec.Cmd, profile *profile.Profile) (Tracer, error) { 37 | tmpDir, err := os.MkdirTemp("", "gomodjail") 38 | if err != nil { 39 | return nil, err 40 | } 41 | sock := filepath.Join(tmpDir, "sock") 42 | ln, err := net.Listen("unix", sock) 43 | if err != nil { 44 | return nil, err 45 | } 46 | hookDylib, err := LibgomodjailHook() 47 | if err != nil { 48 | return nil, err 49 | } 50 | cmd.Env = append(os.Environ(), 51 | "DYLD_INSERT_LIBRARIES="+hookDylib, 52 | "LIBGOMODJAIL_HOOK_SOCKET="+sock, 53 | ) 54 | 55 | tracer := &tracer{ 56 | cmd: cmd, 57 | profile: profile, 58 | ln: ln, 59 | tmpDir: tmpDir, 60 | mainModules: make(map[string]string), 61 | } 62 | for k, v := range profile.Modules { 63 | slog.Debug("Loading profile", "module", k, "policy", v) 64 | } 65 | return tracer, nil 66 | } 67 | 68 | type tracer struct { 69 | cmd *exec.Cmd 70 | profile *profile.Profile 71 | ln net.Listener 72 | tmpDir string 73 | mainModules map[string]string // key: filename, value: main module 74 | mu sync.RWMutex 75 | } 76 | 77 | // Trace traces the process. 78 | func (tracer *tracer) Trace() error { 79 | go func() { 80 | for { 81 | c, err := tracer.ln.Accept() 82 | if err != nil { 83 | slog.Error("failed to accept", "error", err) 84 | break 85 | } 86 | go func() { 87 | if err := tracer.handlerConn(c); err != nil { 88 | slog.Error("failed to handle connection", "error", err) 89 | } 90 | }() 91 | } 92 | }() 93 | err := tracer.cmd.Start() 94 | if err != nil { 95 | return err 96 | } 97 | return tracer.cmd.Wait() 98 | } 99 | 100 | type requestStackEntry struct { 101 | Address uint64 `json:"address,omitempty"` 102 | 103 | File string `json:"file,omitempty"` 104 | Symbol string `json:"symbol,omitempty"` 105 | } 106 | 107 | type request struct { 108 | Pid int `json:"pid"` 109 | Exe string `json:"exe"` 110 | Syscall string `json:"syscall"` 111 | Stack []requestStackEntry `json:"stack,omitempty"` 112 | } 113 | 114 | func (tracer *tracer) handlerConn(c net.Conn) error { 115 | defer c.Close() //nolint:errcheck 116 | jsonLenB := make([]byte, 4) 117 | if _, err := c.Read(jsonLenB); err != nil { 118 | return err 119 | } 120 | jsonLen := binary.NativeEndian.Uint32(jsonLenB) 121 | if jsonLen > (1 << 16) { 122 | return fmt.Errorf("invalid json length: %d", jsonLen) 123 | } 124 | jsonB := make([]byte, jsonLen) 125 | if _, err := c.Read(jsonB); err != nil { 126 | return err 127 | } 128 | var req request 129 | if err := json.Unmarshal(jsonB, &req); err != nil { 130 | return fmt.Errorf("failed to unmarshal %q: %w", string(jsonB), err) 131 | } 132 | slog.Debug("handling request", "req", req) 133 | 134 | tracer.mu.RLock() 135 | mainModule := tracer.mainModules[req.Exe] 136 | tracer.mu.RUnlock() 137 | if mainModule == "" { 138 | buildInfo, err := buildinfo.ReadFile(req.Exe) 139 | if err != nil { 140 | return err 141 | } 142 | mainModule = buildInfo.Main.Path 143 | tracer.mu.Lock() 144 | tracer.mainModules[req.Exe] = mainModule 145 | tracer.mu.Unlock() 146 | } 147 | 148 | allow := true 149 | for _, e := range req.Stack { 150 | if cf := tracer.profile.Confined(mainModule, e.Symbol); cf != nil { 151 | slog.Warn("***Blocked***", "pid", req.Pid, "exe", req.Exe, "syscall", req.Syscall, "entry", e, "module", cf.Module) 152 | allow = false 153 | break 154 | } 155 | } 156 | 157 | respB := []byte{1, 0, 0, 0, '1'} // little endian 158 | if !allow { 159 | respB[4] = '0' 160 | } 161 | if _, err := c.Write(respB); err != nil { 162 | return err 163 | } 164 | return nil 165 | } 166 | 167 | func (tracer *tracer) Close() error { 168 | return os.RemoveAll(tracer.tmpDir) 169 | } 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [[📂**Profiles**]](./examples/profiles) 2 | 3 | # gomodjail: jail for Go modules 4 | 5 | gomodjail imposes syscall restrictions on a specific set of Go modules, 6 | so as to mitigate their potential vulnerabilities and supply chain attack vectors. 7 | 8 | In other words, gomodjail is a "container" (as in Docker containers) for Go modules. 9 | 10 | gomodjail can be applied just in the following two steps: 11 | 12 | **Step 1**: add `gomodjail:confined` comment to `go.mod`: 13 | ```go-module 14 | require ( 15 | example.com/module v1.0.0 // gomodjail:confined 16 | ) 17 | ``` 18 | 19 | **Step2**: run the program with `gomodjail run --go-mod=go.mod`: 20 | ```bash 21 | gomodjail run --go-mod=FILE -- PROG [ARGS]... 22 | ``` 23 | 24 | > [!IMPORTANT] 25 | > 26 | > See [Caveats](#caveats). 27 | 28 | ## Requirements 29 | Runtime dependencies: 30 | - Linux (4.8 or later) or macOS 31 | - x86\_64 (aka "amd64") or aarch64 ("arm64") 32 | 33 | Build dependencies: 34 | - [Go](https://go.dev/dl/) 35 | 36 | ## Install 37 | ```bash 38 | make 39 | sudo make install 40 | ``` 41 | 42 | Makefile variables: 43 | - `PREFIX`: installation prefix (default: `/usr/local`) 44 | 45 | ## Example 46 | An example program is located in [`./examples/victim`](./examples/victim): 47 | ```bash 48 | cd ./examples/victim 49 | go build 50 | ./victim 51 | ``` 52 | 53 | Confirm the "malicious" vi screen: 54 | 55 | ``` 56 | *** ARBITRARY SHELL CODE EXECUTION *** 57 | 58 | This 'vi' command was executed by the 'github.com/AkihiroSuda/gomodjail/examples/poisoned' module. 59 | 60 | This example is harmless, of course, but suppose that this was a malicious code. 61 | 62 | Type ':q!' to leave this screen. 63 | ``` 64 | 65 | Run the program again with `gomodjail run --go-mod=go.mod`, and confirm that the execution of the "malicious" `vi` command is blocked. 66 | 67 | ```bash 68 | gomodjail run --go-mod=go.mod -- ./victim 69 | level=WARN msg=***Blocked*** syscall=pidfd_open module=github.com/AkihiroSuda/gomodjail/examples/poisoned 70 | ``` 71 | 72 | ### More examples 73 | 74 | [`examples/profiles`](./examples/profiles) has several example profiles: 75 | - `docker.mod`: for `docker` (not `dockerd`) 76 | - ... 77 | 78 | ## Caveats 79 | - Not applicable to a Go binary built by non-trustworthy thirdparty, as the symbol information might be faked. 80 | - Not applicable to a Go module that use: 81 | - [`unsafe`](https://pkg.go.dev/unsafe) 82 | - [`reflect`](https://pkg.go.dev/reflect) 83 | - [`plugin`](https://pkg.go.dev/plugin) 84 | - [`go:linkname`](https://tip.golang.org/doc/go1.23#linker) 85 | - [C](https://pkg.go.dev/cmd/cgo) 86 | - [Assembly](https://go.dev/doc/asm) 87 | - No isolation of file descriptors across modules. 88 | A confined module can still read/write an existing file descriptor, although it cannot open a new file descriptor. 89 | - The target binary file must not be replaced during execution. 90 | - The `gomodjail:confined` policy is not well defined and still subject to change. 91 | - This is not a panacea; there can be other loopholes too. 92 | 93 | macOS: 94 | - Not applicable to a Go binary built with `-ldflags="-s"` (disable symbol table) 95 | - The protection can be arbitraliry disabled by unsetting an environment variable `DYLD_INSERT_LIBRARIES`. 96 | - Only works with the following versions of Go: 97 | - 1.22 98 | - 1.23 99 | - 1.24 100 | - 1.25 101 | - Not applicable to a Go module that use: 102 | - [`syscall.Syscall`, `syscall.RawSyscall`, etc.](https://pkg.go.dev/syscall) 103 | 104 | ## Advanced topics 105 | ### Advanced usage 106 | - To create a self-extract archive of gomodjail with a target program, run `gomodjail pack --go-mod=go.mod PROGRAM`. 107 | The self-extract archive is created as `.gomodjail`. 108 | 109 | ### How it works 110 | Linux: 111 | - [`SECCOMP_RET_TRACE`](https://man7.org/linux/man-pages/man2/seccomp.2.html) is used for conditionally 112 | allowing trusted Go modules to execute the syscall. 113 | `SECCOMP_RET_USER_NOTIF` is not used because it cannot access all the CPU registers, 114 | due to the [lack of `struct pt_regs` in `struct seccomp_data`](https://github.com/torvalds/linux/blob/v6.12/kernel/seccomp.c#L242-L266). 115 | - [Stack unwinding](https://www.grant.pizza/blog/go-stack-traces-bpf/) is used for analyzing the call stack to determine the Go module. 116 | 117 | macOS: 118 | - `DYLD_INSERT_LIBRARIES` is used to hook `libSystem` (`libc`) calls. 119 | - In addition to the frame pointer (AArch64 register X29), `struct g` in the TLS and `g->m.libcallsp` are parsed to analyze the CGO call stack. 120 | This analysis is not robust and only works with specific versions of Go. (See [Caveats](#caveats)). 121 | 122 | ### Future works 123 | - Automatically detect non-applicable modules (explained in [Caveats](#caveats)). 124 | - Apply landlock in addition to seccomp. Depends on `SECCOMP_IOCTL_NOTIF_ADDFD`. 125 | - Modify the source code of the Go runtime, so as to remove necessity of using `seccomp` (Linux) and `DYLD_INSERT_LIBRARIES` (macOS). 126 | 127 | ## Additional documents 128 | - [`docs/syntax.md`](./docs/syntax.md): syntax 129 | - [`examples/profiles/README.md`](./examples/profiles/README.md): profiles 130 | -------------------------------------------------------------------------------- /cmd/gomodjail/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io/fs" 7 | "log/slog" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | 13 | "github.com/AkihiroSuda/gomodjail/cmd/gomodjail/commands/pack" 14 | "github.com/AkihiroSuda/gomodjail/cmd/gomodjail/commands/run" 15 | "github.com/AkihiroSuda/gomodjail/cmd/gomodjail/version" 16 | "github.com/AkihiroSuda/gomodjail/pkg/cache" 17 | "github.com/AkihiroSuda/gomodjail/pkg/env" 18 | "github.com/AkihiroSuda/gomodjail/pkg/envutil" 19 | "github.com/AkihiroSuda/gomodjail/pkg/osargs" 20 | "github.com/AkihiroSuda/gomodjail/pkg/tracer" 21 | "github.com/AkihiroSuda/gomodjail/pkg/ziputil" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var logLevel = new(slog.LevelVar) 26 | 27 | func main() { 28 | if exitCode := xmain(); exitCode != 0 { 29 | os.Exit(exitCode) 30 | } 31 | } 32 | 33 | func xmain() int { 34 | logHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}) 35 | slog.SetDefault(slog.New(logHandler)) 36 | rootCmd := newRootCommand() 37 | if _, ok := os.LookupEnv(env.PrivateChild); !ok { 38 | zr, err := ziputil.FindSelfExtractArchive() 39 | if err != nil { 40 | slog.Error("error while detecting self-extract archive", "error", err) 41 | } 42 | if zr != nil { 43 | err := configureSelfExtractMode(rootCmd, zr) 44 | if cErr := zr.Close(); cErr != nil { 45 | slog.Error("failed to call closer", "error", cErr) 46 | } 47 | if err != nil { 48 | slog.Error("exiting with an error while setting up self-extract mode", "error", err) 49 | return 1 50 | } 51 | } 52 | } 53 | err := rootCmd.Execute() 54 | if err != nil { 55 | exitCode := 1 56 | if exitErr, ok := err.(*exec.ExitError); ok { 57 | if ps := exitErr.ProcessState; ps != nil { 58 | exitCode = ps.ExitCode() 59 | } 60 | } else if exitErr, ok := err.(*tracer.ExitError); ok { 61 | exitCode = exitErr.ExitCode 62 | } 63 | if exitCode != 0 { 64 | slog.Debug("exiting with an error", "error", err, "exitCode", exitCode) 65 | } else { 66 | slog.Debug("exiting") 67 | } 68 | return exitCode 69 | } 70 | return 0 71 | } 72 | 73 | func newRootCommand() *cobra.Command { 74 | cmd := &cobra.Command{ 75 | Use: "gomodjail", 76 | Short: "Jail for go modules", 77 | Example: run.Example(), 78 | Version: version.GetVersion(), 79 | Args: cobra.NoArgs, 80 | SilenceUsage: true, 81 | SilenceErrors: true, 82 | } 83 | flags := cmd.PersistentFlags() 84 | flags.Bool("debug", envutil.Bool("DEBUG", false), "debug mode [$DEBUG]") 85 | 86 | cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { 87 | if debug, _ := cmd.Flags().GetBool("debug"); debug { 88 | logLevel.Set(slog.LevelDebug) 89 | if _, ok := os.LookupEnv("DEBUG"); !ok { 90 | // Parsed by libgomodjail_hook_darwin 91 | _ = os.Setenv("DEBUG", "1") 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | cmd.AddCommand( 98 | run.New(), 99 | pack.New(), 100 | ) 101 | return cmd 102 | } 103 | 104 | func configureSelfExtractMode(rootCmd *cobra.Command, zr *zip.ReadCloser) error { 105 | slog.Debug("Running in self-extract mode") 106 | 107 | // td must be kept on exit 108 | // https://github.com/containerd/nerdctl/pull/4012#issuecomment-2840539282 109 | td, err := cache.ExecutableDir() 110 | if err != nil { 111 | return err 112 | } 113 | slog.Debug("unpacking self-extract archive", "dir", td) 114 | fis, err := ziputil.Unzip(td, zr) 115 | if err != nil { 116 | return fmt.Errorf("failed to unzip to %q: %w", td, err) 117 | } 118 | var libgomodjailHookFI, progFI, goModFI fs.FileInfo 119 | switch runtime.GOOS { 120 | case "darwin": 121 | if len(fis) != 3 { 122 | return fmt.Errorf("expected an archive to contain 3 files (libgomodjail_hook_darwin.dylib, program and go.mod), got %d files", len(fis)) 123 | } 124 | libgomodjailHookFI, progFI, goModFI = fis[0], fis[1], fis[2] 125 | default: 126 | if len(fis) != 2 { 127 | return fmt.Errorf("expected an archive to contain 2 files (program and go.mod), got %d files", len(fis)) 128 | } 129 | progFI, goModFI = fis[0], fis[1] 130 | } 131 | if filepath.Base(progFI.Name()) != progFI.Name() { 132 | return fmt.Errorf("unexpected file name: %q", progFI.Name()) 133 | } 134 | if goModFI.Name() != "go.mod" { 135 | return fmt.Errorf("expected \"go.mod\", got %q", goModFI.Name()) 136 | } 137 | prog := filepath.Join(td, progFI.Name()) 138 | goMod := filepath.Join(td, goModFI.Name()) 139 | switch runtime.GOOS { 140 | case "darwin": 141 | if libgomodjailHookFI.Name() != "libgomodjail_hook_darwin.dylib" { 142 | return fmt.Errorf("expected \"libgomodjail_hook_darwin.dylib\", got %q", libgomodjailHookFI.Name()) 143 | } 144 | libgomodjailHook := filepath.Join(td, libgomodjailHookFI.Name()) 145 | if err = os.Setenv("LIBGOMODJAIL_HOOK", libgomodjailHook); err != nil { 146 | return err 147 | } 148 | } 149 | args := append([]string{os.Args[0], "run", "--go-mod=" + goMod, prog, "--"}, os.Args[1:]...) 150 | slog.Debug("Reconfiguring the top-level command", "args", args) 151 | rootCmd.SetArgs(args[1:]) 152 | osargs.SetOSArgs(args) 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /examples/profiles/docker-26.1.3.mod: -------------------------------------------------------------------------------- 1 | // Example profile for `docker` (Docker CLI, not Docker daemon) 2 | // 3 | // From https://github.com/docker/cli/blob/v26.1.3/vendor.mod 4 | // LICENSE: https://github.com/docker/cli/blob/v26.1.3/LICENSE (Apache License 2.0) 5 | 6 | // gomodjail:confined 7 | module github.com/docker/cli 8 | 9 | // 'vendor.mod' enables use of 'go mod vendor' to managed 'vendor/' directory. 10 | // There is no 'go.mod' file, as that would imply opting in for all the rules 11 | // around SemVer, which this repo cannot abide by as it uses CalVer. 12 | 13 | go 1.21 14 | 15 | require ( 16 | dario.cat/mergo v1.0.0 17 | github.com/containerd/containerd v1.7.15 18 | github.com/creack/pty v1.1.21 19 | github.com/distribution/reference v0.5.0 20 | github.com/docker/distribution v2.8.3+incompatible 21 | github.com/docker/docker v26.1.3-0.20240515073302-8e96db1c328d+incompatible // gomodjail:unconfined 22 | github.com/docker/docker-credential-helpers v0.8.1 23 | github.com/docker/go-connections v0.5.0 // gomodjail:unconfined 24 | github.com/docker/go-units v0.5.0 25 | github.com/fvbommel/sortorder v1.0.2 26 | github.com/gogo/protobuf v1.3.2 // gomodjail:unconfined 27 | github.com/google/go-cmp v0.6.0 28 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 29 | github.com/mattn/go-runewidth v0.0.15 30 | github.com/mitchellh/mapstructure v1.5.0 31 | github.com/moby/patternmatcher v0.6.0 32 | github.com/moby/swarmkit/v2 v2.0.0-20240227173239-911c97650f2e 33 | github.com/moby/sys/sequential v0.5.0 34 | github.com/moby/sys/signal v0.7.0 35 | github.com/moby/term v0.5.0 // gomodjail:unconfined 36 | github.com/morikuni/aec v1.0.0 37 | github.com/opencontainers/go-digest v1.0.0 38 | github.com/opencontainers/image-spec v1.1.0-rc5 39 | github.com/pkg/errors v0.9.1 40 | github.com/sirupsen/logrus v1.9.3 41 | github.com/spf13/cobra v1.8.0 // gomodjail:unconfined 42 | github.com/spf13/pflag v1.0.5 43 | github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a 44 | github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d 45 | github.com/xeipuuv/gojsonschema v1.2.0 46 | go.opentelemetry.io/otel v1.21.0 // gomodjail:unconfined 47 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 48 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 49 | go.opentelemetry.io/otel/metric v1.21.0 // gomodjail:unconfined 50 | go.opentelemetry.io/otel/sdk v1.21.0 // gomodjail:unconfined 51 | go.opentelemetry.io/otel/sdk/metric v1.21.0 // gomodjail:unconfined 52 | go.opentelemetry.io/otel/trace v1.21.0 53 | golang.org/x/sync v0.6.0 // gomodjail:unconfined 54 | golang.org/x/sys v0.18.0 // gomodjail:unconfined 55 | golang.org/x/term v0.18.0 56 | golang.org/x/text v0.14.0 57 | gopkg.in/yaml.v2 v2.4.0 58 | gotest.tools/v3 v3.5.1 59 | tags.cncf.io/container-device-interface v0.6.2 60 | ) 61 | 62 | require ( 63 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 64 | github.com/Microsoft/go-winio v0.6.1 // indirect 65 | github.com/Microsoft/hcsshim v0.11.4 // indirect 66 | github.com/beorn7/perks v1.0.1 // indirect 67 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 68 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 69 | github.com/containerd/log v0.1.0 // indirect 70 | github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect 71 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect 72 | github.com/docker/go-metrics v0.0.1 // indirect 73 | github.com/felixge/httpsnoop v1.0.4 // indirect 74 | github.com/go-logr/logr v1.4.1 // indirect 75 | github.com/go-logr/stdr v1.2.2 // indirect 76 | // gomodjail:unconfined 77 | github.com/golang/protobuf v1.5.4 // indirect 78 | github.com/gorilla/mux v1.8.1 // indirect 79 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect 80 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 81 | github.com/klauspost/compress v1.17.4 // indirect 82 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 83 | github.com/miekg/pkcs11 v1.1.1 // indirect 84 | github.com/moby/docker-image-spec v1.3.1 // indirect 85 | github.com/moby/sys/symlink v0.2.0 // indirect 86 | github.com/moby/sys/user v0.1.0 // indirect 87 | // gomodjail:unconfined 88 | github.com/prometheus/client_golang v1.17.0 // indirect 89 | github.com/prometheus/client_model v0.5.0 // indirect 90 | github.com/prometheus/common v0.44.0 // indirect 91 | github.com/prometheus/procfs v0.12.0 // indirect 92 | github.com/rivo/uniseg v0.2.0 // indirect 93 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 94 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 95 | go.etcd.io/etcd/raft/v3 v3.5.6 // indirect 96 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 97 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect 98 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 99 | golang.org/x/crypto v0.21.0 // indirect 100 | golang.org/x/mod v0.14.0 // indirect 101 | // gomodjail:unconfined 102 | golang.org/x/net v0.23.0 // indirect 103 | golang.org/x/time v0.3.0 // indirect 104 | golang.org/x/tools v0.16.0 // indirect 105 | google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect 106 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect 107 | // gomodjail:unconfined 108 | google.golang.org/grpc v1.60.1 // indirect 109 | // gomodjail:unconfined 110 | google.golang.org/protobuf v1.33.0 // indirect 111 | ) 112 | -------------------------------------------------------------------------------- /pkg/tracer/tracer_linux.go: -------------------------------------------------------------------------------- 1 | // Package tracer was forked from 2 | // https://github.com/AkihiroSuda/lsf/blob/ff4e43f59c5dc1a93c2b0e81b741bbc439c211da/pkg/tracer/tracer_linux.go 3 | package tracer 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "log/slog" 9 | "os" 10 | "os/exec" 11 | "runtime" 12 | 13 | "github.com/AkihiroSuda/gomodjail/pkg/procutil" 14 | "github.com/AkihiroSuda/gomodjail/pkg/profile" 15 | "github.com/AkihiroSuda/gomodjail/pkg/tracer/regs" 16 | "github.com/AkihiroSuda/gomodjail/pkg/unwinder" 17 | "github.com/elastic/go-seccomp-bpf/arch" 18 | "golang.org/x/sys/unix" 19 | ) 20 | 21 | func LibgomodjailHook() (string, error) { 22 | return "", errors.New("libgomodjail_hook is unused on linux") 23 | } 24 | 25 | func New(cmd *exec.Cmd, profile *profile.Profile) (Tracer, error) { 26 | selfExe, err := os.Executable() 27 | if err != nil { 28 | return nil, err 29 | } 30 | cmd.SysProcAttr = &unix.SysProcAttr{Ptrace: true} 31 | archInfo, err := arch.GetInfo("") 32 | if err != nil { 33 | return nil, err 34 | } 35 | tracer := &tracer{ 36 | cmd: cmd, 37 | profile: profile, 38 | selfExe: selfExe, 39 | pids: make(map[int]string), 40 | unwinders: make(map[string]unwinder.Unwinder), 41 | archInfo: archInfo, 42 | } 43 | for k, v := range profile.Modules { 44 | slog.Debug("Loading profile", "module", k, "policy", v) 45 | } 46 | return tracer, nil 47 | } 48 | 49 | type tracer struct { 50 | cmd *exec.Cmd 51 | profile *profile.Profile 52 | selfExe string 53 | pids map[int]string // key: pid, value: file name 54 | unwinders map[string]unwinder.Unwinder // key: file name 55 | archInfo *arch.Info 56 | } 57 | 58 | const commonPtraceOptions = unix.PTRACE_O_TRACEFORK | 59 | unix.PTRACE_O_TRACEVFORK | 60 | unix.PTRACE_O_TRACECLONE | 61 | unix.PTRACE_O_TRACEEXEC | 62 | unix.PTRACE_O_TRACEEXIT | 63 | unix.PTRACE_O_TRACESYSGOOD | 64 | unix.PTRACE_O_EXITKILL 65 | 66 | // Trace traces the process. 67 | func (tracer *tracer) Trace() error { 68 | runtime.LockOSThread() // required by SysProcAttr.Ptrace 69 | 70 | // TODO: propagate signals from parent (see RootlessKit's implementation) 71 | 72 | err := tracer.cmd.Start() 73 | if err != nil { 74 | return err 75 | } 76 | pGid, err := unix.Getpgid(tracer.cmd.Process.Pid) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | // Catch the birtycry before setting up the ptrace options 82 | wPid, sig, err := procutil.WaitForStopSignal(-1 * pGid) 83 | if err != nil { 84 | return err 85 | } 86 | if sig != unix.SIGTRAP { 87 | return fmt.Errorf("birthcry: expected SIGTRAP, got %+v", sig) 88 | } 89 | // Set up the ptrace options 90 | ptraceOptions := commonPtraceOptions | unix.PTRACE_O_TRACESECCOMP 91 | if err := unix.PtraceSetOptions(wPid, ptraceOptions); err != nil { 92 | return fmt.Errorf("failed to set ptrace options: %w", err) 93 | } 94 | // Restart the process stopped in the birthcry 95 | if err := unix.PtraceCont(wPid, 0); err != nil { 96 | return fmt.Errorf("failed to call PTRACE_CONT (pid=%d) %w", wPid, err) 97 | } 98 | 99 | for { 100 | if err = tracer.trace(pGid); err != nil { 101 | if errors.Is(err, unix.ESRCH) { 102 | slog.Debug("ESRCH", "error", err) 103 | } else { 104 | return err 105 | } 106 | } 107 | } 108 | } 109 | 110 | func (tracer *tracer) trace(pGid int) error { 111 | var ws unix.WaitStatus 112 | wPid, err := unix.Wait4(-1*pGid, &ws, unix.WALL, nil) 113 | if err != nil { 114 | return err 115 | } 116 | switch { 117 | case ws.Exited(): 118 | exitStatus := ws.ExitStatus() 119 | if wPid == tracer.cmd.Process.Pid { 120 | return &ExitError{ 121 | ExitCode: exitStatus, 122 | } 123 | } 124 | return nil 125 | } 126 | switch uint32(ws) >> 8 { 127 | case uint32(unix.SIGTRAP) | (unix.PTRACE_EVENT_SECCOMP << 8): 128 | var regs regs.Regs 129 | if err = unix.PtraceGetRegs(wPid, ®s.PtraceRegs); err != nil { 130 | return fmt.Errorf("failed to read registers for %d: %w", wPid, err) 131 | } 132 | if err = tracer.handleSyscall(wPid, ®s); err != nil { 133 | slog.Debug("failed to handle syscall", "pid", wPid, "syscall", regs.Syscall(), "error", err) 134 | } else { 135 | if regs.Modified { 136 | if err = unix.PtraceSetRegs(wPid, ®s.PtraceRegs); err != nil { 137 | return fmt.Errorf("failed to set registers for %d: %w", wPid, err) 138 | } 139 | } 140 | } 141 | } 142 | if err := unix.PtraceCont(wPid, 0); err != nil { 143 | return fmt.Errorf("failed to call PTRACE_CONT (pid=%d) %w", wPid, err) 144 | } 145 | return nil 146 | } 147 | 148 | func (tracer *tracer) handleSyscall(pid int, regs *regs.Regs) error { 149 | syscallNr := regs.Syscall() 150 | // FIXME: check the seccomp arch 151 | syscallName, ok := tracer.archInfo.SyscallNumbers[int(syscallNr)] 152 | if !ok { 153 | return fmt.Errorf("unknown syscall %d", syscallNr) 154 | } 155 | switch syscallName { 156 | case "execve", "execveat": 157 | defer func() { 158 | // the process image is going to change 159 | delete(tracer.pids, pid) 160 | }() 161 | } 162 | filename, ok := tracer.pids[pid] 163 | if !ok { 164 | var err error 165 | filename, err = os.Readlink(fmt.Sprintf("/proc/%d/exe", pid)) 166 | if err != nil { 167 | return err 168 | } 169 | tracer.pids[pid] = filename 170 | } 171 | if filename == tracer.selfExe { 172 | return nil 173 | } 174 | uw, ok := tracer.unwinders[filename] 175 | if !ok { 176 | var err error 177 | uw, err = unwinder.New(filename) 178 | if err != nil { // No gosymtab 179 | tracer.unwinders[filename] = nil 180 | return err 181 | } 182 | tracer.unwinders[filename] = uw 183 | slog.Debug("registered an executable", "exe", filename, "mainModule", uw.BuildInfo().Main.Path) 184 | } 185 | if uw == nil { // No gosymtab 186 | return nil 187 | } 188 | entries, err := uw.Unwind(pid, uintptr(regs.PC()), uintptr(regs.FramePointer())) 189 | if err != nil { 190 | return err 191 | } 192 | buildInfo := uw.BuildInfo() 193 | slog.Debug("handler", "pid", pid, "exe", filename, "syscall", syscallName) 194 | for i, e := range entries { 195 | slog.Debug("stack", "entryNo", i, "entry", e.String()) 196 | pkgName := e.Func.PackageName() 197 | if cf := tracer.profile.Confined(buildInfo.Main.Path, pkgName); cf != nil { 198 | slog.Warn("***Blocked***", "pid", pid, "exe", filename, "syscall", syscallName, "entry", e.String(), "module", cf.Module) 199 | ret := -1 * int(unix.EPERM) 200 | regs.SetRet(uint64(ret)) 201 | regs.SetSyscall(unix.SYS_GETPID) // Only needed on amd64? 202 | return nil 203 | } 204 | } 205 | return nil 206 | } 207 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /libgomodjail_hook_darwin/libgomodjail_hook_darwin.c: -------------------------------------------------------------------------------- 1 | /* TODO: split to multiple files */ 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #define STR_EQ(x, y) ((x) != NULL && (y) != NULL && strcmp((x), (y)) == 0) 26 | #define STR_HAS_PREFIX(x, y) \ 27 | ((x) != NULL && (y) != NULL && strncmp((x), (y), strlen(y)) == 0) 28 | 29 | #ifdef DEBUG 30 | static bool debug = true; 31 | #else 32 | static bool debug = false; 33 | #endif 34 | 35 | #define ERRORF(fmt, ...) \ 36 | fprintf(stderr, "GOMODJAIL::ERROR| " fmt "\n", ##__VA_ARGS__); 37 | 38 | #define WARNF(fmt, ...) \ 39 | fprintf(stderr, "GOMODJAIL::WARN | " fmt "\n", ##__VA_ARGS__); 40 | 41 | #define DEBUGF(fmt, ...) \ 42 | do { \ 43 | if (debug) \ 44 | fprintf(stderr, "GOMODJAIL::DEBUG| " fmt "\n", ##__VA_ARGS__); \ 45 | } while (0) 46 | 47 | static bool enabled = false; 48 | 49 | /* TODO: move to go_runtime_info */ 50 | static char exe_path[MAXPATHLEN]; 51 | static uint32_t exe_path_len = MAXPATHLEN; 52 | 53 | struct go_runtime_offset { 54 | char *version_prefix; 55 | off_t g_m; 56 | off_t m_libcallpc; 57 | off_t m_libcallsp; 58 | }; 59 | 60 | static struct go_runtime_offset go_runtime_offsets[] = { 61 | /* When updating the list, update README.md too */ 62 | { 63 | /* go1.25 */ 64 | .version_prefix = "go1.25", 65 | .g_m = 48, 66 | .m_libcallpc = 872, 67 | .m_libcallsp = 880, 68 | }, 69 | 70 | { 71 | /* go1.24.0 */ 72 | .version_prefix = "go1.24", 73 | .g_m = 48, 74 | .m_libcallpc = 856, 75 | .m_libcallsp = 864, 76 | }, 77 | { 78 | /* go1.23.6 */ 79 | .version_prefix = "go1.23", 80 | .g_m = 48, 81 | .m_libcallpc = 832, 82 | .m_libcallsp = 840, 83 | }, 84 | { 85 | /* go1.22.12 */ 86 | .version_prefix = "go1.22", 87 | .g_m = 48, 88 | .m_libcallpc = 1048, 89 | .m_libcallsp = 1056, 90 | }, 91 | { 92 | .version_prefix = NULL, 93 | .g_m = 0, 94 | .m_libcallpc = 0, 95 | .m_libcallsp = 0, 96 | }, 97 | }; 98 | 99 | /* TODO: move to go_runtime_info */ 100 | static struct go_runtime_offset *go_runtime_offset_current = NULL; 101 | 102 | struct go_runtime_info { 103 | char *version; /* *NOT* freeable */ 104 | uint64_t *tls_g_addr; 105 | }; 106 | 107 | static struct go_runtime_info go_runtime_info; 108 | 109 | // The result is *NOT* freeable. 110 | static char *get_go_version_from_go_buildinfo_buf(uint8_t *buf) { 111 | char *res = NULL; 112 | if (memcmp(buf, "\xff Go buildinf:", 14) != 0) { 113 | ERRORF("expected __go_buildinfo to have a valid magic"); 114 | goto done; 115 | } 116 | if (buf[14] != 8) { 117 | ERRORF("expected __go_buildinfo[14] (pointer size) to be 8, got %d", 118 | buf[14]); 119 | goto done; 120 | } 121 | if (buf[15] != 2) { 122 | ERRORF("expected __go_buildinfo[15] (endianness) to be 2, got %d", buf[15]); 123 | goto done; 124 | } 125 | uint8_t varint0 = buf[32]; 126 | if ((varint0 & 0x80) != 0) { 127 | ERRORF("expected __go_buildinfo[32] (varint of go version length) not to " 128 | "have the continuation bit, got %d", 129 | varint0); 130 | goto done; 131 | } 132 | static char res_buf[128]; 133 | memcpy(res_buf, buf + 33, varint0); 134 | res = res_buf; 135 | done: 136 | return res; 137 | } 138 | 139 | static struct go_runtime_info get_go_runtime_info_from_file_buf(void *buf) { 140 | struct go_runtime_info res; 141 | memset(&res, 0, sizeof(res)); 142 | struct mach_header_64 *mh = (struct mach_header_64 *)buf; 143 | if (mh->magic != MH_MAGIC_64) { 144 | /* TODO: support FAT_MAGIC? */ 145 | ERRORF("expected MH_MAGIC_64, got %d", mh->magic); 146 | goto done; 147 | } 148 | struct load_command *lc = (struct load_command *)(buf + sizeof(*mh)); 149 | uint32_t i; 150 | for (i = 0; i < mh->ncmds; i++) { 151 | if (lc->cmd == LC_SEGMENT_64) { 152 | struct segment_command_64 *seg = (struct segment_command_64 *)lc; 153 | struct section_64 *sect = 154 | (struct section_64 *)((uint8_t *)seg + sizeof(*seg)); 155 | for (uint32_t s = 0; s < seg->nsects; s++) { 156 | if (strncmp(sect[s].sectname, "__go_buildinfo", 14) == 0) { 157 | res.version = 158 | get_go_version_from_go_buildinfo_buf(buf + sect[s].offset); 159 | } 160 | } 161 | } else if (lc->cmd == LC_SYMTAB) { 162 | struct symtab_command *stcmd = (struct symtab_command *)lc; 163 | char *strtab = (char *)buf + stcmd->stroff; 164 | struct nlist_64 *symtab = 165 | (struct nlist_64 *)((uint8_t *)buf + stcmd->symoff); 166 | for (uint32_t j = 0; j < stcmd->nsyms; j++) { 167 | char *name = strtab + symtab[j].n_un.n_strx; 168 | if (STR_EQ(name, "_runtime.tls_g")) { 169 | int image_index = 1; /* FIXME: parse */ 170 | uint64_t runtime_tls_g_sym_value = (uint64_t)symtab[j].n_value; 171 | res.tls_g_addr = 172 | (uint64_t *)(_dyld_get_image_vmaddr_slide(image_index) + 173 | runtime_tls_g_sym_value); 174 | } 175 | } 176 | } 177 | lc = (struct load_command *)((uint8_t *)lc + lc->cmdsize); 178 | } 179 | done: 180 | return res; 181 | } 182 | 183 | static struct go_runtime_info get_go_runtime_info_from_file(const char *path) { 184 | struct go_runtime_info res; 185 | memset(&res, 0, sizeof(res)); 186 | void *mm = NULL; 187 | size_t mm_len = -1; 188 | int fd = open(path, O_RDONLY); 189 | if (fd < 0) { 190 | ERRORF("open(\"%s\") failed: %s", path, strerror(errno)); 191 | goto done; 192 | } 193 | struct stat st; 194 | if (fstat(fd, &st) < 0) { 195 | ERRORF("fstat(\"%s\") failed: %s", path, strerror(errno)); 196 | goto done; 197 | } 198 | mm_len = (size_t)st.st_size; 199 | mm = mmap(NULL, mm_len, PROT_READ, MAP_PRIVATE, fd, 0); 200 | if (mm == NULL) { 201 | ERRORF("mmap failed: %s", strerror(errno)); 202 | goto done; 203 | } 204 | close(fd); 205 | fd = -1; 206 | res = get_go_runtime_info_from_file_buf(mm); 207 | done: 208 | if (fd >= 0) 209 | close(fd); 210 | if (mm != NULL) 211 | munmap(mm, mm_len); 212 | return res; 213 | } 214 | 215 | static void init() __attribute__((constructor)); 216 | 217 | static void init() { 218 | debug = getenv("DEBUG") != NULL; 219 | if (_NSGetExecutablePath(exe_path, &exe_path_len) != 0) { 220 | ERRORF("_NSGetExecutablePath() failed"); 221 | return; 222 | } 223 | go_runtime_info = get_go_runtime_info_from_file(exe_path); 224 | char *go_version = go_runtime_info.version; /* Not freeable */ 225 | if (!go_version) { 226 | WARNF("%s: Not a Go binary. Ignoring.", exe_path); 227 | return; 228 | } 229 | DEBUGF("%s: Go version=\"%s\"", exe_path, go_version); 230 | for (int i = 0; go_runtime_offsets[i].version_prefix; i++) { 231 | if (STR_HAS_PREFIX(go_version, go_runtime_offsets[i].version_prefix)) { 232 | DEBUGF("%s: treating Go version \"%s\" as %s", exe_path, go_version, 233 | go_runtime_offsets[i].version_prefix); 234 | DEBUGF("%s: Go runtime offsets: g->m: %lld, m->libcallpc: %lld, " 235 | "m->libcallsp: %lld", 236 | exe_path, go_runtime_offsets[i].g_m, 237 | go_runtime_offsets[i].m_libcallpc, 238 | go_runtime_offsets[i].m_libcallsp); 239 | go_runtime_offset_current = &go_runtime_offsets[i]; 240 | break; 241 | } 242 | } 243 | if (!go_runtime_offset_current) { 244 | ERRORF("%s: Unsupported Go version: \"%s\"", exe_path, go_version); 245 | return; 246 | } 247 | enabled = true; 248 | } 249 | 250 | #if defined(__aarch64__) 251 | static uint64_t fetch_g() { 252 | uintptr_t tls_base; 253 | __asm__ __volatile__("mrs %0, tpidrro_el0" : "=r"(tls_base)); 254 | tls_base &= ~((uintptr_t)7); 255 | uint64_t runtime_tls_g = *go_runtime_info.tls_g_addr; 256 | return *(uint64_t *)(tls_base + runtime_tls_g); 257 | } 258 | #define BP_ADJUSTMENT 8 259 | #elif defined(__x86_64__) 260 | static uint64_t fetch_g() { 261 | uintptr_t g; 262 | /* https://github.com/golang/go/issues/23617 */ 263 | __asm__ __volatile__("movq %%gs:0x30, %0" : "=r"(g)); 264 | return g; 265 | } 266 | #define BP_ADJUSTMENT 16 267 | #endif 268 | 269 | /* Returns true if execution is allowed */ 270 | static bool handle_syscall(const char *syscall_name) { 271 | bool res = true; 272 | int sock = -1; 273 | char *json_buf = NULL; 274 | size_t json_len = -1; 275 | FILE *json_fp = NULL; 276 | 277 | if (!enabled) { 278 | DEBUGF("Handler is not enabled"); 279 | goto done; 280 | } 281 | 282 | if (!go_runtime_offset_current) { 283 | DEBUGF("Go runtime is not recognized"); 284 | goto done; 285 | } 286 | 287 | { 288 | struct sockaddr_un addr; 289 | char *sock_path = getenv("LIBGOMODJAIL_HOOK_SOCKET"); 290 | if (sock_path == NULL) { 291 | ERRORF("LIBGOMODJAIL_HOOK_SOCKET is unset"); 292 | goto done; 293 | } 294 | if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) { 295 | ERRORF("socket() failed: %s", strerror(errno)); 296 | goto done; 297 | } 298 | memset(&addr, 0, sizeof(addr)); 299 | addr.sun_family = PF_UNIX; 300 | strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); 301 | if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { 302 | ERRORF("connect() failed: %s", strerror(errno)); 303 | goto done; 304 | } 305 | } 306 | 307 | if ((json_fp = open_memstream(&json_buf, &json_len)) == NULL) { 308 | ERRORF("open_memstream() failed: %s", strerror(errno)); 309 | goto done; 310 | } 311 | 312 | fprintf(json_fp, "{\"pid\":%d,\"exe\":\"%s\",\"syscall\":\"%s\",\"stack\":[", 313 | getpid(), exe_path, syscall_name); 314 | 315 | { 316 | void *callstack[128]; 317 | int frames = backtrace(callstack, sizeof(callstack) / sizeof(callstack[0])); 318 | for (int i = 0; i < frames; ++i) { 319 | Dl_info dli; 320 | if (dladdr(callstack[i], &dli) > 0) { 321 | DEBUGF("* %s\t%s", dli.dli_fname, dli.dli_sname); 322 | fprintf(json_fp, "{\"file\":\"%s\",\"symbol\":\"%s\"},", dli.dli_fname, 323 | dli.dli_sname); 324 | if (STR_EQ(dli.dli_sname, "runtime.asmcgocall.abi0")) { 325 | uint64_t g_addr = fetch_g(); 326 | if (!g_addr) { 327 | ERRORF("!g_addr"); 328 | break; 329 | } 330 | uint64_t m_addr_addr = g_addr + go_runtime_offset_current->g_m; 331 | uint64_t m_addr = *(uint64_t *)m_addr_addr; 332 | if (!m_addr) { 333 | ERRORF("!m_addr"); 334 | break; 335 | } 336 | uint64_t libcallpc_addr = 337 | m_addr + go_runtime_offset_current->m_libcallpc; 338 | uint64_t libcallsp_addr = 339 | m_addr + go_runtime_offset_current->m_libcallsp; 340 | uint64_t pc = *(uint64_t *)libcallpc_addr; 341 | uint64_t sp = *(uint64_t *)libcallsp_addr; 342 | if (sp) { 343 | uint64_t bp = sp - BP_ADJUSTMENT; 344 | while (bp != 0) { 345 | uint64_t saved_bp = *(uint64_t *)bp; 346 | uint64_t ret_addr = *(uint64_t *)(bp + 8); 347 | Dl_info dli2; 348 | if (dladdr((void *)pc, &dli2) > 0) { 349 | DEBUGF("* %s\t%s", dli2.dli_fname, dli2.dli_sname); 350 | fprintf(json_fp, "{\"file\":\"%s\",\"symbol\":\"%s\"},", 351 | dli2.dli_fname, dli2.dli_sname); 352 | } 353 | pc = ret_addr; 354 | bp = saved_bp; 355 | } 356 | } 357 | } 358 | } else { 359 | DEBUGF("* %p", callstack[i]); 360 | fprintf(json_fp, "{\"address\":%lld},", (uint64_t)callstack[i]); 361 | } 362 | } 363 | } 364 | 365 | /* A terminator entry is added to simplify the trailing comma logic */ 366 | fprintf(json_fp, "{}]}"); 367 | fclose(json_fp); 368 | json_fp = NULL; 369 | 370 | { 371 | uint32_t json_len32 = (uint32_t)json_len; 372 | if (write(sock, &json_len32, sizeof(json_len32)) < 0) { 373 | ERRORF("write() failed: %s", strerror(errno)); 374 | goto done; 375 | } 376 | if (write(sock, json_buf, json_len) < 0) { 377 | ERRORF("write() failed: %s", strerror(errno)); 378 | goto done; 379 | } 380 | } 381 | 382 | { 383 | uint8_t resp[5]; 384 | if (read(sock, resp, sizeof(resp)) < 0) { 385 | ERRORF("read() failed: %s", strerror(errno)); 386 | goto done; 387 | } 388 | /* TODO: parse JSON */ 389 | res = resp[4] != '0'; 390 | } 391 | done: 392 | if (sock >= 0) 393 | close(sock); 394 | if (json_fp != NULL) 395 | fclose(json_fp); 396 | if (json_buf != NULL) 397 | free(json_buf); 398 | return res; 399 | } 400 | 401 | #define INTERPOSE(fn, hook) \ 402 | __attribute__(( \ 403 | used, section("__DATA,__interpose"))) static void *interpose_##fn[] = { \ 404 | hook, fn} 405 | 406 | static int open_needs_mode(int flags) { return flags & O_CREAT; } 407 | 408 | #define HOOK_OPEN(func, args, call_no_mode, call_mode, fmt, ...) \ 409 | static int gmj_##func args { \ 410 | DEBUGF(fmt, __VA_ARGS__); \ 411 | if (handle_syscall(#func)) { \ 412 | if (open_needs_mode(flags)) { \ 413 | va_list ap; \ 414 | va_start(ap, flags); \ 415 | int mode = va_arg(ap, int); \ 416 | va_end(ap); \ 417 | return call_mode; \ 418 | } \ 419 | return call_no_mode; \ 420 | } \ 421 | errno = EPERM; \ 422 | return -1; \ 423 | } \ 424 | INTERPOSE(func, gmj_##func) 425 | 426 | #define HOOK(func, signature, args, fmt, ...) \ 427 | static int gmj_##func signature { \ 428 | DEBUGF(fmt, __VA_ARGS__); \ 429 | if (handle_syscall(#func)) { \ 430 | return func args; \ 431 | } \ 432 | errno = EPERM; \ 433 | return -1; \ 434 | } \ 435 | INTERPOSE(func, gmj_##func) 436 | 437 | #define HOOK_SIMPLE(func, signature, args) \ 438 | HOOK(func, signature, args, "%s(...)", #func) 439 | 440 | /* Files */ 441 | HOOK_OPEN(open, (const char *path, int flags, ...), open(path, flags), 442 | open(path, flags, mode), "open(\"%s\", 0x%x, ...)", path, flags); 443 | 444 | HOOK_OPEN(openat, (int dirfd, const char *path, int flags, ...), 445 | openat(dirfd, path, flags), openat(dirfd, path, flags, mode), 446 | "openat(%d, \"%s\", 0x%x, ...)", dirfd, path, flags); 447 | 448 | HOOK(creat, (const char *path, mode_t mode), (path, mode), 449 | "creat(\"%s\", 0o%o)", path, mode); 450 | 451 | HOOK_SIMPLE(exchangedata, 452 | (const char *path1, const char *path2, unsigned int options), 453 | (path1, path2, options)); 454 | 455 | HOOK_SIMPLE(chmod, (const char *path, mode_t mode), (path, mode)); 456 | HOOK_SIMPLE(fchmod, (int fildes, mode_t mode), (fildes, mode)); 457 | HOOK_SIMPLE(fchmodat, (int fd, const char *path, mode_t mode, int flag), 458 | (fd, path, mode, flag)); 459 | HOOK_SIMPLE(chown, (const char *path, uid_t owner, gid_t group), 460 | (path, owner, group)); 461 | HOOK_SIMPLE(fchown, (int fildes, uid_t owner, gid_t group), 462 | (fildes, owner, group)); 463 | HOOK_SIMPLE(lchown, (const char *path, uid_t owner, gid_t group), 464 | (path, owner, group)); 465 | HOOK_SIMPLE(fchownat, 466 | (int fd, const char *path, uid_t owner, gid_t group, int flag), 467 | (fd, path, owner, group, flag)); 468 | HOOK_SIMPLE(link, (const char *path1, const char *path2), (path1, path2)); 469 | HOOK_SIMPLE(linkat, 470 | (int fd1, const char *name1, int fd2, const char *name2, int flag), 471 | (fd1, name1, fd2, name2, flag)); 472 | HOOK_SIMPLE(mkdir, (const char *path, mode_t mode), (path, mode)); 473 | HOOK_SIMPLE(mkdirat, (int fd, const char *path, mode_t mode), (fd, path, mode)); 474 | HOOK_SIMPLE(mknod, (const char *path, mode_t mode, dev_t dev), 475 | (path, mode, dev)); 476 | HOOK_SIMPLE(mknodat, (int fd, const char *path, mode_t mode, dev_t dev), 477 | (fd, path, mode, dev)); 478 | HOOK_SIMPLE(unlink, (const char *path), (path)); 479 | HOOK_SIMPLE(unlinkat, (int fd, const char *path, int flag), (fd, path, flag)); 480 | HOOK_SIMPLE(undelete, (const char *path), (path)); 481 | 482 | /* Sockets */ 483 | HOOK_SIMPLE(listen, (int socket, int backlog), (socket, backlog)); 484 | HOOK_SIMPLE(connect, 485 | (int socket, const struct sockaddr *address, socklen_t address_len), 486 | (socket, address, address_len)); 487 | 488 | /* Processes */ 489 | HOOK(execve, (const char *path, char *const argv[], char *const envp[]), 490 | (path, argv, envp), "execve(\"%s\", ...)", path); 491 | HOOK_SIMPLE(posix_spawn, 492 | (pid_t *restrict pid, const char *restrict path, 493 | const posix_spawn_file_actions_t *file_actions, 494 | const posix_spawnattr_t *restrict attrp, 495 | char *const argv[restrict], char *const envp[restrict]), 496 | (pid, path, file_actions, attrp, argv, envp)); 497 | HOOK_SIMPLE(posix_spawnp, 498 | (pid_t *restrict pid, const char *restrict file, 499 | const posix_spawn_file_actions_t *file_actions, 500 | const posix_spawnattr_t *restrict attrp, 501 | char *const argv[restrict], char *const envp[restrict]), 502 | (pid, file, file_actions, attrp, argv, envp)); 503 | --------------------------------------------------------------------------------