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 }