diff --git a/.golangci.yml b/.golangci.yml index 72b7797fa8072..c1a532979f3ef 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -65,6 +65,10 @@ linters: customlint: type: module + misspell: + ignore-rules: + - rela # ELF relocation entries with explicit addends. + depguard: rules: main: diff --git a/Herebyfile.mjs b/Herebyfile.mjs index cd7a8c45fc9d4..1038a6bf4b872 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -267,14 +267,42 @@ export const lib = task({ /** * Gets the release build flags for stripping debug info. * @param {string} [versionOverride] Optional version to embed in the binary. + * @param {object} [opts] + * @param {boolean} [opts.staticPie] Build a PIE to be converted by {@link convertToStaticPie}. * @returns {string[]} */ -function getReleaseBuildFlags(versionOverride) { +function getReleaseBuildFlags(versionOverride, opts) { let ldflags = "-ldflags=-s -w"; + if (opts?.staticPie) { + ldflags += " -bindnow"; + } if (versionOverride) { ldflags += ` -X github.com/microsoft/TypeScript/tsc/internal/core.version=${versionOverride}`; } - return ["-trimpath", ldflags]; + return ["-trimpath", ...(opts?.staticPie ? ["-buildmode=pie"] : []), ldflags]; +} + +// Linux targets where Go's internal linker can produce a PIE without cgo. +// Go's PIEs require the glibc loader, so these are converted to static-PIEs +// which also run on musl (Alpine), distroless, and NixOS. Other Linux targets +// would need an external linker for PIE, so they stay non-PIE. Windows, +// macOS, and Android are already PIE by default. +const staticPieGoarches = new Set(["amd64", "arm64", "loong64", "ppc64le", "s390x"]); + +/** + * @param {string} goos + * @param {string} goarch + */ +function supportsStaticPie(goos, goarch) { + return goos === "linux" && staticPieGoarches.has(goarch); +} + +/** + * Removes the ELF interpreter from a Go-linked PIE so it starts via tsc/internal/staticpie. + * @param {string} exe + */ +async function convertToStaticPie(exe) { + await run("go", ["-C", "./tools", "run", "./cmd/staticpie", path.resolve(exe)]); } /** @@ -2794,6 +2822,7 @@ async function buildNativePreviewPlatform({ npmDir, npmPackageName, nodeOs, node await generateLibs(out); const exeName = nativePreviewExeName(nodeOs); + const staticPie = !options.respectGoEnv && supportsStaticPie(goos, goarch); await buildTsc({ out: publishAsTypescript ? path.join(out, exeName) : out, env: { @@ -2803,8 +2832,12 @@ async function buildNativePreviewPlatform({ npmDir, npmPackageName, nodeOs, node }, extraFlags: options.respectGoEnv ? [] - : getReleaseBuildFlags(options.setPrerelease || nativePreviewReleaseVersion ? getVersion() : undefined), + : getReleaseBuildFlags(options.setPrerelease || nativePreviewReleaseVersion ? getVersion() : undefined, { staticPie }), }); + if (staticPie) { + // Without -o naming a file, go build names the binary after ./cmd/tsc. + await convertToStaticPie(publishAsTypescript ? path.join(out, exeName) : path.join(out, "tsc")); + } } async function createNativePreviewInputPackageJson() { diff --git a/tools/cmd/staticpie/main.go b/tools/cmd/staticpie/main.go new file mode 100644 index 0000000000000..dfea83908bcc9 --- /dev/null +++ b/tools/cmd/staticpie/main.go @@ -0,0 +1,236 @@ +// Command staticpie converts a Linux position-independent executable built by +// Go's internal linker into a static-PIE that runs without an ELF interpreter. +// +// Go can only link PIEs that name the system's dynamic loader in PT_INTERP, +// which ties them to glibc-based systems. The binary must link in +// tsc/internal/staticpie, whose entry function applies the relocations that +// the loader would otherwise apply. This command verifies that the binary +// needs nothing else from a loader, then removes PT_INTERP and makes that +// function the ELF entry point. +package main + +import ( + "bytes" + "debug/elf" + "debug/gosym" + "encoding/binary" + "errors" + "fmt" + "io" + "os" +) + +const entrySymbol = "github.com/microsoft/TypeScript/tsc/internal/staticpie.entry" + +// relativeRelocs maps each supported machine to its R_*_RELATIVE type, the +// only relocation the entry function knows how to apply. +var relativeRelocs = map[elf.Machine]uint64{ + elf.EM_X86_64: uint64(elf.R_X86_64_RELATIVE), + elf.EM_AARCH64: uint64(elf.R_AARCH64_RELATIVE), + elf.EM_LOONGARCH: uint64(elf.R_LARCH_RELATIVE), + elf.EM_PPC64: uint64(elf.R_PPC64_RELATIVE), + elf.EM_S390: uint64(elf.R_390_RELATIVE), +} + +const ( + ehdrPhoff = 32 + ehdrEntry = 24 + ehdrPhentsize = 54 + phdrSize = 56 + relaSize = 24 +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "usage: staticpie ") + os.Exit(2) + } + if err := run(os.Args[1]); err != nil { + fmt.Fprintf(os.Stderr, "staticpie: %s: %v\n", os.Args[1], err) + os.Exit(1) + } +} + +func run(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + if convertErr := convert(data); convertErr != nil { + return convertErr + } + info, err := os.Stat(path) + if err != nil { + return err + } + return os.WriteFile(path, data, info.Mode()) +} + +// convert rewrites data, an ELF executable, in place. +func convert(data []byte) error { + f, err := elf.NewFile(bytes.NewReader(data)) + if err != nil { + return err + } + defer f.Close() + + if f.Class != elf.ELFCLASS64 { + return errors.New("not a 64-bit ELF file") + } + if f.Type != elf.ET_DYN { + return fmt.Errorf("type is %v, not ET_DYN; was it built with -buildmode=pie?", f.Type) + } + relative, ok := relativeRelocs[f.Machine] + if !ok || f.Machine == elf.EM_PPC64 && f.ByteOrder != binary.LittleEndian { + return fmt.Errorf("unsupported machine %v (%v)", f.Machine, f.ByteOrder) + } + bo := f.ByteOrder + if phentsize := bo.Uint16(data[ehdrPhentsize:]); phentsize != phdrSize { + return fmt.Errorf("unexpected program header size %d", phentsize) + } + + interp := -1 + var dynamic *elf.Prog + hasPhdr := false + for i, p := range f.Progs { + switch p.Type { + case elf.PT_INTERP: + if interp >= 0 { + return errors.New("multiple PT_INTERP headers") + } + interp = i + case elf.PT_DYNAMIC: + dynamic = p + case elf.PT_PHDR: + hasPhdr = true + case elf.PT_NULL, elf.PT_LOAD, elf.PT_NOTE, elf.PT_TLS, elf.PT_GNU_STACK, elf.PT_GNU_RELRO: + continue + default: + // Unknown headers (e.g. PT_GNU_PROPERTY) may ask the loader + // for something the entry function does not do. + return fmt.Errorf("unsupported program header %v", p.Type) + } + } + if interp < 0 { + return errors.New("no PT_INTERP; is it already static?") + } + if dynamic == nil || !hasPhdr { + return errors.New("missing PT_DYNAMIC or PT_PHDR") + } + + relocations, err := checkDynamic(f, dynamic) + if err != nil { + return err + } + for off := 0; off < len(relocations); off += relaSize { + // Comparing all of r_info also checks that the symbol index is zero. + if info := bo.Uint64(relocations[off+8:]); info != relative { + return fmt.Errorf("unsupported relocation with r_info %#x", info) + } + } + + entry, err := findEntry(f) + if err != nil { + return err + } + + interpHdr := bo.Uint64(data[ehdrPhoff:]) + uint64(interp)*phdrSize + bo.PutUint32(data[interpHdr:], uint32(elf.PT_NULL)) + bo.PutUint64(data[ehdrEntry:], entry) + return nil +} + +// checkDynamic verifies that the dynamic section asks for nothing beyond +// RELATIVE relocations, and returns the contents of DT_RELA. +func checkDynamic(f *elf.File, dynamic *elf.Prog) ([]byte, error) { + dyn, err := io.ReadAll(dynamic.Open()) + if err != nil { + return nil, err + } + bo := f.ByteOrder + var relaAddr, relaSz uint64 + bindNow := false +loop: + for off := 0; off+16 <= len(dyn); off += 16 { + tag := elf.DynTag(bo.Uint64(dyn[off:])) + val := bo.Uint64(dyn[off+8:]) + switch tag { + case elf.DT_NULL: + break loop + case elf.DT_RELA: + relaAddr = val + case elf.DT_RELASZ: + relaSz = val + case elf.DT_RELAENT: + if val != relaSize { + return nil, fmt.Errorf("unexpected DT_RELAENT %d", val) + } + case elf.DT_FLAGS: + if flags := elf.DynFlag(val); flags&^elf.DF_BIND_NOW != 0 { + return nil, fmt.Errorf("unsupported DT_FLAGS %v", flags) + } + bindNow = bindNow || elf.DynFlag(val)&elf.DF_BIND_NOW != 0 + case elf.DT_FLAGS_1: + if flags := elf.DynFlag1(val); flags&^(elf.DF_1_NOW|elf.DF_1_PIE) != 0 { + return nil, fmt.Errorf("unsupported DT_FLAGS_1 %v", flags) + } + bindNow = bindNow || elf.DynFlag1(val)&elf.DF_1_NOW != 0 + case elf.DT_HASH, elf.DT_STRTAB, elf.DT_STRSZ, elf.DT_SYMTAB, elf.DT_SYMENT, elf.DT_DEBUG: + // The symbol tables are for debuggers; DT_DEBUG is filled in + // by a loader, if present. + case elf.DT_PLTGOT, elf.DT_PPC64_OPT: + // Without DT_JMPREL there are no PLT entries for a loader to + // resolve, and DT_PPC64_OPT only enables loader optimizations. + default: + // Anything else may need a loader to act on it. + return nil, fmt.Errorf("unsupported dynamic tag %v", tag) + } + } + // The entry function applies all relocations up front, so the binary + // is effectively BIND_NOW; requiring the flag keeps it advertised. + if !bindNow { + return nil, errors.New("not linked with -bindnow") + } + if relaSz%relaSize != 0 { + return nil, fmt.Errorf("DT_RELASZ %d is not a multiple of %d", relaSz, relaSize) + } + if relaSz == 0 { + return nil, nil + } + relocations := make([]byte, relaSz) + for _, p := range f.Progs { + if p.Type == elf.PT_LOAD && p.Vaddr <= relaAddr && relaAddr+relaSz <= p.Vaddr+p.Filesz { + if _, err := p.ReadAt(relocations, int64(relaAddr-p.Vaddr)); err != nil { + return nil, err + } + return relocations, nil + } + } + return nil, errors.New("DT_RELA is not within a loaded segment") +} + +// findEntry looks up the staticpie entry function in the Go symbol table, +// which is kept even in binaries stripped with -s. +func findEntry(f *elf.File) (uint64, error) { + pclntab := f.Section(".gopclntab") + text := f.Section(".text") + if pclntab == nil || text == nil { + return 0, errors.New("missing .gopclntab or .text") + } + pcln, err := pclntab.Data() + if err != nil { + return 0, err + } + table, err := gosym.NewTable(nil, gosym.NewLineTable(pcln, text.Addr)) + if err != nil { + return 0, err + } + fn := table.LookupFunc(entrySymbol) + if fn == nil { + return 0, fmt.Errorf("%s not found; is the staticpie package linked in?", entrySymbol) + } + if fn.Entry < text.Addr || fn.Entry >= text.Addr+text.Size { + return 0, fmt.Errorf("%s at %#x is outside .text", entrySymbol, fn.Entry) + } + return fn.Entry, nil +} diff --git a/tsc/cmd/tsc/main.go b/tsc/cmd/tsc/main.go index 899113dc19fc9..319e70859716d 100644 --- a/tsc/cmd/tsc/main.go +++ b/tsc/cmd/tsc/main.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/execute" "github.com/microsoft/TypeScript/tsc/internal/osutil" + _ "github.com/microsoft/TypeScript/tsc/internal/staticpie" ) func main() { diff --git a/tsc/internal/staticpie/entry.go b/tsc/internal/staticpie/entry.go new file mode 100644 index 0000000000000..a4796d83364e8 --- /dev/null +++ b/tsc/internal/staticpie/entry.go @@ -0,0 +1,25 @@ +//go:build linux && (amd64 || arm64 || loong64 || ppc64le || s390x) + +package staticpie + +// entry is the replacement ELF entry point, implemented in assembly. +// +// It runs before any relocations are applied, so it may only use +// PC-relative addressing. It: +// +// 1. finds the auxiliary vector above argv and envp on the initial stack; +// 2. uses AT_PHDR and the PT_PHDR program header to compute the load bias; +// 3. applies every dynamic relocation, failing on anything other than +// R_*_RELATIVE; +// 4. makes the page-aligned part of PT_GNU_RELRO read-only, as ld.so does; +// 5. jumps to the runtime's _rt0__linux with the stack untouched. +// +// It exits with status 127 if anything is unexpected. +func entry() + +// entryAddr returns the address of entry. Calling it keeps entry in the +// binary without referencing it from Go, which would make the linker emit +// an ABI wrapper under the same name. +func entryAddr() uintptr + +var _ = entryAddr() diff --git a/tsc/internal/staticpie/entry_linux_amd64.s b/tsc/internal/staticpie/entry_linux_amd64.s new file mode 100644 index 0000000000000..66d3feb39bc83 --- /dev/null +++ b/tsc/internal/staticpie/entry_linux_amd64.s @@ -0,0 +1,140 @@ +#include "textflag.h" +#include "staticpie.h" + +#define R_X86_64_RELATIVE 8 +#define SYS_mprotect 10 +#define SYS_exit_group 231 + +// R15 is avoided because the assembler clobbers it in -shared mode. +TEXT ·entry(SB),NOSPLIT|NOFRAME,$0 + // Skip argc, argv, and envp to find auxv. + MOVQ 0(SP), AX + LEAQ 16(SP)(AX*8), R8 +envloop: + MOVQ 0(R8), AX + ADDQ $8, R8 + TESTQ AX, AX + JNZ envloop + + // R9 = AT_PHDR, R10 = AT_PHNUM, R11 = AT_PAGESZ + XORQ R9, R9 + XORQ R10, R10 + XORQ R11, R11 +auxloop: + MOVQ 0(R8), AX + MOVQ 8(R8), BX + ADDQ $16, R8 + TESTQ AX, AX + JZ auxdone + CMPQ AX, $AT_PHDR + CMOVQEQ BX, R9 + CMPQ AX, $AT_PHNUM + CMOVQEQ BX, R10 + CMPQ AX, $AT_PAGESZ + CMOVQEQ BX, R11 + JMP auxloop +auxdone: + TESTQ R9, R9 + JZ fail + TESTQ R10, R10 + JZ fail + TESTQ R11, R11 + JZ fail + + // R12 = load bias, BP = PT_PHDR seen, R13 = PT_DYNAMIC vaddr, + // R14 = PT_GNU_RELRO vaddr, CX = PT_GNU_RELRO memsz + MOVQ R9, SI + MOVQ R10, DI + XORQ BP, BP + XORQ R13, R13 + XORQ R14, R14 + XORQ CX, CX +phloop: + TESTQ DI, DI + JZ phdone + MOVL 0(SI), AX + CMPL AX, $PT_PHDR + JNE notphdr + MOVQ R9, R12 + SUBQ P_VADDR(SI), R12 + MOVQ $1, BP +notphdr: + CMPL AX, $PT_DYNAMIC + JNE notdyn + MOVQ P_VADDR(SI), R13 +notdyn: + CMPL AX, $PT_GNU_RELRO + JNE phnext + MOVQ P_VADDR(SI), R14 + MOVQ P_MEMSZ(SI), CX +phnext: + ADDQ $PHDR_SIZE, SI + DECQ DI + JMP phloop +phdone: + TESTQ BP, BP + JZ fail + TESTQ R13, R13 + JZ fail + + // SI = DT_RELA, DI = DT_RELASZ + ADDQ R12, R13 + XORQ SI, SI + XORQ DI, DI +dynloop: + MOVQ 0(R13), AX + MOVQ 8(R13), BX + ADDQ $16, R13 + TESTQ AX, AX + JZ dyndone + CMPQ AX, $DT_RELA + CMOVQEQ BX, SI + CMPQ AX, $DT_RELASZ + CMOVQEQ BX, DI + JMP dynloop +dyndone: + ADDQ R12, SI + ADDQ SI, DI +relloop: + CMPQ SI, DI + JEQ reldone + MOVQ R_INFO(SI), AX + CMPQ AX, $R_X86_64_RELATIVE + JNE fail + MOVQ R_OFFSET(SI), BX + MOVQ R_ADDEND(SI), AX + ADDQ R12, AX + MOVQ AX, (R12)(BX*1) + ADDQ $RELA_SIZE, SI + JMP relloop +reldone: + TESTQ CX, CX + JZ start + ADDQ R12, R14 + LEAQ (R14)(CX*1), SI + MOVQ R11, AX + NEGQ AX + ANDQ AX, R14 + ANDQ AX, SI + SUBQ R14, SI + JZ start + MOVQ R14, DI + MOVQ $PROT_READ, DX + MOVQ $SYS_mprotect, AX + SYSCALL + TESTQ AX, AX + JNZ fail +start: + // The ABI says DX holds a function to register with atexit. + XORQ DX, DX + JMP _rt0_amd64_linux(SB) +fail: + MOVQ $EXIT_STATUS, DI + MOVQ $SYS_exit_group, AX + SYSCALL + JMP fail + +TEXT ·entryAddr(SB),NOSPLIT,$0-8 + LEAQ ·entry(SB), AX + MOVQ AX, ret+0(FP) + RET diff --git a/tsc/internal/staticpie/entry_linux_arm64.s b/tsc/internal/staticpie/entry_linux_arm64.s new file mode 100644 index 0000000000000..f68885f3253a8 --- /dev/null +++ b/tsc/internal/staticpie/entry_linux_arm64.s @@ -0,0 +1,123 @@ +#include "textflag.h" +#include "staticpie.h" + +#define R_AARCH64_RELATIVE 1027 +#define SYS_mprotect 226 +#define SYS_exit_group 94 + +TEXT ·entry(SB),NOSPLIT|NOFRAME,$0 + // Skip argc, argv, and envp to find auxv. + MOVD 0(RSP), R1 + ADD $16, RSP, R0 + ADD R1<<3, R0, R0 +envloop: + MOVD.P 8(R0), R2 + CBNZ R2, envloop + + // R9 = AT_PHDR, R10 = AT_PHNUM, R11 = AT_PAGESZ + MOVD ZR, R9 + MOVD ZR, R10 + MOVD ZR, R11 +auxloop: + MOVD 0(R0), R2 + MOVD 8(R0), R3 + ADD $16, R0 + CBZ R2, auxdone + CMP $AT_PHDR, R2 + CSEL EQ, R3, R9, R9 + CMP $AT_PHNUM, R2 + CSEL EQ, R3, R10, R10 + CMP $AT_PAGESZ, R2 + CSEL EQ, R3, R11, R11 + B auxloop +auxdone: + CBZ R9, fail + CBZ R10, fail + CBZ R11, fail + + // R12 = load bias, R13 = PT_PHDR seen, R14 = PT_DYNAMIC vaddr, + // R15 = PT_GNU_RELRO vaddr, R16 = PT_GNU_RELRO memsz + MOVD R9, R4 + MOVD R10, R5 + MOVD ZR, R13 + MOVD ZR, R14 + MOVD ZR, R15 + MOVD ZR, R16 + MOVD $1, R7 + MOVD $PT_GNU_RELRO, R17 +phloop: + CBZ R5, phdone + MOVWU 0(R4), R2 + MOVD P_VADDR(R4), R3 + MOVD P_MEMSZ(R4), R6 + SUB R3, R9, R8 + CMP $PT_PHDR, R2 + CSEL EQ, R8, R12, R12 + CSEL EQ, R7, R13, R13 + CMP $PT_DYNAMIC, R2 + CSEL EQ, R3, R14, R14 + CMP R17, R2 + CSEL EQ, R3, R15, R15 + CSEL EQ, R6, R16, R16 + ADD $PHDR_SIZE, R4 + SUB $1, R5 + B phloop +phdone: + CBZ R13, fail + CBZ R14, fail + + // R4 = DT_RELA, R5 = DT_RELASZ + ADD R12, R14 + MOVD ZR, R4 + MOVD ZR, R5 +dynloop: + MOVD 0(R14), R2 + MOVD 8(R14), R3 + ADD $16, R14 + CBZ R2, dyndone + CMP $DT_RELA, R2 + CSEL EQ, R3, R4, R4 + CMP $DT_RELASZ, R2 + CSEL EQ, R3, R5, R5 + B dynloop +dyndone: + ADD R12, R4 + ADD R4, R5 +relloop: + CMP R4, R5 + BEQ reldone + MOVD R_INFO(R4), R2 + CMP $R_AARCH64_RELATIVE, R2 + BNE fail + MOVD R_OFFSET(R4), R2 + MOVD R_ADDEND(R4), R3 + ADD R12, R3 + MOVD R3, (R12)(R2) + ADD $RELA_SIZE, R4 + B relloop +reldone: + CBZ R16, start + ADD R12, R15 + ADD R15, R16, R1 + NEG R11, R2 + AND R2, R15 + AND R2, R1 + SUB R15, R1, R1 + CBZ R1, start + MOVD R15, R0 + MOVD $PROT_READ, R2 + MOVD $SYS_mprotect, R8 + SVC + CBNZ R0, fail +start: + B _rt0_arm64_linux(SB) +fail: + MOVD $EXIT_STATUS, R0 + MOVD $SYS_exit_group, R8 + SVC + B fail + +TEXT ·entryAddr(SB),NOSPLIT,$0-8 + MOVD $·entry(SB), R0 + MOVD R0, ret+0(FP) + RET diff --git a/tsc/internal/staticpie/entry_linux_loong64.s b/tsc/internal/staticpie/entry_linux_loong64.s new file mode 100644 index 0000000000000..bb3671b9c6775 --- /dev/null +++ b/tsc/internal/staticpie/entry_linux_loong64.s @@ -0,0 +1,135 @@ +#include "textflag.h" +#include "staticpie.h" + +#define R_LARCH_RELATIVE 3 +#define SYS_mprotect 226 +#define SYS_exit_group 94 + +TEXT ·entry(SB),NOSPLIT|NOFRAME,$0 + // Skip argc, argv, and envp to find auxv. + MOVV 0(R3), R5 + SLLV $3, R5 + ADDV R3, R5 + ADDV $16, R5 +envloop: + MOVV 0(R5), R6 + ADDV $8, R5 + BNE R6, envloop + + // R7 = AT_PHDR, R8 = AT_PHNUM, R9 = AT_PAGESZ + MOVV R0, R7 + MOVV R0, R8 + MOVV R0, R9 +auxloop: + MOVV 0(R5), R6 + MOVV 8(R5), R10 + ADDV $16, R5 + BEQ R6, auxdone + MOVV $AT_PHDR, R19 + BNE R6, R19, notphdraux + MOVV R10, R7 +notphdraux: + MOVV $AT_PHNUM, R19 + BNE R6, R19, notphnum + MOVV R10, R8 +notphnum: + MOVV $AT_PAGESZ, R19 + BNE R6, R19, auxloop + MOVV R10, R9 + JMP auxloop +auxdone: + BEQ R7, fail + BEQ R8, fail + BEQ R9, fail + + // R14 = load bias, R15 = PT_PHDR seen, R16 = PT_DYNAMIC vaddr, + // R17 = PT_GNU_RELRO vaddr, R18 = PT_GNU_RELRO memsz + MOVV R7, R12 + MOVV R8, R13 + MOVV R0, R15 + MOVV R0, R16 + MOVV R0, R17 + MOVV R0, R18 +phloop: + BEQ R13, phdone + MOVWU 0(R12), R6 + MOVV P_VADDR(R12), R10 + MOVV $PT_PHDR, R19 + BNE R6, R19, notphdr + SUBV R10, R7, R14 + MOVV $1, R15 +notphdr: + MOVV $PT_DYNAMIC, R19 + BNE R6, R19, notdyn + MOVV R10, R16 +notdyn: + MOVV $PT_GNU_RELRO, R19 + BNE R6, R19, phnext + MOVV R10, R17 + MOVV P_MEMSZ(R12), R18 +phnext: + ADDV $PHDR_SIZE, R12 + SUBV $1, R13 + JMP phloop +phdone: + BEQ R15, fail + BEQ R16, fail + + // R12 = DT_RELA, R13 = DT_RELASZ + ADDV R14, R16 + MOVV R0, R12 + MOVV R0, R13 +dynloop: + MOVV 0(R16), R6 + MOVV 8(R16), R10 + ADDV $16, R16 + BEQ R6, dyndone + MOVV $DT_RELA, R19 + BNE R6, R19, notrela + MOVV R10, R12 +notrela: + MOVV $DT_RELASZ, R19 + BNE R6, R19, dynloop + MOVV R10, R13 + JMP dynloop +dyndone: + ADDV R14, R12 + ADDV R12, R13 + MOVV $R_LARCH_RELATIVE, R19 +relloop: + BEQ R12, R13, reldone + MOVV R_INFO(R12), R6 + BNE R6, R19, fail + MOVV R_OFFSET(R12), R6 + MOVV R_ADDEND(R12), R10 + ADDV R14, R10 + ADDV R14, R6 + MOVV R10, 0(R6) + ADDV $RELA_SIZE, R12 + JMP relloop +reldone: + BEQ R18, start + ADDV R14, R17 + ADDV R17, R18, R6 + SUBV R9, R0, R10 + AND R10, R17 + AND R10, R6 + SUBV R17, R6, R5 + BEQ R5, start + MOVV R17, R4 + MOVV $PROT_READ, R6 + MOVV $SYS_mprotect, R11 + SYSCALL + BNE R4, fail +start: + JMP _rt0_loong64_linux(SB) +fail: + MOVV $EXIT_STATUS, R4 + MOVV $SYS_exit_group, R11 + SYSCALL + JMP fail + +TEXT ·entryAddr(SB),NOSPLIT,$0-8 + MOVV $·entry(SB), R4 + MOVV R4, ret+0(FP) + RET diff --git a/tsc/internal/staticpie/entry_linux_ppc64le.s b/tsc/internal/staticpie/entry_linux_ppc64le.s new file mode 100644 index 0000000000000..368d80c0093ea --- /dev/null +++ b/tsc/internal/staticpie/entry_linux_ppc64le.s @@ -0,0 +1,150 @@ +#include "textflag.h" +#include "staticpie.h" + +#define R_PPC64_RELATIVE 22 +#define SYS_mprotect 125 +#define SYS_exit_group 234 + +// The kernel sets R12 to the entry address, from which the assembler's +// prologue computes R2 (the TOC pointer), and R13 to zero, which the runtime +// relies on. Neither is touched here, nor is R30 (g) or R31 (REGTMP). +TEXT ·entry(SB),NOSPLIT|NOFRAME,$0 + // Skip argc, argv, and envp to find auxv. + MOVD 0(R1), R5 + SLD $3, R5 + ADD R1, R5 + ADD $16, R5 +envloop: + MOVD 0(R5), R6 + ADD $8, R5 + CMP R6, $0 + BNE envloop + + // R7 = AT_PHDR, R8 = AT_PHNUM, R9 = AT_PAGESZ + MOVD $0, R7 + MOVD $0, R8 + MOVD $0, R9 +auxloop: + MOVD 0(R5), R6 + MOVD 8(R5), R10 + ADD $16, R5 + CMP R6, $0 + BEQ auxdone + CMP R6, $AT_PHDR + BNE notphdraux + MOVD R10, R7 +notphdraux: + CMP R6, $AT_PHNUM + BNE notphnum + MOVD R10, R8 +notphnum: + CMP R6, $AT_PAGESZ + BNE auxloop + MOVD R10, R9 + BR auxloop +auxdone: + CMP R7, $0 + BEQ fail + CMP R8, $0 + BEQ fail + CMP R9, $0 + BEQ fail + + // R14 = load bias, R15 = PT_PHDR seen, R16 = PT_DYNAMIC vaddr, + // R17 = PT_GNU_RELRO vaddr, R18 = PT_GNU_RELRO memsz + MOVD R7, R11 + MOVD R8, R19 + MOVD $0, R15 + MOVD $0, R16 + MOVD $0, R17 + MOVD $0, R18 + MOVD $PT_GNU_RELRO, R20 +phloop: + CMP R19, $0 + BEQ phdone + MOVWZ 0(R11), R6 + MOVD P_VADDR(R11), R10 + CMP R6, $PT_PHDR + BNE notphdr + SUB R10, R7, R14 + MOVD $1, R15 +notphdr: + CMP R6, $PT_DYNAMIC + BNE notdyn + MOVD R10, R16 +notdyn: + CMP R6, R20 + BNE phnext + MOVD R10, R17 + MOVD P_MEMSZ(R11), R18 +phnext: + ADD $PHDR_SIZE, R11 + ADD $-1, R19 + BR phloop +phdone: + CMP R15, $0 + BEQ fail + CMP R16, $0 + BEQ fail + + // R11 = DT_RELA, R19 = DT_RELASZ + ADD R14, R16 + MOVD $0, R11 + MOVD $0, R19 +dynloop: + MOVD 0(R16), R6 + MOVD 8(R16), R10 + ADD $16, R16 + CMP R6, $0 + BEQ dyndone + CMP R6, $DT_RELA + BNE notrela + MOVD R10, R11 +notrela: + CMP R6, $DT_RELASZ + BNE dynloop + MOVD R10, R19 + BR dynloop +dyndone: + ADD R14, R11 + ADD R11, R19 +relloop: + CMP R11, R19 + BEQ reldone + MOVD R_INFO(R11), R6 + CMP R6, $R_PPC64_RELATIVE + BNE fail + MOVD R_OFFSET(R11), R6 + MOVD R_ADDEND(R11), R10 + ADD R14, R10 + MOVD R10, (R14)(R6) + ADD $RELA_SIZE, R11 + BR relloop +reldone: + CMP R18, $0 + BEQ start + ADD R14, R17 + ADD R17, R18, R6 + NEG R9, R10 + AND R10, R17 + AND R10, R6 + SUB R17, R6, R4 + CMP R4, $0 + BEQ start + MOVD R17, R3 + MOVD $PROT_READ, R5 + MOVD $SYS_mprotect, R0 + SYSCALL + BVS fail +start: + BR _rt0_ppc64le_linux(SB) +fail: + MOVD $EXIT_STATUS, R3 + MOVD $SYS_exit_group, R0 + SYSCALL + BR fail + +TEXT ·entryAddr(SB),NOSPLIT,$0-8 + MOVD $·entry(SB), R3 + MOVD R3, ret+0(FP) + RET diff --git a/tsc/internal/staticpie/entry_linux_s390x.s b/tsc/internal/staticpie/entry_linux_s390x.s new file mode 100644 index 0000000000000..0860938a6ec1c --- /dev/null +++ b/tsc/internal/staticpie/entry_linux_s390x.s @@ -0,0 +1,127 @@ +#include "textflag.h" +#include "staticpie.h" + +#define R_390_RELATIVE 12 +#define SYS_mprotect 125 +#define SYS_exit_group 248 + +// R10 and R11 (REGTMP) and R13 (g) are avoided. +TEXT ·entry(SB),NOSPLIT|NOFRAME,$0 + // Skip argc, argv, and envp to find auxv. + MOVD 0(R15), R5 + SLD $3, R5 + ADD R15, R5 + ADD $16, R5 +envloop: + MOVD 0(R5), R6 + ADD $8, R5 + CMPBNE R6, $0, envloop + + // R7 = AT_PHDR, R8 = AT_PHNUM, R9 = AT_PAGESZ + MOVD $0, R7 + MOVD $0, R8 + MOVD $0, R9 +auxloop: + MOVD 0(R5), R6 + MOVD 8(R5), R12 + ADD $16, R5 + CMPBEQ R6, $0, auxdone + CMPBNE R6, $AT_PHDR, notphdraux + MOVD R12, R7 +notphdraux: + CMPBNE R6, $AT_PHNUM, notphnum + MOVD R12, R8 +notphnum: + CMPBNE R6, $AT_PAGESZ, auxloop + MOVD R12, R9 + BR auxloop +auxdone: + CMPBEQ R7, $0, fail + CMPBEQ R8, $0, fail + CMPBEQ R9, $0, fail + + // R14 = load bias, R0 = PT_PHDR seen, R1 = PT_DYNAMIC vaddr, + // R2 = PT_GNU_RELRO vaddr, R3 = PT_GNU_RELRO memsz + MOVD R7, R4 + MOVD R8, R5 + MOVD $0, R0 + MOVD $0, R1 + MOVD $0, R2 + MOVD $0, R3 + MOVD $PT_GNU_RELRO, R6 +phloop: + CMPBEQ R5, $0, phdone + MOVWZ 0(R4), R12 + MOVD P_VADDR(R4), R8 + CMPBNE R12, $PT_PHDR, notphdr + SUB R8, R7, R14 + MOVD $1, R0 +notphdr: + CMPBNE R12, $PT_DYNAMIC, notdyn + MOVD R8, R1 +notdyn: + CMPBNE R12, R6, phnext + MOVD R8, R2 + MOVD P_MEMSZ(R4), R3 +phnext: + ADD $PHDR_SIZE, R4 + SUB $1, R5 + BR phloop +phdone: + CMPBEQ R0, $0, fail + CMPBEQ R1, $0, fail + + // R4 = DT_RELA, R5 = DT_RELASZ + ADD R14, R1 + MOVD $0, R4 + MOVD $0, R5 +dynloop: + MOVD 0(R1), R12 + MOVD 8(R1), R8 + ADD $16, R1 + CMPBEQ R12, $0, dyndone + CMPBNE R12, $DT_RELA, notrela + MOVD R8, R4 +notrela: + CMPBNE R12, $DT_RELASZ, dynloop + MOVD R8, R5 + BR dynloop +dyndone: + ADD R14, R4 + ADD R4, R5 +relloop: + CMPBEQ R4, R5, reldone + MOVD R_INFO(R4), R12 + CMPBNE R12, $R_390_RELATIVE, fail + MOVD R_OFFSET(R4), R12 + MOVD R_ADDEND(R4), R8 + ADD R14, R8 + MOVD R8, 0(R14)(R12*1) + ADD $RELA_SIZE, R4 + BR relloop +reldone: + CMPBEQ R3, $0, start + ADD R14, R2 + ADD R2, R3 + MOVD R9, R12 + NEG R12 + AND R12, R2 + AND R12, R3 + SUB R2, R3 + CMPBEQ R3, $0, start + MOVD $PROT_READ, R4 + MOVD $SYS_mprotect, R1 + SYSCALL + CMPBNE R2, $0, fail +start: + BR _rt0_s390x_linux(SB) +fail: + MOVD $EXIT_STATUS, R2 + MOVD $SYS_exit_group, R1 + SYSCALL + BR fail + +TEXT ·entryAddr(SB),NOSPLIT,$0-8 + MOVD $·entry(SB), R2 + MOVD R2, ret+0(FP) + RET diff --git a/tsc/internal/staticpie/staticpie.go b/tsc/internal/staticpie/staticpie.go new file mode 100644 index 0000000000000..5451568c491d0 --- /dev/null +++ b/tsc/internal/staticpie/staticpie.go @@ -0,0 +1,18 @@ +// Package staticpie provides an entry point that lets a Go-linked +// position-independent executable start without an ELF interpreter. +// +// Go's internal linker can only produce PIEs that ask the system's dynamic +// loader (e.g. /lib64/ld-linux-x86-64.so.2) to apply their relocations. That +// loader path does not exist on musl-based systems like Alpine, in distroless +// containers, or on NixOS, so those binaries fail to start there. +// +// The tools/cmd/staticpie post-processor removes PT_INTERP from such a binary +// and points its ELF entry at the entry function in this package, which does +// the loader's remaining work (applying R_*_RELATIVE relocations and +// write-protecting PT_GNU_RELRO) before jumping to the Go runtime's usual +// entry point. The result is a static-PIE that runs on any Linux system. +// +// This package must be linked into the binary for the post-processor to find +// the entry function; it has no effect on binaries that are not +// post-processed. +package staticpie diff --git a/tsc/internal/staticpie/staticpie.h b/tsc/internal/staticpie/staticpie.h new file mode 100644 index 0000000000000..3b2c07db11fb8 --- /dev/null +++ b/tsc/internal/staticpie/staticpie.h @@ -0,0 +1,24 @@ +#define AT_PHDR 3 +#define AT_PHNUM 5 +#define AT_PAGESZ 6 + +#define PT_DYNAMIC 2 +#define PT_PHDR 6 +#define PT_GNU_RELRO 0x6474e552 + +#define DT_RELA 7 +#define DT_RELASZ 8 + +// Elf64_Phdr field offsets and size. +#define P_VADDR 16 +#define P_MEMSZ 40 +#define PHDR_SIZE 56 + +// Elf64_Rela field offsets and size. +#define R_OFFSET 0 +#define R_INFO 8 +#define R_ADDEND 16 +#define RELA_SIZE 24 + +#define PROT_READ 1 +#define EXIT_STATUS 127