├── .github ├── dependabot.yml └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd └── gomodjail │ ├── commands │ ├── pack │ │ └── pack.go │ └── run │ │ └── run.go │ ├── main.go │ └── version │ └── version.go ├── docs └── syntax.md ├── examples ├── README.md ├── poisoned │ ├── go.mod │ └── poisoned.go ├── profiles │ ├── README.md │ ├── docker-26.1.3.mod │ └── docker.mod └── victim │ ├── go.mod │ └── main.go ├── go.mod ├── go.sum ├── libgomodjail_hook_darwin ├── libgomodjail_hook_darwin.c └── offset.sh └── pkg ├── cache └── cache.go ├── child ├── child_linux.go └── child_others.go ├── cp └── cp.go ├── env └── env.go ├── envutil └── envutil.go ├── osargs └── osargs.go ├── parent ├── parent.go ├── parent_darwin.go └── parent_linux.go ├── procutil └── procutil_linux.go ├── profile ├── fromgomod │ ├── fromgomod.go │ └── fromgomod_test.go ├── profile.go ├── profile_test.go └── seccompprofile │ └── seccompprofile_linux.go ├── tracer ├── regs │ ├── regs_linux.go │ ├── regs_linux_amd64.go │ └── regs_linux_arm64.go ├── tracer.go ├── tracer_darwin.go └── tracer_linux.go ├── unwinder ├── unwinder_linux.go ├── unwinder_linux_amd64.go └── unwinder_linux_arm64.go └── ziputil └── ziputil.go /.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 | -------------------------------------------------------------------------------- /.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.24.x 18 | build_mode: "" 19 | - runner: ubuntu-24.04 # Intel 20 | go: 1.24.x 21 | build_mode: "strip" 22 | - runner: macos-13 # Intel 23 | go: 1.24.x 24 | build_mode: "" 25 | - runner: macos-15 # ARM 26 | # libgomodjail_hook_darwin is sensitive to Go version 27 | go: 1.23.x 28 | build_mode: "" 29 | - runner: macos-15 # ARM 30 | go: 1.24.x 31 | build_mode: "" 32 | runs-on: ${{ matrix.runner }} 33 | timeout-minutes: 10 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions/setup-go@v5 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@v8 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 <> _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 | -------------------------------------------------------------------------------- /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 | - Not applicable to a Go module that use: 101 | - [`syscall.Syscall`, `syscall.RawSyscall`, etc.](https://pkg.go.dev/syscall) 102 | 103 | ## Advanced topics 104 | ### Advanced usage 105 | - To create a self-extract archive of gomodjail with a target program, run `gomodjail pack --go-mod=go.mod PROGRAM`. 106 | The self-extract archive is created as `.gomodjail`. 107 | 108 | ### How it works 109 | Linux: 110 | - [`SECCOMP_RET_TRACE`](https://man7.org/linux/man-pages/man2/seccomp.2.html) is used for conditionally 111 | allowing trusted Go modules to execute the syscall. 112 | `SECCOMP_RET_USER_NOTIF` is not used because it cannot access all the CPU registers, 113 | 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). 114 | - [Stack unwinding](https://www.grant.pizza/blog/go-stack-traces-bpf/) is used for analyzing the call stack to determine the Go module. 115 | 116 | macOS: 117 | - `DYLD_INSERT_LIBRARIES` is used to hook `libSystem` (`libc`) calls. 118 | - 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. 119 | This analysis is not robust and only works with specific versions of Go. (See [Caveats](#caveats)). 120 | 121 | ### Future works 122 | - Automatically detect non-applicable modules (explained in [Caveats](#caveats)). 123 | - Apply landlock in addition to seccomp. Depends on `SECCOMP_IOCTL_NOTIF_ADDFD`. 124 | - Modify the source code of the Go runtime, so as to remove necessity of using `seccomp` (Linux) and `DYLD_INSERT_LIBRARIES` (macOS). 125 | 126 | ## Additional documents 127 | - [`docs/syntax.md`](./docs/syntax.md): syntax 128 | - [`examples/profiles/README.md`](./examples/profiles/README.md): profiles 129 | -------------------------------------------------------------------------------- /cmd/gomodjail/commands/pack/pack.go: -------------------------------------------------------------------------------- 1 | package pack 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | 12 | "github.com/AkihiroSuda/gomodjail/pkg/tracer" 13 | "github.com/AkihiroSuda/gomodjail/pkg/ziputil" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func Example() string { 18 | return ` # Pack nerdctl with gomodjail 19 | gomodjail pack --go-mod=go.mod /usr/local/bin/nerdctl 20 | ` 21 | } 22 | 23 | func New() *cobra.Command { 24 | cmd := &cobra.Command{ 25 | Use: "pack FILE", 26 | Short: "Pack a Go program with gomodjail", 27 | Example: Example(), 28 | Args: cobra.ExactArgs(1), 29 | RunE: action, 30 | DisableFlagsInUseLine: true, 31 | } 32 | flags := cmd.Flags() 33 | flags.String("go-mod", "", "go.mod file with comment lines like `gomodjail:confined`") 34 | flags.StringP("output", "o", "", "output file (default: .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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/poisoned/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AkihiroSuda/gomodjail/examples/poisoned 2 | 3 | go 1.23 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/profiles/docker.mod: -------------------------------------------------------------------------------- 1 | docker-26.1.3.mod -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AkihiroSuda/gomodjail 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/elastic/go-seccomp-bpf v1.5.0 7 | github.com/spf13/cobra v1.9.1 8 | golang.org/x/mod v0.24.0 9 | golang.org/x/sys v0.33.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.6 // indirect 17 | golang.org/x/net v0.24.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /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.5.0 h1:gJV+U1iP+YC70ySyGUUNk2YLJW5/IkEw4FZBJfW8ZZY= 5 | github.com/elastic/go-seccomp-bpf v1.5.0/go.mod h1:umdhQ/3aybliBF2jjiZwS492I/TOKz+ZRvsLT3hVe1o= 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.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 14 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 15 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 16 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 17 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 18 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 19 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 20 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 21 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 22 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 23 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 24 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= 29 | gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 30 | -------------------------------------------------------------------------------- /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.24.0 */ 64 | .version_prefix = "go1.24", 65 | .g_m = 48, 66 | .m_libcallpc = 856, 67 | .m_libcallsp = 864, 68 | }, 69 | { 70 | /* go1.23.6 */ 71 | .version_prefix = "go1.23", 72 | .g_m = 48, 73 | .m_libcallpc = 832, 74 | .m_libcallsp = 840, 75 | }, 76 | { 77 | /* go1.22.12 */ 78 | .version_prefix = "go1.22", 79 | .g_m = 48, 80 | .m_libcallpc = 1048, 81 | .m_libcallsp = 1056, 82 | }, 83 | { 84 | .version_prefix = NULL, 85 | .g_m = 0, 86 | .m_libcallpc = 0, 87 | .m_libcallsp = 0, 88 | }, 89 | }; 90 | 91 | /* TODO: move to go_runtime_info */ 92 | static struct go_runtime_offset *go_runtime_offset_current = NULL; 93 | 94 | struct go_runtime_info { 95 | char *version; /* *NOT* freeable */ 96 | uint64_t *tls_g_addr; 97 | }; 98 | 99 | static struct go_runtime_info go_runtime_info; 100 | 101 | // The result is *NOT* freeable. 102 | static char *get_go_version_from_go_buildinfo_buf(uint8_t *buf) { 103 | char *res = NULL; 104 | if (memcmp(buf, "\xff Go buildinf:", 14) != 0) { 105 | ERRORF("expected __go_buildinfo to have a valid magic"); 106 | goto done; 107 | } 108 | if (buf[14] != 8) { 109 | ERRORF("expected __go_buildinfo[14] (pointer size) to be 8, got %d", 110 | buf[14]); 111 | goto done; 112 | } 113 | if (buf[15] != 2) { 114 | ERRORF("expected __go_buildinfo[15] (endianness) to be 2, got %d", buf[15]); 115 | goto done; 116 | } 117 | uint8_t varint0 = buf[32]; 118 | if ((varint0 & 0x80) != 0) { 119 | ERRORF("expected __go_buildinfo[32] (varint of go version length) not to " 120 | "have the continuation bit, got %d", 121 | varint0); 122 | goto done; 123 | } 124 | static char res_buf[128]; 125 | memcpy(res_buf, buf + 33, varint0); 126 | res = res_buf; 127 | done: 128 | return res; 129 | } 130 | 131 | static struct go_runtime_info get_go_runtime_info_from_file_buf(void *buf) { 132 | struct go_runtime_info res; 133 | memset(&res, 0, sizeof(res)); 134 | struct mach_header_64 *mh = (struct mach_header_64 *)buf; 135 | if (mh->magic != MH_MAGIC_64) { 136 | /* TODO: support FAT_MAGIC? */ 137 | ERRORF("expected MH_MAGIC_64, got %d", mh->magic); 138 | goto done; 139 | } 140 | struct load_command *lc = (struct load_command *)(buf + sizeof(*mh)); 141 | uint32_t i; 142 | for (i = 0; i < mh->ncmds; i++) { 143 | if (lc->cmd == LC_SEGMENT_64) { 144 | struct segment_command_64 *seg = (struct segment_command_64 *)lc; 145 | struct section_64 *sect = 146 | (struct section_64 *)((uint8_t *)seg + sizeof(*seg)); 147 | for (uint32_t s = 0; s < seg->nsects; s++) { 148 | if (strncmp(sect[s].sectname, "__go_buildinfo", 14) == 0) { 149 | res.version = 150 | get_go_version_from_go_buildinfo_buf(buf + sect[s].offset); 151 | } 152 | } 153 | } else if (lc->cmd == LC_SYMTAB) { 154 | struct symtab_command *stcmd = (struct symtab_command *)lc; 155 | char *strtab = (char *)buf + stcmd->stroff; 156 | struct nlist_64 *symtab = 157 | (struct nlist_64 *)((uint8_t *)buf + stcmd->symoff); 158 | for (uint32_t j = 0; j < stcmd->nsyms; j++) { 159 | char *name = strtab + symtab[j].n_un.n_strx; 160 | if (STR_EQ(name, "_runtime.tls_g")) { 161 | int image_index = 1; /* FIXME: parse */ 162 | uint64_t runtime_tls_g_sym_value = (uint64_t)symtab[j].n_value; 163 | res.tls_g_addr = 164 | (uint64_t *)(_dyld_get_image_vmaddr_slide(image_index) + 165 | runtime_tls_g_sym_value); 166 | } 167 | } 168 | } 169 | lc = (struct load_command *)((uint8_t *)lc + lc->cmdsize); 170 | } 171 | done: 172 | return res; 173 | } 174 | 175 | static struct go_runtime_info get_go_runtime_info_from_file(const char *path) { 176 | struct go_runtime_info res; 177 | memset(&res, 0, sizeof(res)); 178 | void *mm = NULL; 179 | size_t mm_len = -1; 180 | int fd = open(path, O_RDONLY); 181 | if (fd < 0) { 182 | ERRORF("open(\"%s\") failed: %s", path, strerror(errno)); 183 | goto done; 184 | } 185 | struct stat st; 186 | if (fstat(fd, &st) < 0) { 187 | ERRORF("fstat(\"%s\") failed: %s", path, strerror(errno)); 188 | goto done; 189 | } 190 | mm_len = (size_t)st.st_size; 191 | mm = mmap(NULL, mm_len, PROT_READ, MAP_PRIVATE, fd, 0); 192 | if (mm == NULL) { 193 | ERRORF("mmap failed: %s", strerror(errno)); 194 | goto done; 195 | } 196 | close(fd); 197 | fd = -1; 198 | res = get_go_runtime_info_from_file_buf(mm); 199 | done: 200 | if (fd >= 0) 201 | close(fd); 202 | if (mm != NULL) 203 | munmap(mm, mm_len); 204 | return res; 205 | } 206 | 207 | static void init() __attribute__((constructor)); 208 | 209 | static void init() { 210 | debug = getenv("DEBUG") != NULL; 211 | if (_NSGetExecutablePath(exe_path, &exe_path_len) != 0) { 212 | ERRORF("_NSGetExecutablePath() failed"); 213 | return; 214 | } 215 | go_runtime_info = get_go_runtime_info_from_file(exe_path); 216 | char *go_version = go_runtime_info.version; /* Not freeable */ 217 | if (!go_version) { 218 | WARNF("%s: Not a Go binary. Ignoring.", exe_path); 219 | return; 220 | } 221 | DEBUGF("%s: Go version=\"%s\"", exe_path, go_version); 222 | for (int i = 0; go_runtime_offsets[i].version_prefix; i++) { 223 | if (STR_HAS_PREFIX(go_version, go_runtime_offsets[i].version_prefix)) { 224 | DEBUGF("%s: treating Go version \"%s\" as %s", exe_path, go_version, 225 | go_runtime_offsets[i].version_prefix); 226 | DEBUGF("%s: Go runtime offsets: g->m: %lld, m->libcallpc: %lld, " 227 | "m->libcallsp: %lld", 228 | exe_path, go_runtime_offsets[i].g_m, 229 | go_runtime_offsets[i].m_libcallpc, 230 | go_runtime_offsets[i].m_libcallsp); 231 | go_runtime_offset_current = &go_runtime_offsets[i]; 232 | break; 233 | } 234 | } 235 | if (!go_runtime_offset_current) { 236 | ERRORF("%s: Unsupported Go version: \"%s\"", exe_path, go_version); 237 | return; 238 | } 239 | enabled = true; 240 | } 241 | 242 | #if defined(__aarch64__) 243 | static uint64_t fetch_g() { 244 | uintptr_t tls_base; 245 | __asm__ __volatile__("mrs %0, tpidrro_el0" : "=r"(tls_base)); 246 | tls_base &= ~((uintptr_t)7); 247 | uint64_t runtime_tls_g = *go_runtime_info.tls_g_addr; 248 | return *(uint64_t *)(tls_base + runtime_tls_g); 249 | } 250 | #define BP_ADJUSTMENT 8 251 | #elif defined(__x86_64__) 252 | static uint64_t fetch_g() { 253 | uintptr_t g; 254 | /* https://github.com/golang/go/issues/23617 */ 255 | __asm__ __volatile__("movq %%gs:0x30, %0" : "=r"(g)); 256 | return g; 257 | } 258 | #define BP_ADJUSTMENT 16 259 | #endif 260 | 261 | /* Returns true if execution is allowed */ 262 | static bool handle_syscall(const char *syscall_name) { 263 | bool res = true; 264 | int sock = -1; 265 | char *json_buf = NULL; 266 | size_t json_len = -1; 267 | FILE *json_fp = NULL; 268 | 269 | if (!enabled) { 270 | DEBUGF("Handler is not enabled"); 271 | goto done; 272 | } 273 | 274 | if (!go_runtime_offset_current) { 275 | DEBUGF("Go runtime is not recognized"); 276 | goto done; 277 | } 278 | 279 | { 280 | struct sockaddr_un addr; 281 | char *sock_path = getenv("LIBGOMODJAIL_HOOK_SOCKET"); 282 | if (sock_path == NULL) { 283 | ERRORF("LIBGOMODJAIL_HOOK_SOCKET is unset"); 284 | goto done; 285 | } 286 | if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) { 287 | ERRORF("socket() failed: %s", strerror(errno)); 288 | goto done; 289 | } 290 | memset(&addr, 0, sizeof(addr)); 291 | addr.sun_family = PF_UNIX; 292 | strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); 293 | if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { 294 | ERRORF("connect() failed: %s", strerror(errno)); 295 | goto done; 296 | } 297 | } 298 | 299 | if ((json_fp = open_memstream(&json_buf, &json_len)) == NULL) { 300 | ERRORF("open_memstream() failed: %s", strerror(errno)); 301 | goto done; 302 | } 303 | 304 | fprintf(json_fp, "{\"pid\":%d,\"exe\":\"%s\",\"syscall\":\"%s\",\"stack\":[", 305 | getpid(), exe_path, syscall_name); 306 | 307 | { 308 | void *callstack[128]; 309 | int frames = backtrace(callstack, sizeof(callstack) / sizeof(callstack[0])); 310 | for (int i = 0; i < frames; ++i) { 311 | Dl_info dli; 312 | if (dladdr(callstack[i], &dli) > 0) { 313 | DEBUGF("* %s\t%s", dli.dli_fname, dli.dli_sname); 314 | fprintf(json_fp, "{\"file\":\"%s\",\"symbol\":\"%s\"},", dli.dli_fname, 315 | dli.dli_sname); 316 | if (STR_EQ(dli.dli_sname, "runtime.asmcgocall.abi0")) { 317 | uint64_t g_addr = fetch_g(); 318 | if (!g_addr) { 319 | ERRORF("!g_addr"); 320 | break; 321 | } 322 | uint64_t m_addr_addr = g_addr + go_runtime_offset_current->g_m; 323 | uint64_t m_addr = *(uint64_t *)m_addr_addr; 324 | if (!m_addr) { 325 | ERRORF("!m_addr"); 326 | break; 327 | } 328 | uint64_t libcallpc_addr = 329 | m_addr + go_runtime_offset_current->m_libcallpc; 330 | uint64_t libcallsp_addr = 331 | m_addr + go_runtime_offset_current->m_libcallsp; 332 | uint64_t pc = *(uint64_t *)libcallpc_addr; 333 | uint64_t sp = *(uint64_t *)libcallsp_addr; 334 | if (sp) { 335 | uint64_t bp = sp - BP_ADJUSTMENT; 336 | while (bp != 0) { 337 | uint64_t saved_bp = *(uint64_t *)bp; 338 | uint64_t ret_addr = *(uint64_t *)(bp + 8); 339 | Dl_info dli2; 340 | if (dladdr((void *)pc, &dli2) > 0) { 341 | DEBUGF("* %s\t%s", dli2.dli_fname, dli2.dli_sname); 342 | fprintf(json_fp, "{\"file\":\"%s\",\"symbol\":\"%s\"},", 343 | dli2.dli_fname, dli2.dli_sname); 344 | } 345 | pc = ret_addr; 346 | bp = saved_bp; 347 | } 348 | } 349 | } 350 | } else { 351 | DEBUGF("* %p", callstack[i]); 352 | fprintf(json_fp, "{\"address\":%lld},", (uint64_t)callstack[i]); 353 | } 354 | } 355 | } 356 | 357 | /* A terminator entry is added to simplify the trailing comma logic */ 358 | fprintf(json_fp, "{}]}"); 359 | fclose(json_fp); 360 | json_fp = NULL; 361 | 362 | { 363 | uint32_t json_len32 = (uint32_t)json_len; 364 | if (write(sock, &json_len32, sizeof(json_len32)) < 0) { 365 | ERRORF("write() failed: %s", strerror(errno)); 366 | goto done; 367 | } 368 | if (write(sock, json_buf, json_len) < 0) { 369 | ERRORF("write() failed: %s", strerror(errno)); 370 | goto done; 371 | } 372 | } 373 | 374 | { 375 | uint8_t resp[5]; 376 | if (read(sock, resp, sizeof(resp)) < 0) { 377 | ERRORF("read() failed: %s", strerror(errno)); 378 | goto done; 379 | } 380 | /* TODO: parse JSON */ 381 | res = resp[4] != '0'; 382 | } 383 | done: 384 | if (sock >= 0) 385 | close(sock); 386 | if (json_fp != NULL) 387 | fclose(json_fp); 388 | if (json_buf != NULL) 389 | free(json_buf); 390 | return res; 391 | } 392 | 393 | #define INTERPOSE(fn, hook) \ 394 | __attribute__(( \ 395 | used, section("__DATA,__interpose"))) static void *interpose_##fn[] = { \ 396 | hook, fn} 397 | 398 | static int open_needs_mode(int flags) { return flags & O_CREAT; } 399 | 400 | #define HOOK_OPEN(func, args, call_no_mode, call_mode, fmt, ...) \ 401 | static int gmj_##func args { \ 402 | DEBUGF(fmt, __VA_ARGS__); \ 403 | if (handle_syscall(#func)) { \ 404 | if (open_needs_mode(flags)) { \ 405 | va_list ap; \ 406 | va_start(ap, flags); \ 407 | int mode = va_arg(ap, int); \ 408 | va_end(ap); \ 409 | return call_mode; \ 410 | } \ 411 | return call_no_mode; \ 412 | } \ 413 | errno = EPERM; \ 414 | return -1; \ 415 | } \ 416 | INTERPOSE(func, gmj_##func) 417 | 418 | #define HOOK(func, signature, args, fmt, ...) \ 419 | static int gmj_##func signature { \ 420 | DEBUGF(fmt, __VA_ARGS__); \ 421 | if (handle_syscall(#func)) { \ 422 | return func args; \ 423 | } \ 424 | errno = EPERM; \ 425 | return -1; \ 426 | } \ 427 | INTERPOSE(func, gmj_##func) 428 | 429 | #define HOOK_SIMPLE(func, signature, args) \ 430 | HOOK(func, signature, args, "%s(...)", #func) 431 | 432 | /* Files */ 433 | HOOK_OPEN(open, (const char *path, int flags, ...), open(path, flags), 434 | open(path, flags, mode), "open(\"%s\", 0x%x, ...)", path, flags); 435 | 436 | HOOK_OPEN(openat, (int dirfd, const char *path, int flags, ...), 437 | openat(dirfd, path, flags), openat(dirfd, path, flags, mode), 438 | "openat(%d, \"%s\", 0x%x, ...)", dirfd, path, flags); 439 | 440 | HOOK(creat, (const char *path, mode_t mode), (path, mode), 441 | "creat(\"%s\", 0o%o)", path, mode); 442 | 443 | HOOK_SIMPLE(exchangedata, 444 | (const char *path1, const char *path2, unsigned int options), 445 | (path1, path2, options)); 446 | 447 | HOOK_SIMPLE(chmod, (const char *path, mode_t mode), (path, mode)); 448 | HOOK_SIMPLE(fchmod, (int fildes, mode_t mode), (fildes, mode)); 449 | HOOK_SIMPLE(fchmodat, (int fd, const char *path, mode_t mode, int flag), 450 | (fd, path, mode, flag)); 451 | HOOK_SIMPLE(chown, (const char *path, uid_t owner, gid_t group), 452 | (path, owner, group)); 453 | HOOK_SIMPLE(fchown, (int fildes, uid_t owner, gid_t group), 454 | (fildes, owner, group)); 455 | HOOK_SIMPLE(lchown, (const char *path, uid_t owner, gid_t group), 456 | (path, owner, group)); 457 | HOOK_SIMPLE(fchownat, 458 | (int fd, const char *path, uid_t owner, gid_t group, int flag), 459 | (fd, path, owner, group, flag)); 460 | HOOK_SIMPLE(link, (const char *path1, const char *path2), (path1, path2)); 461 | HOOK_SIMPLE(linkat, 462 | (int fd1, const char *name1, int fd2, const char *name2, int flag), 463 | (fd1, name1, fd2, name2, flag)); 464 | HOOK_SIMPLE(mkdir, (const char *path, mode_t mode), (path, mode)); 465 | HOOK_SIMPLE(mkdirat, (int fd, const char *path, mode_t mode), (fd, path, mode)); 466 | HOOK_SIMPLE(mknod, (const char *path, mode_t mode, dev_t dev), 467 | (path, mode, dev)); 468 | HOOK_SIMPLE(mknodat, (int fd, const char *path, mode_t mode, dev_t dev), 469 | (fd, path, mode, dev)); 470 | HOOK_SIMPLE(unlink, (const char *path), (path)); 471 | HOOK_SIMPLE(unlinkat, (int fd, const char *path, int flag), (fd, path, flag)); 472 | HOOK_SIMPLE(undelete, (const char *path), (path)); 473 | 474 | /* Sockets */ 475 | HOOK_SIMPLE(listen, (int socket, int backlog), (socket, backlog)); 476 | HOOK_SIMPLE(connect, 477 | (int socket, const struct sockaddr *address, socklen_t address_len), 478 | (socket, address, address_len)); 479 | 480 | /* Processes */ 481 | HOOK(execve, (const char *path, char *const argv[], char *const envp[]), 482 | (path, argv, envp), "execve(\"%s\", ...)", path); 483 | HOOK_SIMPLE(posix_spawn, 484 | (pid_t *restrict pid, const char *restrict path, 485 | const posix_spawn_file_actions_t *file_actions, 486 | const posix_spawnattr_t *restrict attrp, 487 | char *const argv[restrict], char *const envp[restrict]), 488 | (pid, path, file_actions, attrp, argv, envp)); 489 | HOOK_SIMPLE(posix_spawnp, 490 | (pid_t *restrict pid, const char *restrict file, 491 | const posix_spawn_file_actions_t *file_actions, 492 | const posix_spawnattr_t *restrict attrp, 493 | char *const argv[restrict], char *const envp[restrict]), 494 | (pid, file, file_actions, attrp, argv, envp)); 495 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | const ( 4 | PrivateChild = "_GOMODJAIL_PRIVATE_CHILD" // no value 5 | CacheHome = "GOMODJAIL_CACHE_HOME" 6 | ) 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/profile/fromgomod/fromgomod.go: -------------------------------------------------------------------------------- 1 | package fromgomod 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "slices" 7 | "strings" 8 | 9 | "golang.org/x/mod/modfile" 10 | 11 | "github.com/AkihiroSuda/gomodjail/pkg/profile" 12 | ) 13 | 14 | func FromGoMod(mod *modfile.File, prof *profile.Profile) error { 15 | prof.Module = mod.Module.Mod.Path 16 | currentDefaultPolicy := profile.PolicyUnconfined 17 | 18 | for _, c := range append(mod.Module.Syntax.Before, mod.Module.Syntax.Suffix...) { 19 | if tok := c.Token; tok != "" { 20 | pol, err := policyFromComment(tok) 21 | if err != nil { 22 | err = fmt.Errorf("failed to parse comment %+v: %w", c, err) 23 | return err 24 | } 25 | currentDefaultPolicy = pol 26 | } 27 | } 28 | 29 | for _, c := range append(mod.Go.Syntax.Before, mod.Go.Syntax.Suffix...) { 30 | if tok := c.Token; tok != "" { 31 | pol, err := policyFromComment(tok) 32 | if err != nil { 33 | err = fmt.Errorf("failed to parse comment %+v: %w", c, err) 34 | return err 35 | } 36 | return fmt.Errorf("policy %q is specified in an invalid position", pol) 37 | } 38 | } 39 | 40 | for _, f := range mod.Require { 41 | if syn := f.Syntax; syn != nil { 42 | pol := currentDefaultPolicy 43 | for _, c := range append(syn.Before, syn.Suffix...) { 44 | if tok := c.Token; tok != "" { 45 | polFromComment, err := policyFromComment(tok) 46 | if err != nil { 47 | err = fmt.Errorf("failed to parse comment %+v: %w", c, err) 48 | return err 49 | } 50 | if polFromComment != "" { 51 | pol = polFromComment 52 | } 53 | } 54 | } 55 | if pol == "" { 56 | pol = currentDefaultPolicy 57 | } 58 | if pol == profile.PolicyUnconfined { 59 | pol = "" // reduce map size 60 | } 61 | if existPol, ok := prof.Modules[f.Mod.Path]; ok && existPol != pol { 62 | slog.Warn("Overwriting an existing policy", "module", f.Mod.Path, "old", existPol, "new", pol) 63 | } 64 | if pol == "" { 65 | delete(prof.Modules, f.Mod.Path) 66 | } else { 67 | prof.Modules[f.Mod.Path] = pol 68 | } 69 | } 70 | } 71 | return nil 72 | } 73 | 74 | func policyFromComment(token string) (string, error) { 75 | token = strings.TrimPrefix(token, "//") 76 | // TODO: support /* ... */ 77 | for _, f := range strings.Fields(token) { 78 | f = strings.TrimPrefix(f, "//") 79 | if strings.HasPrefix(f, "gomodjail:") { 80 | pol := profile.Policy(strings.TrimPrefix(f, "gomodjail:")) 81 | if !slices.Contains(profile.KnownPolicies, pol) { 82 | return pol, fmt.Errorf("unknown policy %q", pol) 83 | } 84 | return pol, nil 85 | } 86 | } 87 | return "", nil 88 | } 89 | -------------------------------------------------------------------------------- /pkg/profile/fromgomod/fromgomod_test.go: -------------------------------------------------------------------------------- 1 | package fromgomod 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/AkihiroSuda/gomodjail/pkg/profile" 7 | "golang.org/x/mod/modfile" 8 | "gotest.tools/v3/assert" 9 | ) 10 | 11 | func TestFromGoMod(t *testing.T) { 12 | type testCase struct { 13 | name string 14 | goMod string 15 | expected map[string]profile.Policy 16 | } 17 | testCases := []testCase{ 18 | { 19 | name: "basic", 20 | goMod: ` 21 | module example.com/foo 22 | 23 | go 1.23 24 | 25 | require ( 26 | example.com/mod100 v1.2.3 // gomodjail:confined 27 | example.com/mod101 v1.2.3 28 | // gomodjail:confined 29 | example.com/mod102 v1.2.3 30 | example.com/mod103 v1.2.3 31 | example.com/mod104 v1.2.3 //gomodjail:confined 32 | ) 33 | 34 | require ( 35 | example.com/mod200 v1.2.3 // indirect 36 | example.com/mod201 v1.2.3 // indirect; gomodjail:confined 37 | example.com/mod202 v1.2.3 // indirect // gomodjail:confined 38 | // gomodjail:confined 39 | example.com/mod203 v1.2.4 // indirect 40 | example.com/mod204 v1.2.3 // indirect //gomodjail:confined 41 | ) 42 | `, 43 | expected: map[string]string{ 44 | "example.com/mod100": "confined", 45 | "example.com/mod102": "confined", 46 | "example.com/mod104": "confined", 47 | "example.com/mod201": "confined", 48 | "example.com/mod202": "confined", 49 | "example.com/mod203": "confined", 50 | "example.com/mod204": "confined", 51 | }, 52 | }, 53 | 54 | { 55 | name: "global", 56 | goMod: ` 57 | // gomodjail:confined 58 | module example.com/foo 59 | 60 | go 1.23 61 | 62 | require ( 63 | example.com/mod100 v1.2.3 64 | example.com/mod101 v1.2.3 // gomodjail:unconfined 65 | example.com/mod102 v1.2.3 66 | // gomodjail:unconfined 67 | example.com/mod103 v1.2.3 68 | ) 69 | 70 | require ( 71 | // gomodjail:unconfined 72 | example.com/mod200 v1.2.3 // indirect 73 | example.com/mod201 v1.2.3 // indirect 74 | example.com/mod202 v1.2.3 // indirect 75 | ) 76 | 77 | // policy cannot be specified here because the parser ignores 78 | // the comment lines here 79 | require ( 80 | ) 81 | `, 82 | expected: map[string]string{ 83 | "example.com/mod100": "confined", 84 | "example.com/mod102": "confined", 85 | "example.com/mod201": "confined", 86 | "example.com/mod202": "confined", 87 | }, 88 | }, 89 | } 90 | for _, tc := range testCases { 91 | t.Run(tc.name, func(t *testing.T) { 92 | mod, err := modfile.Parse(tc.name, []byte(tc.goMod), nil) 93 | assert.NilError(t, err) 94 | prof := profile.New() 95 | assert.NilError(t, FromGoMod(mod, prof)) 96 | assert.DeepEqual(t, "example.com/foo", prof.Module) 97 | assert.DeepEqual(t, tc.expected, prof.Modules) 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | slog.Debug("handler", "pid", pid, "exe", filename, "syscall", syscallName) 193 | for i, e := range entries { 194 | slog.Debug("stack", "entryNo", i, "entry", e.String()) 195 | pkgName := e.Func.PackageName() 196 | if cf := tracer.profile.Confined(uw.BuildInfo.Main.Path, pkgName); cf != nil { 197 | slog.Warn("***Blocked***", "pid", pid, "exe", filename, "syscall", syscallName, "entry", e.String(), "module", cf.Module) 198 | ret := -1 * int(unix.EPERM) 199 | regs.SetRet(uint64(ret)) 200 | regs.SetSyscall(unix.SYS_GETPID) // Only needed on amd64? 201 | return nil 202 | } 203 | } 204 | return nil 205 | } 206 | -------------------------------------------------------------------------------- /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/buildinfo" 8 | "debug/elf" 9 | "debug/gosym" 10 | "fmt" 11 | "log/slog" 12 | 13 | "github.com/AkihiroSuda/gomodjail/pkg/procutil" 14 | ) 15 | 16 | type Unwinder struct { 17 | Symtab *gosym.Table 18 | BuildInfo *buildinfo.BuildInfo 19 | } 20 | 21 | func New(binary string) (*Unwinder, error) { 22 | e, err := elf.Open(binary) 23 | if err != nil { 24 | return nil, err 25 | } 26 | gopclntabSec := e.Section(".gopclntab") 27 | if gopclntabSec == nil { 28 | return nil, fmt.Errorf("no .gopclntab section found in %q", binary) 29 | } 30 | gopclntabData, err := gopclntabSec.Data() 31 | if err != nil { 32 | return nil, err 33 | } 34 | textSec := e.Section(".text") 35 | if textSec == nil { 36 | return nil, fmt.Errorf("no .text section found in %q", binary) 37 | } 38 | 39 | var gosymtabData []byte 40 | gosymtabSec := e.Section(".gosymtab") 41 | if gosymtabSec == nil { 42 | slog.Warn("no .gosymtab section found", "binary", binary) 43 | // gopclntab seems to suffice in this case 44 | } else { 45 | gosymtabData, err = gosymtabSec.Data() 46 | if err != nil { 47 | return nil, err 48 | } 49 | } 50 | 51 | symtab, err := gosym.NewTable(gosymtabData, 52 | gosym.NewLineTable(gopclntabData, textSec.Addr)) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | buildInfo, err := buildinfo.ReadFile(binary) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | u := &Unwinder{ 63 | Symtab: symtab, 64 | BuildInfo: buildInfo, 65 | } 66 | return u, nil 67 | } 68 | 69 | func (u *Unwinder) Unwind(pid int, pc, bp uintptr) ([]Entry, error) { 70 | var res []Entry 71 | frameCount := 0 72 | const maxFrames = 1024 73 | for bp != 0 && frameCount < maxFrames { 74 | // FIXME: read the memory regions at once 75 | savedBp, err := procutil.ReadUint64(pid, bp) 76 | if err != nil { 77 | return res, err 78 | } 79 | retAddr, err := procutil.ReadUint64(pid, bp+wordSize) 80 | if err != nil { 81 | return res, err 82 | } 83 | var ent Entry 84 | ent.File, ent.Line, ent.Func = u.Symtab.PCToLine(uint64(pc)) 85 | if ent.Func != nil { 86 | res = append(res, ent) 87 | } 88 | pc = uintptr(retAddr) 89 | bp = uintptr(savedBp) 90 | frameCount++ 91 | } 92 | return res, nil 93 | } 94 | 95 | type Entry struct { 96 | File string 97 | Line int 98 | Func *gosym.Func 99 | } 100 | 101 | func (e *Entry) String() string { 102 | return fmt.Sprintf("%s:%d:%s", e.File, e.Line, e.Func.Name) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/unwinder/unwinder_linux_amd64.go: -------------------------------------------------------------------------------- 1 | package unwinder 2 | 3 | const wordSize = 8 4 | -------------------------------------------------------------------------------- /pkg/unwinder/unwinder_linux_arm64.go: -------------------------------------------------------------------------------- 1 | package unwinder 2 | 3 | const wordSize = 8 4 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------