258 lines
7.0 KiB
Go
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
|
|
}
|