The ZAP wire was pinned to a pseudo-version of a commit that exists only
in one checkout, so a clean fetch could not resolve it. v1.36.49 carries
the same Layouts generator and SCM_RIGHTS file passing.
Co-authored-by: Hanzo Dev <[email protected]>
Reading a ZAP result writes every field its layout states, the file payload
included, so files attached before decoding were overwritten with none. A
frame whose header is not ZAP is refused at the magic rather than read as a
length.
Co-authored-by: Hanzo Dev <[email protected]>
encoding/json walked every payload on every call. ZAP reads a field at a
constant offset out of the buffer that arrived, and //tools/zap has
already stated those offsets for every payload, so the transport now asks
the payload for its own bytes and the parse step is gone.
The envelope collapses with it. clientCall and serverCall existed only
because JSON needed one type to hold an any and another to hold a
RawMessage; bytes are bytes, so one Call serves both ends. Framing comes
from the ZAP header, which declares its own length.
A payload that is nothing -- the *struct{} a method takes when it has no
argument or no result -- carries nothing. Anything else without a layout
fails the call naming the type, so a payload that would have arrived
silently empty does not arrive at all.
The methods that took a bare string or returned a bare int now take and
return named payloads, because a scalar has no layout to state and the
rest of the API was already shaped this way.
Co-authored-by: Hanzo Dev <[email protected]>
A prometheus Data held its labels in a map, and control.ExecArgs held the
live kernel objects an exec runs against -- a mount namespace, an fd
table, a limit set. Neither has offsets, so neither payload could state
a layout.
Labels become a list, which also retires the O(n^2) walk that existed to
read a map in sorted order. The kernel objects move to control.Env, which
is what the sentry supplies and what a call arriving on the wire cannot:
a namespace is a reference, not a value.
Every urpc payload now states its own wire.
Co-authored-by: Hanzo Dev <[email protected]>
urpc encodes with encoding/json, which walks the payload on every call: a
realistic control.ExecArgs costs 7084ns and 10 allocations to cross. ZAP
reads a field at a constant offset out of the buffer that arrived, so the
same payload is 740ns and one allocation -- but only if the offsets are
stated, and a stated offset that is wrong is silent corruption rather than
a compile error. So state them from the types themselves.
//tools/zap reflects over the payloads at build time and writes the wire
as Go source with the offsets as constants. It lives inside Bazel because
pkg/sentry/control cannot be imported from outside it: the *_go_proto
packages it depends on have no source in the tree.
Six payloads had no layout to state. A map, an interface and a type from
another module have no offsets, so each is answered in the type:
SaveOpts.Metadata and StartContainerArgs.InitialCgroups/Limits cross as
lists of pairs; boot.Route.Destination restates net.IPNet as Subnet;
OpenMountArgs restates the OCI mount fields the call actually uses;
StartArgs.Spec and SinkConfig.Config carry their documents as the bytes
they arrived as; MetricsRegistrationResponse carries the protocol
buffer the same way; and seccheck's session options become a type
instead of a map of any.
ExecArgs and MetricsExportData still have none: ExecArgs holds live
kernel objects behind an interface, and MetricsExportData reaches a
prometheus snapshot whose labels are a map.
Co-authored-by: Hanzo Dev <[email protected]>
`runsc --version` prints two lines, and -visor is in neither:
runsc version <STABLE_VERSION>
spec: <OCI spec version>
STABLE_VERSION is `git describe --always --tags` — tools/workspace_status.sh,
wired by .bazelrc, substituted through runsc/BUILD's x_defs. The spec line is
opencontainers/runtime-spec's. The -visor suffix lives in
pkg/sentry/kernel/version/version.go: it is the release the sandbox advertises
through uname(2), which only a process inside a running sandbox can read. Its
own comment says as much — the two builds "are told apart by asking the guest".
So the gate fails for every build that can exist. visor-20260919.0 is queued and
has never run; it would have compiled all of gVisor and died on the last line
before creating the release, reading as a broken fork rather than a bad grep.
Assert what the binary does report, and what a release needs anyway: that the
bytes carry the tag being published. At a tag, describe returns the tag name, so
the line is exactly `runsc version $TAG`. Everything that should be rejected
still is — VERSION_MISSING with no stamp, 0.0.0 or a bare commit with no tag in
reach, -dirty from a modified tree, and upstream, whose tags are release-*.
The bazelisk asset now names the pool it runs on. bazelisk publishes <os>-<arch>
with exactly the labels our runners carry, so the arm64 row cannot come back
downloading an amd64 bazel.
Co-authored-by: Hanzo Dev <[email protected]>
The lane that builds visor and attaches verified binaries to its release has
existed since e3027ddbc, at .github/workflows/publish.yml. Tag visor-20260911.0
was pushed at a commit that already carried it, and no release was ever created:
this estate runs CI on Hanzo Git Actions from .hanzo/workflows, and has no
runners on GitHub Actions at all. A workflow in the wrong directory does not
fail, it waits — which reads exactly like a lane nobody triggered, and is why
every node still runs a visor built by hand from a working tree.
Also takes the canonical runner label. RUNNERS.md: "A runs-on is one of exactly
these" — linux-amd64. hanzo-linux-amd64 still routes, as an alias kept so
existing workflows keep running, and none of it should be written into a new one.
Nothing else changes: same bazelisk bootstrap, same single bazel invocation for
the runtime and the shim it execs, same gate refusing a binary that does not
report -visor, same sha512 beside every artifact.
Co-authored-by: Hanzo Dev <[email protected]>
FD 3 arrives without FD_CLOEXEC, which is how it crosses the exec into
execd, and execd never set it again, so every command, pty and git it
ran held the host's channel: it could read requests meant for the
daemon and answer in its name. newDaemon now sets FD_CLOEXEC. The test
harness made its pair with SOCK_CLOEXEC, which hid this; the daemon's
end now arrives inheritable, as under runsc. The workspace descriptor
dup'd for a top-level name closes on exec too.
The git ceiling governs only discovery, so -C, --git-dir, --work-tree
and a GIT_DIR or GIT_CONFIG_GLOBAL in env reached a repository or a
config file outside the workspace. Before its subcommand git now takes
-c and options that move nothing, and refuses the rest, since an
option execd does not know could take a value that hides the next.
GIT_* in env is refused and the global config is /dev/null, so HOME
picks no file. The ceiling test now covers each of these.
Co-authored-by: Hanzo Dev <[email protected]>
The git ceiling named the workspace, which git only applies to a proper
ancestor of the directory it starts from, so a request with no dir found,
read and wrote a repository above the workspace. The ceiling is now the
workspace's parent, which refuses from the root and from any directory
under it.
patch staged by action id, so two changes naming one file collided and
the second failed with the first's bytes already in place, and a delete
that could not happen left earlier renames behind. Every change now
stages under a random name, the file that was there is kept on a second
link until every move has landed, and a move that fails is put back. A
diff that names one file twice, gives two names for one file, would
delete a directory, or renames is refused.
A zero-length packet is a packet on SOCK_SEQPACKET, and reading it as end
of file ended the daemon for good. POLLRDHUP tells the two apart.
Also: a listing fills a page to what a frame holds instead of promising
4096 entries no frame can carry; write and patch resolve the parent with
openat2 on the path as written, so one string cannot mean two places; a
refused request is not recorded, so its id is free, while an action that
ran is recorded whatever came of it; a staged name is random, so a
workspace file is never unlinked to make room; children start from a
declared environment instead of the daemon's; write reports the mode on
disk and keeps the mode a file had; mode means permission bits in stat as
well as read and list; a read past the end is refused; an oversize
refusal names the op it refused; the ledger is bounded by bytes as well
as count; the diff path is the reading both header lines agree on; the
build constraint sits above the package doc; and BUILD embeds a library,
which is how the repo builds a main package with tests.
Co-authored-by: Hanzo Dev <[email protected]>
A coding agent is two programs: one reasons, one runs cargo test. execd is the
second half, and it knows nothing about the first — no model, no session, no
plan, no credential. Actions arrive as ZAP frames on FD 3, one end of a
SOCK_SEQPACKET pair the host connected before exec, so there is no listen, no
connect and no path in a filesystem.
Every request carries an action id and every reply names it. A repeated id for
a completed action is answered from the record instead of run again, so losing
an answer to git commit does not commit twice; the table is bounded by count
and an id whose result was evicted is refused rather than replayed. Paths
resolve with openat2 RESOLVE_BENEATH, so the kernel refuses "..", an absolute
path and a symlink that leaves the workspace. write and patch stage a
temporary file and rename over the target, and patch verifies every file in a
diff before renaming any of them.
Nothing fsyncs: the host serves the workspace and owns durability, and an
fsync inside the cell can cost seconds for nothing. Tests need no visor — a
socketpair in-process is the same code path as FD 3 under runsc.
Co-authored-by: Hanzo Dev <[email protected]>
Runners execute inside the visor sandbox, where setuid is not honoured: sudo
fails with "nosuid" before the step does anything. Bazel goes into a directory
the job already owns, named on GITHUB_PATH.
Co-authored-by: Hanzo Dev <[email protected]>
make copy wraps bazel in a container and pushes to an upstream registry, so it
needs a docker socket these runners do not have: it failed on
dial unix /var/run/docker.sock before building anything.
aarch64 is dropped for now rather than left to queue. git-runner:5 is a single
manifest, so an arm64 node cannot start it — the pods StartError and the job
then waits forever, which reads as nothing happening. It comes back in the same
commit that publishes a multi-arch runner image.
Co-authored-by: Hanzo Dev <[email protected]>
publish / binaries (aarch64, hanzo-linux-arm64) (push) Waiting to run
publish / binaries (x86_64, hanzo-linux-amd64) (push) Failing after 22s
A node installed a hand-built runtime nobody could reproduce or attribute to a
commit. Both binaries now come from one bazel invocation, ship beside their
sha512, and refuse to publish unless the runtime reports -visor.
This path, not .hanzo/workflows: the forge has no online runner, and an
unmatched runs-on queues silently instead of failing.
Co-authored-by: Hanzo Dev <[email protected]>
The lane depended on a GitHub PAT nobody had set on the forge, so it could
never have run. It now publishes to the forge release with the token every
runner is given, which is one less credential that can rot.
Each node installed a hand-built runtime nobody could reproduce or attribute
to a commit. Both binaries now come from one bazel invocation, ship beside
their sha512, and refuse to publish unless the runtime reports -visor.
It was the only thing in the tree importing google.golang.org/grpc, and it
reaches no runsc build path — 71 files of network conformance tests against a
posix_server that is not part of the sandbox. The control plane is urpc, and
now nothing here speaks gRPC at all.
Co-authored-by: Hanzo Dev <[email protected]>
A sandbox that answers "-gvisor" is upstream's binary. Telling the two apart
by config is a guess — the RuntimeClass, the containerd entry and the shim
options can all name our binary while the pod runs something else, which is
exactly what happened when an inline BinaryName was accepted by containerd and
never read by the shim. The guest's own uname is the one answer that cannot be
wrong about which kernel is running it.
Co-authored-by: Hanzo Dev <[email protected]>
Add a CLI tool and Go library that speak the libtpu checkpoint/restore
control protocol (pkg/sentry/control/tpu_control.proto) against
unsandboxed processes.
gVisor coordinates libtpu C/R during sandbox save/restore
(pkg/sentry/control/tpu_control.go), but there is no way to exercise
or debug that protocol path without a full runsc save cycle. This tool
fills the gap: it discovers libtpu control threads by scanning
/proc/<pid>/task/*/comm, opens the encoded pipe FDs via /proc/<pid>/fd/,
and sends ControlRequest messages.
Accepts a comma-separated PID list and runs actions concurrently
across PIDs with per-PID results, for multi-process jobs that need all
processes checkpointed together.
Co-authored-by: Hanzo Dev <[email protected]>
createdState.State and runningState.State transitioned the shim's init
process to stoppedState whenever a single `runsc state` call answered
"stopped". That transition bypasses SetExited: waitBlock is never
closed, no TaskExit event is published, and handleStoppedKill silently
swallows all subsequent kills, so containerd waits forever and the
container can never be torn down.
This behaviour comes from commit 0b39816a3, which tried to fix the
opposite problem of containerd never seeing that a container died due
to query errors. However, this part was never essential, because the
real fixes had to do with state queries on dead or deleted containers
erroring out. In light of https://github.com/google/gvisor/pull/14645,
there is now a concrete example of a buggy runsc not being an
authoritative and irreversible source for the stopped state, so kills
can be accepted but dropped, and the pod was stuck terminating forever.
Fix a panic when writing to cgroup v2 control files from outside the sandbox
(e.g., via Cgroups.WriteControlFiles / WriteControl), where writes occur
without an open in-sandbox VFS file descriptor (fd == nil). cgroup.procs now
safely falls back to context credentials with unconfined namespace migration,
and checkNSDelegateWrite skips namespace delegation checks. Add an integration
test for runsc cgroup v2 control file writes.
Also:
- Export cgroup2fs.InitHierarchy to initialize sandbox-wide cgroup2 state
and root controller enablement.
- Plumb CgroupNamespace through sentry lifecycle and task context to derive
initial container cgroup2 placements.
PiperOrigin-RevId: 978087817
Implement the remaining runtime profiling subcommands for `runsc profile`:
- `goroutine`: Dumps the current stack traces of all running goroutines
in Sentry.
- `block`: Collects a goroutine blocking contention profile.
- `mutex`: Collects a mutex contention profile.
- `trace`: Collects an execution trace.
PiperOrigin-RevId: 978048587
If we don't register the newly created sysmsg thread _before_ we start executing
contexts on it, we are liable to race with any interrupts that may arrive to
contexts it starts executing, upon which point we won't find the newly created
thread.
Reported-by: [email protected]
PiperOrigin-RevId: 977899577
Mark seccomp as partially supported on amd64 and arm64 and document the accepted operation and flags.
Fixes#14627
Co-authored-by: Hanzo Dev <[email protected]>
To kill the subprocess, we kill the syscall thread, which in turn kills all
stub threads by virtue of being their parent. This means we have two critical
sections that need to be protected:
1. Creating new sysmsg threads -- the function expects that it's new baby
will stay alive at least while it's being created;
2. Using the syscall thread itself.
We also kick out any currently sleeping task goroutines, and put guard-checks
on wait loops and possible entrances to subprocess functionality so that tasks
don't come back to a dead subprocess.
Additionally, killed subprocesses were not properly decref'ed before, this CL
also fixes that.
PiperOrigin-RevId: 977651945
OrderedChildren.Lookup returns the inode with a ref that the caller is
expected to release (normally by handing it to a dentry via Init). Five
cgroup2fs call sites that look an inode up for a transient purpose
(permission checks, control-file read/write, interface-file removal)
never released that ref.
This is NOT a bug fix: every inode these sites can return
(cgroupInterfaceFile and eventFile, both via kernfs.DynamicBytesFile,
and cgroup directories themselves) embeds kernfs.InodeNoopRefCount, so
IncRef/DecRef are no-ops and nothing leaks today. The change only makes
the ref discipline explicit and correct, so these sites stay safe if a
lookup ever returns a genuinely refcounted inode.
PiperOrigin-RevId: 976563179
This adds a tool to pull in and analyze the licenses of all
dependencies in the project, and ensures they are in a set of allowed
licenses.
This ensures that if we add new deps down the line, they must be part
of the set of allowed licenses.
SCMSender.beforeSave disconnects the endpoint on save: it shuts down and
closes the host fd, leaving fd = -1. Send already handles this state via
its IsSendClosed check (beforeSave sets sendShutdown), so writes fail
with EPIPE, but Recv had no guard and passed -1 to recvmsg(2), leaking
EBADF to applications for an fd that is still valid in their fd table.
Return ErrClosedForReceive instead, which the socket layer already
surfaces as read() == 0, matching Linux EOF semantics for a socket whose
read side is shut down and making a severed host endpoint behave like
any socket whose peer has gone away.
The guard keys on fd < 0 rather than IsRecvClosed: after a local SHUT_RD
on a live fd, the host socket may still hold queued data that must be
drained before EOF, and host recvmsg(2) handles that correctly.
PiperOrigin-RevId: 976063849
- In HostSender.HostReadiness, return hangup and readable/writeable events
((ReadableEvents | WriteableEvents | EventHUp | EventRdHUp) & mask) when
the host FD is closed (c.fd < 0), ensuring pollers (epoll/poll) are notified
of EOF and disconnection instead of masking the closed state.
- In SCMSender.beforeSave, return early if e.fd < 0 to make beforeSave
idempotent and prevent nil-pointer dereferences in fdnotifier.RemoveFD(-1)
on already-closed endpoints.
PiperOrigin-RevId: 975898094