Files
zeekayandHanzo Dev 0cc5982824
CI / build (push) Waiting to run
Go / generate (push) Waiting to run
fix(execd): keep the host socket from children, and git where dir says
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]>
2026-09-19 04:05:18 -07:00

134 lines
3.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 (
"fmt"
"os"
"path"
"strings"
"golang.org/x/sys/unix"
)
// root is the workspace. Every path in a request is resolved relative to it
// with RESOLVE_BENEATH, so the kernel — not a string check — rejects an
// absolute path, a ".." that would leave the workspace, and a symlink that
// points out of it. A ".." that stays inside resolves normally.
type root struct {
fd int // O_PATH descriptor of the workspace directory
name string // its canonical absolute path
}
const resolve = unix.RESOLVE_BENEATH | unix.RESOLVE_NO_MAGICLINKS
func openRoot(dir string) (*root, error) {
fd, err := unix.Open(dir, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return nil, fmt.Errorf("open workspace %s: %w", dir, err)
}
name, err := readFd(fd)
if err != nil {
unix.Close(fd)
return nil, err
}
return &root{fd: fd, name: name}, nil
}
func (r *root) close() error { return unix.Close(r.fd) }
// open resolves p beneath the workspace and returns an open descriptor.
func (r *root) open(p string, flags int, mode uint32) (int, error) {
return r.openAt(r.fd, p, flags, mode)
}
func (r *root) openAt(dirfd int, p string, flags int, mode uint32) (int, error) {
if p == "" {
p = "."
}
how := &unix.OpenHow{
Flags: uint64(flags) | unix.O_CLOEXEC,
Mode: uint64(mode),
Resolve: resolve,
}
fd, err := unix.Openat2(dirfd, p, how)
if err != nil {
return -1, fmt.Errorf("%s: %w", p, err)
}
return fd, nil
}
// file resolves p and hands back an *os.File that owns the descriptor.
func (r *root) file(p string, flags int, mode uint32) (*os.File, error) {
fd, err := r.open(p, flags, mode)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(fd), path.Join(r.name, p)), nil
}
// abs resolves p beneath the workspace and returns its canonical absolute
// path. Used where the kernel interface takes a path and not a descriptor:
// a child's working directory, an inotify watch.
func (r *root) abs(p string) (string, error) {
fd, err := r.open(p, unix.O_PATH, 0)
if err != nil {
return "", err
}
defer unix.Close(fd)
name, err := readFd(fd)
if err != nil {
return "", err
}
if name != r.name && !strings.HasPrefix(name, r.name+"/") {
return "", fmt.Errorf("%s: resolves outside the workspace", p)
}
return name, nil
}
// parent resolves p's directory beneath the workspace and returns its
// descriptor together with p's last component, which is what an atomic
// create-then-rename needs. The directory is handed to the kernel as written,
// never cleaned first: "away/../f" through a symlink out of the tree has to
// mean here what it means to a read.
func (r *root) parent(p string) (int, string, error) {
dir, base := path.Split(p)
if base == "" || base == "." || base == ".." {
return -1, "", fmt.Errorf("%s: not a file name", p)
}
if dir == "" {
fd, err := unix.FcntlInt(uintptr(r.fd), unix.F_DUPFD_CLOEXEC, 0)
if err != nil {
return -1, "", err
}
return fd, base, nil
}
fd, err := r.open(strings.TrimSuffix(dir, "/"), unix.O_PATH|unix.O_DIRECTORY, 0)
if err != nil {
return -1, "", err
}
return fd, base, nil
}
func readFd(fd int) (string, error) {
name, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", fd))
if err != nil {
return "", fmt.Errorf("read /proc/self/fd/%d: %w", fd, err)
}
return name, nil
}