Files
visor/cmd/execd/frame_test.go
zeekayandHanzo Dev 6208c7df95
CI / build (push) Canceled after 0s
Go / generate (push) Canceled after 0s
Add execd, the execution daemon inside the sandbox
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]>
2026-09-18 13:48:08 -07:00

124 lines
3.2 KiB
Go

// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
package main
import (
"bytes"
"reflect"
"testing"
)
func TestRequestRoundTrip(t *testing.T) {
want := &req{
op: opExec,
id: 7821,
handle: 4,
cols: 120,
rows: 40,
signal: 9,
timeout: 2500,
off: -3,
length: 4096,
mode: 0o600,
path: "pkg/main.go",
data: []byte("on stdin"),
dir: "pkg",
argv: []string{"cargo", "test", "--", "--nocapture"},
env: []string{"PATH=/bin", "RUST_BACKTRACE=1"},
command: "",
}
got, err := decodeReq(want.encode())
if err != nil {
t.Fatalf("decode: %v", err)
}
if !reflect.DeepEqual(want, got) {
t.Fatalf("round trip\n want %+v\n got %+v", want, got)
}
}
func TestRequestCommandRoundTrip(t *testing.T) {
want := &req{op: opExec, id: 1, command: "ls -l | wc -l"}
got, err := decodeReq(want.encode())
if err != nil {
t.Fatalf("decode: %v", err)
}
if got.command != want.command || len(got.argv) != 0 {
t.Fatalf("got command %q argv %v", got.command, got.argv)
}
}
func TestReplyRoundTrip(t *testing.T) {
want := &rep{
op: opList,
kind: kindReply,
id: 99,
handle: 3,
exit: -1,
size: 2,
mode: 0o644,
flags: flagDir,
mtime: 1750000000123456789,
mask: 0x200,
data: []byte("stdout"),
log: []byte("stderr"),
err: "no answer within 10ms",
path: "src",
entries: []entry{
{name: "main.go", size: 12, mode: 0o644, mtime: 5},
{name: "sub", flags: flagDir, mode: 0o755, mtime: 6},
},
}
got, err := decodeRep(want.encode())
if err != nil {
t.Fatalf("decode: %v", err)
}
if !reflect.DeepEqual(want, got) {
t.Fatalf("round trip\n want %+v\n got %+v", want, got)
}
}
func TestReplyEmptyFields(t *testing.T) {
got, err := decodeRep((&rep{op: opStat, id: 5}).encode())
if err != nil {
t.Fatalf("decode: %v", err)
}
if got.id != 5 || got.err != "" || got.data != nil || len(got.entries) != 0 {
t.Fatalf("got %+v", got)
}
}
func TestDecodeRefusesGarbage(t *testing.T) {
if _, err := decodeReq([]byte("not a zap frame at all")); err == nil {
t.Fatal("a buffer with no ZAP header decoded")
}
}
// The id has to be readable from a truncated head, which is how an oversize
// request is still answered by the action it names.
func TestIDSitsAtAFixedOffset(t *testing.T) {
frame := (&req{op: opExec, id: 4242, data: bytes.Repeat([]byte("x"), 1024)}).encode()
d := &daemon{out: make(chan []byte, 1), gone: make(chan struct{})}
d.refuseOversize(frame[:256], frameMax+1)
r, err := decodeRep(<-d.out)
if err != nil {
t.Fatalf("decode: %v", err)
}
if r.id != 4242 || r.err == "" {
t.Fatalf("got id %d err %q", r.id, r.err)
}
}