Files
exec/proot/exec_resolve.go
T

176 lines
5.5 KiB
Go

//go:build linux || android
package proot
import (
"bytes"
"debug/elf"
"os"
"path/filepath"
"runtime"
"strings"
)
type execRewrite struct {
ExecPath string
Argv []string
}
func (pm *pathMapper) libraryPath() string {
return pm.libraryPathGuest()
}
func (pm *pathMapper) libraryPathGuest() string {
candidates := []string{}
switch runtime.GOARCH {
case "amd64":
candidates = append(candidates, "/lib/x86_64-linux-gnu", "/usr/lib/x86_64-linux-gnu", "/lib64")
case "arm64":
candidates = append(candidates, "/lib/aarch64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/system/lib64")
case "arm":
candidates = append(candidates, "/lib/arm-linux-gnueabihf", "/usr/lib/arm-linux-gnueabihf", "/system/lib")
case "386":
candidates = append(candidates, "/lib/i386-linux-gnu", "/usr/lib/i386-linux-gnu", "/lib32", "/usr/lib32")
case "loong64":
candidates = append(candidates, "/lib/loongarch64-linux-gnu", "/usr/lib/loongarch64-linux-gnu")
case "mips":
candidates = append(candidates, "/lib/mips-linux-gnu", "/usr/lib/mips-linux-gnu")
case "mips64":
candidates = append(candidates, "/lib/mips64-linux-gnuabi64", "/usr/lib/mips64-linux-gnuabi64")
case "mips64le":
candidates = append(candidates, "/lib/mips64el-linux-gnuabi64", "/usr/lib/mips64el-linux-gnuabi64")
case "mipsle":
candidates = append(candidates, "/lib/mipsel-linux-gnu", "/usr/lib/mipsel-linux-gnu")
case "ppc64":
candidates = append(candidates, "/lib/powerpc64-linux-gnu", "/usr/lib/powerpc64-linux-gnu")
case "ppc64le":
candidates = append(candidates, "/lib/powerpc64le-linux-gnu", "/usr/lib/powerpc64le-linux-gnu")
case "riscv64":
candidates = append(candidates, "/lib/riscv64-linux-gnu", "/usr/lib/riscv64-linux-gnu")
case "s390x":
candidates = append(candidates, "/lib/s390x-linux-gnu", "/usr/lib/s390x-linux-gnu")
}
candidates = append(candidates, "/lib", "/usr/lib", "/usr/local/lib", "/usr/lib64", "/system/lib64", "/system/lib")
seen := map[string]bool{}
out := make([]string, 0, len(candidates))
for _, guest := range candidates {
host := pm.GuestToHost(guest)
if host == "" || seen[guest] {
continue
}
if st, err := os.Stat(host); err == nil && st.IsDir() {
seen[guest] = true
out = append(out, guest)
}
}
return strings.Join(out, ":")
}
func (pm *pathMapper) resolveExec(hostPath, guestPath string, argv []string) (*execRewrite, bool) {
if rw, ok := pm.resolveScript(hostPath, guestPath, argv); ok {
return rw, true
}
if rw, ok := pm.resolveELF(hostPath, guestPath, argv); ok {
return rw, true
}
return &execRewrite{ExecPath: hostPath, Argv: argv}, false
}
func (pm *pathMapper) resolveScript(hostPath, guestPath string, argv []string) (*execRewrite, bool) {
data := make([]byte, 256)
f, err := os.Open(hostPath)
if err != nil {
return nil, false
}
defer f.Close()
n, _ := f.Read(data)
data = data[:n]
if !bytes.HasPrefix(data, []byte("#!")) {
return nil, false
}
line := string(data[2:])
if i := strings.IndexByte(line, '\n'); i >= 0 {
line = line[:i]
}
line = strings.TrimSpace(strings.TrimRight(line, "\r"))
if line == "" {
return nil, false
}
parts := strings.Fields(line)
interpGuest := parts[0]
interpHost := interpGuest
if strings.HasPrefix(interpGuest, "/") {
interpHost = pm.GuestToHost(interpGuest)
}
newArgv := []string{interpGuest}
if len(parts) > 1 {
newArgv = append(newArgv, parts[1:]...)
}
// The interpreter runs under ptrace, so argv paths must stay in the guest
// namespace. Passing the host path here makes the interpreter/loader try to
// open /rootfs/... and the tracer would translate it to /rootfs/rootfs/...
// again.
newArgv = append(newArgv, guestPath)
if len(argv) > 1 {
newArgv = append(newArgv, argv[1:]...)
}
// If the script interpreter is itself a dynamically-linked ELF (for example
// /usr/bin/perl), do not exec the interpreter host path directly. A direct
// exec would let the kernel resolve PT_INTERP in the host namespace, so the
// process may start with the host dynamic loader and guest libraries mixed
// together. Route the interpreter through the same ELF loader rewrite used
// for normal binaries, while keeping argv in the guest namespace.
if rw, ok := pm.resolveELF(interpHost, interpGuest, newArgv); ok {
return rw, true
}
return &execRewrite{ExecPath: interpHost, Argv: newArgv}, true
}
func (pm *pathMapper) resolveELF(hostPath, guestPath string, argv []string) (*execRewrite, bool) {
f, err := elf.Open(hostPath)
if err != nil {
return nil, false
}
defer f.Close()
var interp string
for _, p := range f.Progs {
if p.Type != elf.PT_INTERP {
continue
}
buf := make([]byte, p.Filesz)
if _, err := p.ReadAt(buf, 0); err == nil {
interp = strings.TrimRight(string(buf), "\x00")
}
break
}
if interp == "" {
return nil, false
}
interpGuest := interp
interpHost := interp
if filepath.IsAbs(interp) {
interpHost = pm.GuestToHost(interp)
}
if _, err := os.Stat(interpHost); err != nil {
// Minimal test rootfs trees often contain only /bin/<cmd>. Falling back
// to the host interpreter keeps those cases working when the host and
// guest ABI match, while still presenting guest paths to the traced loader.
if _, hostErr := os.Stat(interp); hostErr != nil {
return nil, false
}
interpHost = interp
}
libPath := pm.libraryPathGuest()
newArgv := []string{interpGuest}
if libPath != "" {
newArgv = append(newArgv, "--library-path", libPath)
}
newArgv = append(newArgv, guestPath)
if len(argv) > 1 {
newArgv = append(newArgv, argv[1:]...)
}
return &execRewrite{ExecPath: interpHost, Argv: newArgv}, true
}