Files
exec/proot/proot.go
T

258 lines
7.0 KiB
Go

package proot
import (
"errors"
"fmt"
"io"
"net/netip"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sirherobrine23.com.br/go-bds/exec/v2/process"
prootext "sirherobrine23.com.br/go-bds/exec/v2/proot/extensions/extensions"
)
var (
_ process.Proc = &Proot{}
_ = process.RegisterProcess(NewProc, "proot")
ErrPlatformNotSupported = fmt.Errorf("proot: pure-Go backend is only supported on linux/android current is %s/%s", runtime.GOOS, runtime.GOARCH)
)
// Proot runs a process inside Rootfs without requiring chroot(2) privileges.
//
// This implementation is intentionally pure Go: on Linux/Android it uses
// ptrace(2) to rewrite path based syscalls instead of spawning the external
// proot binary. It is not a byte-for-byte clone of upstream PRoot yet, but it
// implements the core proot semantics needed by go-bds: rootfs remapping,
// bind mappings, cwd tracking, execve/openat/stat/access/readlink/chdir and
// common mutating path syscalls.
type Proot struct {
Rootfs string // Rootfs to expose as guest /
Qemu string // Optional qemu user emulator, e.g. qemu-aarch64-static
GID uint // Fake/target GID; currently kept for API compatibility
UID uint // Fake/target UID; currently kept for API compatibility
Binds map[string][]string // Host path -> one or more guest destinations
// DefaultBinds controls automatic host binds. Nil uses the conservative
// default (/dev only). /proc and /sys are live host kernel trees; binding
// them by default makes commands such as `find /` traverse dynamic host
// pseudo-filesystems that can race disappearing tasks/devices. Add "/proc"
// and/or "/sys" explicitly when a command really needs them. Set to an
// empty slice to disable all automatic binds.
DefaultBinds []string
Extensions []prootext.Extension // Ordered runtime extensions applied before process start
proc *nativeProcess
}
// NewProc returns an empty pure-Go proot process. Set Rootfs before Start.
func NewProc() (*Proot, error) {
return &Proot{
Rootfs: "/",
Binds: map[string][]string{},
Extensions: []prootext.Extension{},
}, nil
}
// Append dns server to /etc/resolv.conf.
//
// Example: pr.AddNameservers(netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("1.1.1.1"))
func (pr Proot) AddNameservers(addrs ...netip.Addr) error {
file, err := os.OpenFile(filepath.Join(pr.Rootfs, "etc/resolv.conf"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()
fmt.Fprint(file, "\n")
for _, addr := range addrs {
if _, err := fmt.Fprintf(file, "nameserver %s\n", addr.String()); err != nil {
return err
}
}
return nil
}
// Start starts the process under the pure-Go ptrace based proot backend.
func (pr *Proot) Start(options *process.Exec) error {
if options == nil || len(options.Arguments) == 0 || options.Arguments[0] == "" {
return errors.New("proot: missing command")
}
runtimeConfig, err := pr.prepareExtensions()
if err != nil {
return err
}
started := false
defer func() {
if !started {
_ = runtimeConfig.cleanup()
}
}()
if runtimeConfig.config.Rootfs == "" {
return errors.New("proot: Rootfs is required")
}
if options.Environment == nil {
options.Environment = process.Env{}
}
// A guest process must not inherit the host's absolute PATH. In Termux that
// would make /bin/sh search /data/data/com.termux/files/usr/bin inside the
// guest rootfs, so otherwise valid guest commands such as cat, ln and id
// appear to be missing. Preserve an explicitly supplied guest PATH.
if _, ok := options.Environment["PATH"]; !ok {
options.Environment["PATH"] = defaultGuestSearchPath
}
// Do not leak host-only absolute directories into the guest. Termux sets
// TMPDIR to /data/data/com.termux/files/usr/tmp; guest maintainer scripts
// then pass that path explicitly to tools such as mktemp, where it is
// correctly interpreted inside the rootfs and therefore does not exist.
// Preserve values explicitly supplied by the caller.
guestEnvDefaults := map[string]string{
"TMPDIR": "/tmp",
"TMP": "/tmp",
"TEMP": "/tmp",
"HOME": "/root",
"LANG": "C.UTF-8",
}
for name, value := range guestEnvDefaults {
if _, ok := options.Environment[name]; !ok {
options.Environment[name] = value
}
}
if err := pr.resolveExecutable(options, runtimeConfig.config); err != nil {
return err
}
if runtime.GOOS == "android" {
// Termux commonly injects LD_PRELOAD; guest dynamic linkers should not see it.
options.Environment["LD_PRELOAD"] = ""
}
p, err := startNative(pr, runtimeConfig.config, runtimeConfig.cleanups, options)
if err != nil {
return err
}
started = true
pr.proc = p
return nil
}
const defaultGuestSearchPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/system/bin:/system/xbin"
func (pr *Proot) resolveExecutable(options *process.Exec, config prootext.Config) error {
cmd := options.Arguments[0]
if path.IsAbs(cmd) || strings.ContainsRune(cmd, '/') {
return nil
}
pm, err := newPathMapper(config.Rootfs, config.PathResolvers)
if err != nil {
return err
}
cwd := options.Cwd
if cwd == "" {
cwd = "/"
}
searchPath := guestSearchPath(options.Environment)
for _, dir := range splitGuestSearchPath(searchPath) {
guest := path.Join(dir, cmd)
if !path.IsAbs(guest) {
guest = path.Join(cleanGuestPath(cwd), guest)
}
if isExecutable(pm.GuestToHost(guest)) {
options.Arguments[0] = cleanGuestPath(guest)
return nil
}
}
return fmt.Errorf("proot: executable %q not found in PATH %q", cmd, searchPath)
}
func guestSearchPath(env process.Env) string {
if env != nil {
if value, ok := env["PATH"]; ok {
return value
}
}
return defaultGuestSearchPath
}
func splitGuestSearchPath(searchPath string) []string {
dirs := strings.Split(searchPath, ":")
for i, dir := range dirs {
if dir == "" {
dirs[i] = "."
}
}
return dirs
}
func isExecutable(name string) bool {
st, err := os.Stat(name)
if err != nil || st.IsDir() {
return false
}
return st.Mode().Perm()&0111 != 0
}
func (pr *Proot) Kill() error {
if pr.proc == nil {
return process.ErrNoProcess
}
return pr.proc.Kill()
}
func (pr *Proot) Close() error {
if pr.proc == nil {
return nil
}
return pr.proc.Signal(os.Interrupt)
}
func (pr *Proot) Wait() error {
if pr.proc == nil {
return process.ErrNoProcess
}
return pr.proc.Wait()
}
func (pr *Proot) Signal(sig os.Signal) error {
if pr.proc == nil {
return process.ErrNoProcess
}
return pr.proc.Signal(sig)
}
func (pr *Proot) ExitCode() (int, error) {
if pr.proc == nil {
return -1, process.ErrNoProcess
}
return pr.proc.ExitCode()
}
func (pr *Proot) AttachStdin(r io.Reader) error {
if pr.proc == nil {
pr.proc = &nativeProcess{}
}
pr.proc.stdin = r
return nil
}
func (pr *Proot) AttachStdout(w io.Writer) error {
if pr.proc == nil {
pr.proc = &nativeProcess{}
}
pr.proc.stdout = w
return nil
}
func (pr *Proot) AttachStderr(w io.Writer) error {
if pr.proc == nil {
pr.proc = &nativeProcess{}
}
pr.proc.stderr = w
return nil
}