mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
CI / build (push) Failing after 33s
Go / generate (push) Failing after 46s
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]>
372 lines
8.8 KiB
Go
372 lines
8.8 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 (
|
|
"fmt"
|
|
|
|
zap "github.com/zap-proto/go"
|
|
)
|
|
|
|
// Ops. One byte on the wire, stable forever: a number is never reused.
|
|
const (
|
|
opExec uint8 = 1 // run argv or command to completion
|
|
opSpawn uint8 = 2 // start a process on a pty, return a handle
|
|
opPtyWrite uint8 = 3 // write bytes to a pty
|
|
opResize uint8 = 4 // set a pty window size
|
|
opKill uint8 = 5 // signal a pty process, or close a watch
|
|
opRead uint8 = 6 // read a byte range of a file
|
|
opWrite uint8 = 7 // replace a file's contents
|
|
opStat uint8 = 8 // one path's size, mode, mtime
|
|
opList uint8 = 9 // one directory's entries
|
|
opWatch uint8 = 10 // inotify on a path, return a handle
|
|
opPatch uint8 = 11 // apply a unified diff
|
|
opGit uint8 = 12 // run git in the workspace
|
|
)
|
|
|
|
func opName(op uint8) string {
|
|
switch op {
|
|
case opExec:
|
|
return "exec"
|
|
case opSpawn:
|
|
return "spawn"
|
|
case opPtyWrite:
|
|
return "pty.write"
|
|
case opResize:
|
|
return "resize"
|
|
case opKill:
|
|
return "kill"
|
|
case opRead:
|
|
return "read"
|
|
case opWrite:
|
|
return "write"
|
|
case opStat:
|
|
return "stat"
|
|
case opList:
|
|
return "list"
|
|
case opWatch:
|
|
return "watch"
|
|
case opPatch:
|
|
return "patch"
|
|
case opGit:
|
|
return "git"
|
|
}
|
|
return fmt.Sprintf("op(%d)", op)
|
|
}
|
|
|
|
// Reply kinds. A reply answers a request; the other kinds are pushed by a
|
|
// handle the host already holds, and are never recorded in the ledger.
|
|
const (
|
|
kindReply uint8 = 0
|
|
kindData uint8 = 1 // pty output
|
|
kindEvent uint8 = 2 // watch event
|
|
kindExit uint8 = 3 // the spawned process ended
|
|
)
|
|
|
|
// Flag bits in rep.flags.
|
|
const (
|
|
flagDir uint32 = 1 << 0
|
|
)
|
|
|
|
// frameMax bounds one packet. A request over it cannot be received and a
|
|
// reply over it is refused with the size that would have been needed, so a
|
|
// big file is read in ranges rather than truncated in silence.
|
|
const frameMax = 128 << 10
|
|
|
|
// Request field offsets. Fixed section, little-endian, zero-copy read.
|
|
const (
|
|
reqOp = 0
|
|
reqCols = 4
|
|
reqRows = 8
|
|
reqSignal = 12
|
|
reqID = 16
|
|
reqHandle = 24
|
|
reqTimeout = 32
|
|
reqOff = 40
|
|
reqLen = 48
|
|
reqMode = 52
|
|
reqPath = 56
|
|
reqData = 64
|
|
reqDir = 72
|
|
reqArgv = 80
|
|
reqEnv = 88
|
|
reqCommand = 96
|
|
reqSize = 104
|
|
)
|
|
|
|
// Reply field offsets.
|
|
const (
|
|
repOp = 0
|
|
repKind = 1
|
|
repExit = 4
|
|
repID = 8
|
|
repHandle = 16
|
|
repSize = 24
|
|
repMode = 32
|
|
repFlags = 36
|
|
repMtime = 40
|
|
repData = 48
|
|
repLog = 56
|
|
repErr = 64
|
|
repPath = 72
|
|
repEntries = 80
|
|
repMask = 88
|
|
repRepSize = 96
|
|
)
|
|
|
|
// Directory entry field offsets, inside one element of rep.entries.
|
|
const (
|
|
entSize = 0
|
|
entMode = 8
|
|
entFlags = 12
|
|
entMtime = 16
|
|
entName = 24
|
|
entTotal = 32
|
|
)
|
|
|
|
// req is one action. Every request carries an id; the reply names it.
|
|
type req struct {
|
|
op uint8
|
|
id uint64
|
|
handle uint64
|
|
cols uint32
|
|
rows uint32
|
|
signal int32
|
|
timeout uint64 // milliseconds; 0 means no deadline
|
|
off int64
|
|
length uint32
|
|
mode uint32
|
|
path string
|
|
data []byte
|
|
dir string
|
|
argv []string
|
|
env []string
|
|
command string
|
|
}
|
|
|
|
// rep answers one action, or pushes output under a handle's action id.
|
|
type rep struct {
|
|
op uint8
|
|
kind uint8
|
|
id uint64
|
|
handle uint64
|
|
exit int32
|
|
size uint64
|
|
mode uint32
|
|
flags uint32
|
|
mtime int64
|
|
mask uint32
|
|
data []byte
|
|
log []byte
|
|
err string
|
|
path string
|
|
entries []entry
|
|
|
|
// refused marks an answer to a request that was not carried out: the
|
|
// reason travels in err, nothing in the workspace changed, and the ledger
|
|
// records nothing, so asking again runs it. It is the daemon's own note
|
|
// and is not encoded.
|
|
refused bool
|
|
}
|
|
|
|
// entry is one name in a directory listing.
|
|
type entry struct {
|
|
name string
|
|
size uint64
|
|
mode uint32
|
|
flags uint32
|
|
mtime int64
|
|
}
|
|
|
|
func (r *req) encode() []byte {
|
|
b := zap.NewBuilder(512 + len(r.data))
|
|
argvOff, argvLen := texts(b, r.argv)
|
|
envOff, envLen := texts(b, r.env)
|
|
|
|
ob := b.StartObject(reqSize)
|
|
ob.SetUint8(reqOp, r.op)
|
|
ob.SetUint32(reqCols, r.cols)
|
|
ob.SetUint32(reqRows, r.rows)
|
|
ob.SetInt32(reqSignal, r.signal)
|
|
ob.SetUint64(reqID, r.id)
|
|
ob.SetUint64(reqHandle, r.handle)
|
|
ob.SetUint64(reqTimeout, r.timeout)
|
|
ob.SetInt64(reqOff, r.off)
|
|
ob.SetUint32(reqLen, r.length)
|
|
ob.SetUint32(reqMode, r.mode)
|
|
ob.SetText(reqPath, r.path)
|
|
ob.SetBytes(reqData, r.data)
|
|
ob.SetText(reqDir, r.dir)
|
|
ob.SetList(reqArgv, argvOff, argvLen)
|
|
ob.SetList(reqEnv, envOff, envLen)
|
|
ob.SetText(reqCommand, r.command)
|
|
ob.FinishAsRoot()
|
|
return b.Finish()
|
|
}
|
|
|
|
func decodeReq(buf []byte) (*req, error) {
|
|
m, err := zap.Parse(buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
o := m.Root()
|
|
if o.IsNull() {
|
|
return nil, fmt.Errorf("request has no root")
|
|
}
|
|
return &req{
|
|
op: o.Uint8(reqOp),
|
|
cols: o.Uint32(reqCols),
|
|
rows: o.Uint32(reqRows),
|
|
signal: o.Int32(reqSignal),
|
|
id: o.Uint64(reqID),
|
|
handle: o.Uint64(reqHandle),
|
|
timeout: o.Uint64(reqTimeout),
|
|
off: o.Int64(reqOff),
|
|
length: o.Uint32(reqLen),
|
|
mode: o.Uint32(reqMode),
|
|
path: string(o.Bytes(reqPath)),
|
|
data: o.Bytes(reqData),
|
|
dir: string(o.Bytes(reqDir)),
|
|
argv: readTexts(o.List(reqArgv)),
|
|
env: readTexts(o.List(reqEnv)),
|
|
command: string(o.Bytes(reqCommand)),
|
|
}, nil
|
|
}
|
|
|
|
func (r *rep) encode() []byte {
|
|
b := zap.NewBuilder(512 + len(r.data) + len(r.log))
|
|
entOff, entLen := entries(b, r.entries)
|
|
|
|
ob := b.StartObject(repRepSize)
|
|
ob.SetUint8(repOp, r.op)
|
|
ob.SetUint8(repKind, r.kind)
|
|
ob.SetInt32(repExit, r.exit)
|
|
ob.SetUint64(repID, r.id)
|
|
ob.SetUint64(repHandle, r.handle)
|
|
ob.SetUint64(repSize, r.size)
|
|
ob.SetUint32(repMode, r.mode)
|
|
ob.SetUint32(repFlags, r.flags)
|
|
ob.SetInt64(repMtime, r.mtime)
|
|
ob.SetUint32(repMask, r.mask)
|
|
ob.SetBytes(repData, r.data)
|
|
ob.SetBytes(repLog, r.log)
|
|
ob.SetText(repErr, r.err)
|
|
ob.SetText(repPath, r.path)
|
|
ob.SetList(repEntries, entOff, entLen)
|
|
ob.FinishAsRoot()
|
|
return b.Finish()
|
|
}
|
|
|
|
func decodeRep(buf []byte) (*rep, error) {
|
|
m, err := zap.Parse(buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
o := m.Root()
|
|
if o.IsNull() {
|
|
return nil, fmt.Errorf("reply has no root")
|
|
}
|
|
r := &rep{
|
|
op: o.Uint8(repOp),
|
|
kind: o.Uint8(repKind),
|
|
exit: o.Int32(repExit),
|
|
id: o.Uint64(repID),
|
|
handle: o.Uint64(repHandle),
|
|
size: o.Uint64(repSize),
|
|
mode: o.Uint32(repMode),
|
|
flags: o.Uint32(repFlags),
|
|
mtime: o.Int64(repMtime),
|
|
mask: o.Uint32(repMask),
|
|
data: o.Bytes(repData),
|
|
log: o.Bytes(repLog),
|
|
err: string(o.Bytes(repErr)),
|
|
path: string(o.Bytes(repPath)),
|
|
}
|
|
l := o.List(repEntries)
|
|
for i := 0; i < l.Len(); i++ {
|
|
e := l.ObjectAt(i)
|
|
if e.IsNull() {
|
|
return nil, fmt.Errorf("entry %d does not parse", i)
|
|
}
|
|
r.entries = append(r.entries, entry{
|
|
name: string(e.Bytes(entName)),
|
|
size: e.Uint64(entSize),
|
|
mode: e.Uint32(entMode),
|
|
flags: e.Uint32(entFlags),
|
|
mtime: e.Int64(entMtime),
|
|
})
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// texts lays a list of strings down as length-prefixed elements and returns
|
|
// its offset and element count.
|
|
func texts(b *zap.Builder, ss []string) (int, int) {
|
|
if len(ss) == 0 {
|
|
return 0, 0
|
|
}
|
|
lb := b.StartList(0)
|
|
for _, s := range ss {
|
|
lb.AddUint32(uint32(len(s)))
|
|
lb.AddBytes([]byte(s))
|
|
}
|
|
off, _ := lb.Finish()
|
|
return off, len(ss)
|
|
}
|
|
|
|
func readTexts(l zap.List) []string {
|
|
n := l.Len()
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
ss := make([]string, 0, n)
|
|
for i := 0; i < n; i++ {
|
|
ss = append(ss, string(l.BytesAt(i)))
|
|
}
|
|
return ss
|
|
}
|
|
|
|
// entries lays each directory entry down as its own ZAP message inside a
|
|
// length-prefixed element, which is what List.ObjectAt reads.
|
|
func entries(b *zap.Builder, es []entry) (int, int) {
|
|
if len(es) == 0 {
|
|
return 0, 0
|
|
}
|
|
lb := b.StartList(0)
|
|
for _, e := range es {
|
|
eb := zap.NewBuilder(64 + len(e.name))
|
|
ob := eb.StartObject(entTotal)
|
|
ob.SetUint64(entSize, e.size)
|
|
ob.SetUint32(entMode, e.mode)
|
|
ob.SetUint32(entFlags, e.flags)
|
|
ob.SetInt64(entMtime, e.mtime)
|
|
ob.SetText(entName, e.name)
|
|
ob.FinishAsRoot()
|
|
buf := eb.Finish()
|
|
lb.AddUint32(uint32(len(buf)))
|
|
lb.AddBytes(buf)
|
|
}
|
|
off, _ := lb.Finish()
|
|
return off, len(es)
|
|
}
|
|
|
|
// fail is the reply to a request that was not carried out. The reason travels
|
|
// with the action id; nothing is ever answered with a false success.
|
|
func fail(r *req, err error) *rep {
|
|
return &rep{op: r.op, kind: kindReply, id: r.id, exit: -1, err: err.Error(), refused: true}
|
|
}
|