gomodjail confines a specific set of Go modules, so as to mitigate their potential vulnerabilities and supply chain attack vectors.
In other words, gomodjail is a "container" (as in Docker containers) for Go modules.
gomodjail can be applied just in the following two steps:
Step 1: add gomodjail:confined comment to go.mod:
require (
example.com/module v1.0.0 // gomodjail:confined
)
Step 2: statically verify the confinement with gomodjail analyze:
gomodjail analyze ./...
The build fails (non-zero exit) if a confined module's code can reach a denied capability: filesystem, network, process execution, raw syscalls, OS state modification, or cgo.
The legacy dynamic mode (gomodjail run), which enforces a similar (but
not identical) policy at runtime via syscall interception, is still
available — see Dynamic mode.
The static analysis itself is platform-independent, but the gomodjail binary currently builds only on Linux and macOS, as the legacy dynamic mode is compiled in unconditionally (moving it behind a build tag is planned).
make
sudo make install
Makefile variables:
PREFIX: installation prefix (default: /usr/local)An example program is located in ./examples/victim:
cd ./examples/victim
go build
./victim
Confirm the "malicious" vi screen:
*** ARBITRARY SHELL CODE EXECUTION ***
This 'vi' command was executed by the 'github.com/AkihiroSuda/gomodjail/examples/poisoned' module.
This example is harmless, of course, but suppose that this was a malicious code.
Type ':q!' to leave this screen.
The poisoned module is marked gomodjail:confined in victim's go.mod,
so gomodjail analyze catches the exec statically, with the witness call
path, before the program ever runs:
$ gomodjail analyze ./...
FAIL github.com/AkihiroSuda/gomodjail/examples/poisoned: reaches EXEC
[EXEC]
github.com/AkihiroSuda/gomodjail/examples/poisoned.Add
os/exec.Command (poisoned.go:19)
gomodjail: 1 confined module(s): 0 ok, 0 warning(s), 1 violation(s)
gomodjail analyze prints one verdict per confined module:
FILES/READ, FILES/WRITE, NETWORK, EXEC,
SYSTEM_CALLS, OPERATING_SYSTEM, MODIFY_SYSTEM_STATE, or CGO.
The exit code is non-zero.reflect, unsafe, assembly,
unresolvable dynamic calls), so the verdict is best-effort. Warnings do
not fail the gate unless --strict is set. Nearly every real-world
module that marshals, hashes, or logs earns at least one warning; this is
expected.ok (unused) so stale annotations stay visible.Reading or writing an io.Writer/io.Reader/*os.File handed to the
module by your program is not a violation: the capability was granted when
your code handed over the handle. Opening a new file, dialing, or execing is.
File descriptors are not isolated across modules: reading or writing an
already-open file descriptor (including one wrapped with os.NewFile) is
not a violation either.
Useful flags:
--strict: treat warnings as violations--explain: print witness call paths for warnings too--format=json: machine-readable full report--format=sarif: SARIF 2.1.0 for code-scanning integrations (findings are
anchored to the module's require line in go.mod)--goos, --goarch: analyze a different target platform--parallel=N: analyze up to N modules concurrently (default: the number
of CPUs; memory use grows with N)gomodjail fix rewrites go.mod so that gomodjail analyze passes:
every module with a FAIL verdict has its annotation downgraded to
// gomodjail:unconfined (a per-line annotation overrides any file- or
block-level gomodjail:confined default, and the rest of the line is
preserved; for // indirect requires the annotation is written on its own
line above the require, keeping the // indirect comment byte-for-byte
intact for other toolchains):
$ gomodjail fix ./...
fix github.com/containerd/go-runc: unconfined (reaches EXEC, FILES/READ, NETWORK, SYSTEM_CALLS)
gomodjail: unconfined 1 of 101 confined module(s) in go.mod
gomodjail: 35 module(s) with warnings kept confined (use --strict to unconfine them too)
Unconfining is the only safe automated fix: a FAIL means the module's
own code (or its own dependency cone) reaches the capability, which no edit
to your program can prevent. The explicit gomodjail:unconfined annotation
keeps the decision visible and reviewable in go.mod.
Unconfining every confined module at once is refused, as gomodjail analyze would then have nothing left to verify.
Useful flags:
--dry-run: print the edits without writing go.mod--strict: also unconfine warning-only modules (mirrors analyze --strict)--from-report=FILE: reuse a saved gomodjail analyze --format=json
report (- for stdin) instead of re-running the analysis--parallel=N: analyze up to N modules concurrently (default: the number
of CPUs; memory use grows with N)examples/profiles has several example profiles:
docker.mod: for docker (not dockerd)reflect, unsafe, assembly, etc. cannot be statically verified.
Use --strict to reject it./proc) fail on FILES/READ by design; they were never
confinable at runtime either.gomodjail:confined policy is not well defined and still subject to change.The original runtime enforcement is still available:
gomodjail run --go-mod=go.mod -- ./victim
level=WARN msg=***Blocked*** syscall=pidfd_open module=github.com/AkihiroSuda/gomodjail/examples/poisoned
It imposes syscall restrictions on the confined modules by intercepting syscalls and attributing them to modules via stack unwinding. It needs no source code (only symbols), and it catches host-injected capability abuse, but it is slower and considerably more fragile than the static gate.
How it works:
SECCOMP_RET_TRACE
is used for conditionally allowing trusted Go modules to execute the
syscall. SECCOMP_RET_USER_NOTIF is not used because it cannot access all
the CPU registers, due to the
lack of struct pt_regs in struct seccomp_data.
Stack unwinding is
used for analyzing the call stack to determine the Go module.DYLD_INSERT_LIBRARIES is used to hook libSystem (libc) calls.
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.Dynamic-mode caveats:
unsafe, reflect, plugin,
etc. (gomodjail analyze reports these as warnings.)macOS:
DYLD_INSERT_LIBRARIES.macOS on Intel:
-ldflags="-s" (disable symbol table)To create a self-extract archive of gomodjail with a target program, run
gomodjail pack --go-mod=go.mod PROGRAM.
The self-extract archive is created as <PROGRAM>.gomodjail.
The packed program runs under the dynamic mode.
gomodjail analyze reuses Capslock
(pinned) as its capability-analysis engine. Each confined module is analyzed
in its own dependency slice — the module's packages plus their transitive
imports, with the query rooted at the module's own functions — so a module
is blamed only for what its own code and its own dependency cone can do,
not for values the host program hands it. Capability classes map to three
severity tiers (deny / caveat / allow) calibrated against what the dynamic
mode's seccomp filter actually blocked; unknown classes are denied (fail
closed).
FILES/READ for a
config-loading module).SECCOMP_IOCTL_NOTIF_ADDFD.docs/syntax.md: syntaxexamples/profiles/README.md: profiles341 followers · starred Jan 2026
204 followers · starred Jan 2025
5,205 followers · starred Feb 2025
197 followers · starred Feb 2026
gomodjail confines a specific set of Go modules, so as to mitigate their potential vulnerabilities and supply chain attack vectors.
In other words, gomodjail is a "container" (as in Docker containers) for Go modules.
gomodjail can be applied just in the following two steps:
Step 1: add gomodjail:confined comment to go.mod:
require (
example.com/module v1.0.0 // gomodjail:confined
)
Step 2: statically verify the confinement with gomodjail analyze:
gomodjail analyze ./...
The build fails (non-zero exit) if a confined module's code can reach a denied capability: filesystem, network, process execution, raw syscalls, OS state modification, or cgo.
The legacy dynamic mode (gomodjail run), which enforces a similar (but
not identical) policy at runtime via syscall interception, is still
available — see Dynamic mode.
The static analysis itself is platform-independent, but the gomodjail binary currently builds only on Linux and macOS, as the legacy dynamic mode is compiled in unconditionally (moving it behind a build tag is planned).
make
sudo make install
Makefile variables:
PREFIX: installation prefix (default: /usr/local)An example program is located in ./examples/victim:
cd ./examples/victim
go build
./victim
Confirm the "malicious" vi screen:
*** ARBITRARY SHELL CODE EXECUTION ***
This 'vi' command was executed by the 'github.com/AkihiroSuda/gomodjail/examples/poisoned' module.
This example is harmless, of course, but suppose that this was a malicious code.
Type ':q!' to leave this screen.
The poisoned module is marked gomodjail:confined in victim's go.mod,
so gomodjail analyze catches the exec statically, with the witness call
path, before the program ever runs:
$ gomodjail analyze ./...
FAIL github.com/AkihiroSuda/gomodjail/examples/poisoned: reaches EXEC
[EXEC]
github.com/AkihiroSuda/gomodjail/examples/poisoned.Add
os/exec.Command (poisoned.go:19)
gomodjail: 1 confined module(s): 0 ok, 0 warning(s), 1 violation(s)
gomodjail analyze prints one verdict per confined module:
FILES/READ, FILES/WRITE, NETWORK, EXEC,
SYSTEM_CALLS, OPERATING_SYSTEM, MODIFY_SYSTEM_STATE, or CGO.
The exit code is non-zero.reflect, unsafe, assembly,
unresolvable dynamic calls), so the verdict is best-effort. Warnings do
not fail the gate unless --strict is set. Nearly every real-world
module that marshals, hashes, or logs earns at least one warning; this is
expected.ok (unused) so stale annotations stay visible.Reading or writing an io.Writer/io.Reader/*os.File handed to the
module by your program is not a violation: the capability was granted when
your code handed over the handle. Opening a new file, dialing, or execing is.
File descriptors are not isolated across modules: reading or writing an
already-open file descriptor (including one wrapped with os.NewFile) is
not a violation either.
Useful flags:
--strict: treat warnings as violations--explain: print witness call paths for warnings too--format=json: machine-readable full report--format=sarif: SARIF 2.1.0 for code-scanning integrations (findings are
anchored to the module's require line in go.mod)--goos, --goarch: analyze a different target platform--parallel=N: analyze up to N modules concurrently (default: the number
of CPUs; memory use grows with N)gomodjail fix rewrites go.mod so that gomodjail analyze passes:
every module with a FAIL verdict has its annotation downgraded to
// gomodjail:unconfined (a per-line annotation overrides any file- or
block-level gomodjail:confined default, and the rest of the line is
preserved; for // indirect requires the annotation is written on its own
line above the require, keeping the // indirect comment byte-for-byte
intact for other toolchains):
$ gomodjail fix ./...
fix github.com/containerd/go-runc: unconfined (reaches EXEC, FILES/READ, NETWORK, SYSTEM_CALLS)
gomodjail: unconfined 1 of 101 confined module(s) in go.mod
gomodjail: 35 module(s) with warnings kept confined (use --strict to unconfine them too)
Unconfining is the only safe automated fix: a FAIL means the module's
own code (or its own dependency cone) reaches the capability, which no edit
to your program can prevent. The explicit gomodjail:unconfined annotation
keeps the decision visible and reviewable in go.mod.
Unconfining every confined module at once is refused, as gomodjail analyze would then have nothing left to verify.
Useful flags:
--dry-run: print the edits without writing go.mod--strict: also unconfine warning-only modules (mirrors analyze --strict)--from-report=FILE: reuse a saved gomodjail analyze --format=json
report (- for stdin) instead of re-running the analysis--parallel=N: analyze up to N modules concurrently (default: the number
of CPUs; memory use grows with N)examples/profiles has several example profiles:
docker.mod: for docker (not dockerd)reflect, unsafe, assembly, etc. cannot be statically verified.
Use --strict to reject it./proc) fail on FILES/READ by design; they were never
confinable at runtime either.gomodjail:confined policy is not well defined and still subject to change.The original runtime enforcement is still available:
gomodjail run --go-mod=go.mod -- ./victim
level=WARN msg=***Blocked*** syscall=pidfd_open module=github.com/AkihiroSuda/gomodjail/examples/poisoned
It imposes syscall restrictions on the confined modules by intercepting syscalls and attributing them to modules via stack unwinding. It needs no source code (only symbols), and it catches host-injected capability abuse, but it is slower and considerably more fragile than the static gate.
How it works:
SECCOMP_RET_TRACE
is used for conditionally allowing trusted Go modules to execute the
syscall. SECCOMP_RET_USER_NOTIF is not used because it cannot access all
the CPU registers, due to the
lack of struct pt_regs in struct seccomp_data.
Stack unwinding is
used for analyzing the call stack to determine the Go module.DYLD_INSERT_LIBRARIES is used to hook libSystem (libc) calls.
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.Dynamic-mode caveats:
unsafe, reflect, plugin,
etc. (gomodjail analyze reports these as warnings.)macOS:
DYLD_INSERT_LIBRARIES.macOS on Intel:
-ldflags="-s" (disable symbol table)To create a self-extract archive of gomodjail with a target program, run
gomodjail pack --go-mod=go.mod PROGRAM.
The self-extract archive is created as <PROGRAM>.gomodjail.
The packed program runs under the dynamic mode.
gomodjail analyze reuses Capslock
(pinned) as its capability-analysis engine. Each confined module is analyzed
in its own dependency slice — the module's packages plus their transitive
imports, with the query rooted at the module's own functions — so a module
is blamed only for what its own code and its own dependency cone can do,
not for values the host program hands it. Capability classes map to three
severity tiers (deny / caveat / allow) calibrated against what the dynamic
mode's seccomp filter actually blocked; unknown classes are denied (fail
closed).
FILES/READ for a
config-loading module).SECCOMP_IOCTL_NOTIF_ADDFD.docs/syntax.md: syntaxexamples/profiles/README.md: profiles341 followers · starred Jan 2026
204 followers · starred Jan 2025
5,205 followers · starred Feb 2025
197 followers · starred Feb 2026