//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/. 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 }