mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
CI / build (push) Waiting to run
Go / generate (push) Waiting to run
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]>
188 lines
4.9 KiB
Go
188 lines
4.9 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 (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// tree lays out a workspace with something to escape to just outside it.
|
|
func tree(t *testing.T) (*root, string) {
|
|
t.Helper()
|
|
base := t.TempDir()
|
|
work := filepath.Join(base, "work")
|
|
outside := filepath.Join(base, "outside")
|
|
for _, d := range []string{work, outside, filepath.Join(work, "src")} {
|
|
if err := os.Mkdir(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write(t, filepath.Join(work, "src", "main.go"), "package main\n")
|
|
write(t, filepath.Join(outside, "secret"), "not yours\n")
|
|
if err := os.Symlink(outside, filepath.Join(work, "away")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink("src", filepath.Join(work, "here")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(filepath.Join(work, "src"), filepath.Join(work, "rooted")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
r, err := openRoot(work)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { r.close() })
|
|
return r, base
|
|
}
|
|
|
|
func write(t *testing.T, name, body string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(name, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestRootOpensWhatIsInside(t *testing.T) {
|
|
r, _ := tree(t)
|
|
fd, err := r.open("src/main.go", unix.O_RDONLY, 0)
|
|
if err != nil {
|
|
t.Fatalf("src/main.go: %v", err)
|
|
}
|
|
unix.Close(fd)
|
|
}
|
|
|
|
func TestRootRefusesEveryEscape(t *testing.T) {
|
|
r, _ := tree(t)
|
|
for _, p := range []string{
|
|
"../outside/secret", // up and out
|
|
"src/../../outside/secret",
|
|
"/etc/passwd", // absolute
|
|
"away/secret", // through a symlink that leaves
|
|
"away", // the symlink itself
|
|
"rooted", // an absolute symlink, even one aimed back inside
|
|
} {
|
|
if fd, err := r.open(p, unix.O_RDONLY, 0); err == nil {
|
|
unix.Close(fd)
|
|
t.Errorf("%s opened; it is outside the workspace", p)
|
|
}
|
|
if _, err := r.abs(p); err == nil {
|
|
t.Errorf("%s resolved; it is outside the workspace", p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRootFollowsASymlinkThatStaysInside(t *testing.T) {
|
|
r, _ := tree(t)
|
|
name, err := r.abs("here")
|
|
if err != nil {
|
|
t.Fatalf("here: %v", err)
|
|
}
|
|
if want := r.name + "/src"; name != want {
|
|
t.Fatalf("got %q want %q", name, want)
|
|
}
|
|
}
|
|
|
|
func TestRootAbsOfTheWorkspaceItself(t *testing.T) {
|
|
r, _ := tree(t)
|
|
name, err := r.abs("")
|
|
if err != nil {
|
|
t.Fatalf("empty path: %v", err)
|
|
}
|
|
if name != r.name {
|
|
t.Fatalf("got %q want %q", name, r.name)
|
|
}
|
|
}
|
|
|
|
func TestRootParentIsInsideToo(t *testing.T) {
|
|
r, _ := tree(t)
|
|
fd, base, err := r.parent("src/main.go")
|
|
if err != nil {
|
|
t.Fatalf("parent: %v", err)
|
|
}
|
|
unix.Close(fd)
|
|
if base != "main.go" {
|
|
t.Fatalf("got base %q", base)
|
|
}
|
|
if _, _, err := r.parent("../outside/secret"); err == nil {
|
|
t.Fatal("the parent of a path outside the workspace resolved")
|
|
}
|
|
}
|
|
|
|
// A path is resolved by the kernel for a write as well as for a read, so one
|
|
// string cannot mean two places. away is a symlink out of the workspace, so
|
|
// "away/../f" leaves it even though the string alone looks like it stays.
|
|
func TestRootParentRefusesWhatTheKernelRefuses(t *testing.T) {
|
|
r, _ := tree(t)
|
|
for _, p := range []string{
|
|
"away/../escaped",
|
|
"../outside/planted",
|
|
"/tmp/planted",
|
|
"src/../../outside/planted",
|
|
} {
|
|
if fd, _, err := r.parent(p); err == nil {
|
|
unix.Close(fd)
|
|
t.Errorf("the parent of %s resolved; a read of it is refused", p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RESOLVE_BENEATH refuses a ".." that escapes, not every "..".
|
|
func TestRootTakesADotDotThatStaysInside(t *testing.T) {
|
|
r, _ := tree(t)
|
|
fd, err := r.open("src/../src/main.go", unix.O_RDONLY, 0)
|
|
if err != nil {
|
|
t.Fatalf("src/../src/main.go: %v", err)
|
|
}
|
|
unix.Close(fd)
|
|
|
|
fd, base, err := r.parent("src/../src/main.go")
|
|
if err != nil {
|
|
t.Fatalf("parent: %v", err)
|
|
}
|
|
unix.Close(fd)
|
|
if base != "main.go" {
|
|
t.Fatalf("got base %q", base)
|
|
}
|
|
}
|
|
|
|
// A write or a patch holds the directory of each file it names while it runs,
|
|
// and a command may start meanwhile. That descriptor closes on exec like every
|
|
// other the daemon opens, the workspace's own included.
|
|
func TestRootParentClosesOnExec(t *testing.T) {
|
|
r, _ := tree(t)
|
|
for _, p := range []string{"top.go", "src/main.go"} {
|
|
fd, _, err := r.parent(p)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", p, err)
|
|
}
|
|
flags, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
|
|
unix.Close(fd)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if flags&unix.FD_CLOEXEC == 0 {
|
|
t.Errorf("the directory of %s is held without FD_CLOEXEC", p)
|
|
}
|
|
}
|
|
}
|