Files
zeekayandHanzo Dev 4855f06fd1
CI / build (push) Failing after 33s
Go / generate (push) Failing after 46s
fix(execd): confine git, make patch all-or-nothing, answer only what happened
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]>
2026-09-18 17:49:38 -07:00

111 lines
3.1 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"
"sync"
)
// ledger remembers the reply of every completed action, keyed by its id, so
// a request the host repeats after losing an answer is answered from the
// record instead of run a second time.
//
// It is bounded twice, by count and by bytes: the newest replies are held
// while they are no more than max of them and take no more than room
// together, and the max ids evicted before those are kept as bare marks —
// enough to refuse a repeat whose result is gone rather than run the effect
// twice.
type ledger struct {
mu sync.Mutex
max int
room int // bytes the held replies may take together
held int // bytes they take now
done map[uint64][]byte // nil value: completed, result no longer held
kept []uint64 // ids whose result is held, oldest first
mark []uint64 // ids kept as marks, oldest first
live map[uint64]bool
}
func newLedger(max, room int) *ledger {
if max < 1 {
max = 1
}
return &ledger{
max: max,
room: room,
done: make(map[uint64][]byte, 2*max),
live: make(map[uint64]bool),
}
}
// begin claims an id. It returns the recorded reply when the action already
// completed, and an error when the id is running, or completed with its
// result no longer held.
func (l *ledger) begin(id uint64) ([]byte, error) {
if id == 0 {
return nil, fmt.Errorf("action id is zero")
}
l.mu.Lock()
defer l.mu.Unlock()
if frame, ok := l.done[id]; ok {
if frame == nil {
return nil, fmt.Errorf("action %d completed and its result is no longer held", id)
}
return frame, nil
}
if l.live[id] {
return nil, fmt.Errorf("action %d is already running", id)
}
l.live[id] = true
return nil, nil
}
// finish records an action's reply under its id.
func (l *ledger) finish(id uint64, frame []byte) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.live, id)
if _, ok := l.done[id]; ok {
return
}
l.done[id] = frame
l.kept = append(l.kept, id)
l.held += len(frame)
for len(l.kept) > l.max || (l.held > l.room && len(l.kept) > 1) {
old := l.kept[0]
l.kept = l.kept[1:]
l.held -= len(l.done[old])
l.done[old] = nil
l.mark = append(l.mark, old)
}
for len(l.mark) > l.max {
old := l.mark[0]
l.mark = l.mark[1:]
delete(l.done, old)
}
}
// drop releases an id, so a repeat of it runs. It is what a refused request
// gets: nothing happened, so there is no result to record and nothing to
// replay.
func (l *ledger) drop(id uint64) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.live, id)
}