Files
visor/pkg/sentry/loader/elf.go
T
Dean DengandgVisor bot 1352edc1fc Update symlink traversal limit when resolving interpreter path.
When execveat is called on an interpreter script, the symlink count for
resolving the script path should be separate from the count for resolving the
the corresponding interpreter. An ELOOP error should not occur if we do not hit
the symlink limit along any individual path, even if the total number of
symlinks encountered exceeds the limit.

Closes #574

PiperOrigin-RevId: 277358474
2019-10-29 13:59:28 -07:00

692 lines
20 KiB
Go

// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package loader
import (
"bytes"
"debug/elf"
"fmt"
"io"
"gvisor.dev/gvisor/pkg/abi"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/binary"
"gvisor.dev/gvisor/pkg/cpuid"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/arch"
"gvisor.dev/gvisor/pkg/sentry/context"
"gvisor.dev/gvisor/pkg/sentry/fs"
"gvisor.dev/gvisor/pkg/sentry/limits"
"gvisor.dev/gvisor/pkg/sentry/memmap"
"gvisor.dev/gvisor/pkg/sentry/mm"
"gvisor.dev/gvisor/pkg/sentry/usermem"
"gvisor.dev/gvisor/pkg/syserror"
)
const (
// elfMagic identifies an ELF file.
elfMagic = "\x7fELF"
// maxTotalPhdrSize is the maximum combined size of all program
// headers. Linux limits this to one page.
maxTotalPhdrSize = usermem.PageSize
)
var (
// header64Size is the size of elf.Header64.
header64Size = int(binary.Size(elf.Header64{}))
// Prog64Size is the size of elf.Prog64.
prog64Size = int(binary.Size(elf.Prog64{}))
)
func progFlagsAsPerms(f elf.ProgFlag) usermem.AccessType {
var p usermem.AccessType
if f&elf.PF_R == elf.PF_R {
p.Read = true
}
if f&elf.PF_W == elf.PF_W {
p.Write = true
}
if f&elf.PF_X == elf.PF_X {
p.Execute = true
}
return p
}
// elfInfo contains the metadata needed to load an ELF binary.
type elfInfo struct {
// os is the target OS of the ELF.
os abi.OS
// arch is the target architecture of the ELF.
arch arch.Arch
// entry is the program entry point.
entry usermem.Addr
// phdrs are the program headers.
phdrs []elf.ProgHeader
// phdrSize is the size of a single program header in the ELF.
phdrSize int
// phdrOff is the offset of the program headers in the file.
phdrOff uint64
// sharedObject is true if the ELF represents a shared object.
sharedObject bool
}
// parseHeader parse the ELF header, verifying that this is a supported ELF
// file and returning the ELF program headers.
//
// This is similar to elf.NewFile, except that it is more strict about what it
// accepts from the ELF, and it doesn't parse unnecessary parts of the file.
//
// ctx may be nil if f does not need it.
func parseHeader(ctx context.Context, f *fs.File) (elfInfo, error) {
// Check ident first; it will tell us the endianness of the rest of the
// structs.
var ident [elf.EI_NIDENT]byte
_, err := readFull(ctx, f, usermem.BytesIOSequence(ident[:]), 0)
if err != nil {
log.Infof("Error reading ELF ident: %v", err)
// The entire ident array always exists.
if err == io.EOF || err == io.ErrUnexpectedEOF {
err = syserror.ENOEXEC
}
return elfInfo{}, err
}
// Only some callers pre-check the ELF magic.
if !bytes.Equal(ident[:len(elfMagic)], []byte(elfMagic)) {
log.Infof("File is not an ELF")
return elfInfo{}, syserror.ENOEXEC
}
// We only support 64-bit, little endian binaries
if class := elf.Class(ident[elf.EI_CLASS]); class != elf.ELFCLASS64 {
log.Infof("Unsupported ELF class: %v", class)
return elfInfo{}, syserror.ENOEXEC
}
if endian := elf.Data(ident[elf.EI_DATA]); endian != elf.ELFDATA2LSB {
log.Infof("Unsupported ELF endianness: %v", endian)
return elfInfo{}, syserror.ENOEXEC
}
byteOrder := binary.LittleEndian
if version := elf.Version(ident[elf.EI_VERSION]); version != elf.EV_CURRENT {
log.Infof("Unsupported ELF version: %v", version)
return elfInfo{}, syserror.ENOEXEC
}
// EI_OSABI is ignored by Linux, which is the only OS supported.
os := abi.Linux
var hdr elf.Header64
hdrBuf := make([]byte, header64Size)
_, err = readFull(ctx, f, usermem.BytesIOSequence(hdrBuf), 0)
if err != nil {
log.Infof("Error reading ELF header: %v", err)
// The entire header always exists.
if err == io.EOF || err == io.ErrUnexpectedEOF {
err = syserror.ENOEXEC
}
return elfInfo{}, err
}
binary.Unmarshal(hdrBuf, byteOrder, &hdr)
// We support amd64 and arm64.
var a arch.Arch
switch machine := elf.Machine(hdr.Machine); machine {
case elf.EM_X86_64:
a = arch.AMD64
case elf.EM_AARCH64:
a = arch.ARM64
default:
log.Infof("Unsupported ELF machine %d", machine)
return elfInfo{}, syserror.ENOEXEC
}
var sharedObject bool
elfType := elf.Type(hdr.Type)
switch elfType {
case elf.ET_EXEC:
sharedObject = false
case elf.ET_DYN:
sharedObject = true
default:
log.Infof("Unsupported ELF type %v", elfType)
return elfInfo{}, syserror.ENOEXEC
}
if int(hdr.Phentsize) != prog64Size {
log.Infof("Unsupported phdr size %d", hdr.Phentsize)
return elfInfo{}, syserror.ENOEXEC
}
totalPhdrSize := prog64Size * int(hdr.Phnum)
if totalPhdrSize < prog64Size {
log.Warningf("No phdrs or total phdr size overflows: prog64Size: %d phnum: %d", prog64Size, int(hdr.Phnum))
return elfInfo{}, syserror.ENOEXEC
}
if totalPhdrSize > maxTotalPhdrSize {
log.Infof("Too many phdrs (%d): total size %d > %d", hdr.Phnum, totalPhdrSize, maxTotalPhdrSize)
return elfInfo{}, syserror.ENOEXEC
}
phdrBuf := make([]byte, totalPhdrSize)
_, err = readFull(ctx, f, usermem.BytesIOSequence(phdrBuf), int64(hdr.Phoff))
if err != nil {
log.Infof("Error reading ELF phdrs: %v", err)
// If phdrs were specified, they should all exist.
if err == io.EOF || err == io.ErrUnexpectedEOF {
err = syserror.ENOEXEC
}
return elfInfo{}, err
}
phdrs := make([]elf.ProgHeader, hdr.Phnum)
for i := range phdrs {
var prog64 elf.Prog64
binary.Unmarshal(phdrBuf[:prog64Size], byteOrder, &prog64)
phdrBuf = phdrBuf[prog64Size:]
phdrs[i] = elf.ProgHeader{
Type: elf.ProgType(prog64.Type),
Flags: elf.ProgFlag(prog64.Flags),
Off: prog64.Off,
Vaddr: prog64.Vaddr,
Paddr: prog64.Paddr,
Filesz: prog64.Filesz,
Memsz: prog64.Memsz,
Align: prog64.Align,
}
}
return elfInfo{
os: os,
arch: a,
entry: usermem.Addr(hdr.Entry),
phdrs: phdrs,
phdrOff: hdr.Phoff,
phdrSize: prog64Size,
sharedObject: sharedObject,
}, nil
}
// mapSegment maps a phdr into the Task. offset is the offset to apply to
// phdr.Vaddr.
func mapSegment(ctx context.Context, m *mm.MemoryManager, f *fs.File, phdr *elf.ProgHeader, offset usermem.Addr) error {
// We must make a page-aligned mapping.
adjust := usermem.Addr(phdr.Vaddr).PageOffset()
addr, ok := offset.AddLength(phdr.Vaddr)
if !ok {
// If offset != 0 we should have ensured this would fit.
ctx.Warningf("Computed segment load address overflows: %#x + %#x", phdr.Vaddr, offset)
return syserror.ENOEXEC
}
addr -= usermem.Addr(adjust)
fileSize := phdr.Filesz + adjust
if fileSize < phdr.Filesz {
ctx.Infof("Computed segment file size overflows: %#x + %#x", phdr.Filesz, adjust)
return syserror.ENOEXEC
}
ms, ok := usermem.Addr(fileSize).RoundUp()
if !ok {
ctx.Infof("fileSize %#x too large", fileSize)
return syserror.ENOEXEC
}
mapSize := uint64(ms)
if mapSize > 0 {
// This must result in a page-aligned offset. i.e., the original
// phdr.Off must have the same alignment as phdr.Vaddr. If that is not
// true, MMap will reject the mapping.
fileOffset := phdr.Off - adjust
prot := progFlagsAsPerms(phdr.Flags)
mopts := memmap.MMapOpts{
Length: mapSize,
Offset: fileOffset,
Addr: addr,
Fixed: true,
// Linux will happily allow conflicting segments to map over
// one another.
Unmap: true,
Private: true,
Perms: prot,
MaxPerms: usermem.AnyAccess,
}
defer func() {
if mopts.MappingIdentity != nil {
mopts.MappingIdentity.DecRef()
}
}()
if err := f.ConfigureMMap(ctx, &mopts); err != nil {
ctx.Infof("File is not memory-mappable: %v", err)
return err
}
if _, err := m.MMap(ctx, mopts); err != nil {
ctx.Infof("Error mapping PT_LOAD segment %+v at %#x: %v", phdr, addr, err)
return err
}
// We need to clear the end of the last page that exceeds fileSize so
// we don't map part of the file beyond fileSize.
//
// Note that Linux *does not* clear the portion of the first page
// before phdr.Off.
if mapSize > fileSize {
zeroAddr, ok := addr.AddLength(fileSize)
if !ok {
panic(fmt.Sprintf("successfully mmaped address overflows? %#x + %#x", addr, fileSize))
}
zeroSize := int64(mapSize - fileSize)
if zeroSize < 0 {
panic(fmt.Sprintf("zeroSize too big? %#x", uint64(zeroSize)))
}
if _, err := m.ZeroOut(ctx, zeroAddr, zeroSize, usermem.IOOpts{IgnorePermissions: true}); err != nil {
ctx.Warningf("Failed to zero end of page [%#x, %#x): %v", zeroAddr, zeroAddr+usermem.Addr(zeroSize), err)
return err
}
}
}
memSize := phdr.Memsz + adjust
if memSize < phdr.Memsz {
ctx.Infof("Computed segment mem size overflows: %#x + %#x", phdr.Memsz, adjust)
return syserror.ENOEXEC
}
// Allocate more anonymous pages if necessary.
if mapSize < memSize {
anonAddr, ok := addr.AddLength(mapSize)
if !ok {
panic(fmt.Sprintf("anonymous memory doesn't fit in pre-sized range? %#x + %#x", addr, mapSize))
}
anonSize, ok := usermem.Addr(memSize - mapSize).RoundUp()
if !ok {
ctx.Infof("extra anon pages too large: %#x", memSize-mapSize)
return syserror.ENOEXEC
}
// N.B. Linux uses vm_brk_flags to map these pages, which only
// honors the X bit, always mapping at least RW. ignoring These
// pages are not included in the final brk region.
prot := usermem.ReadWrite
if phdr.Flags&elf.PF_X == elf.PF_X {
prot.Execute = true
}
if _, err := m.MMap(ctx, memmap.MMapOpts{
Length: uint64(anonSize),
Addr: anonAddr,
// Fixed without Unmap will fail the mmap if something is
// already at addr.
Fixed: true,
Private: true,
Perms: prot,
MaxPerms: usermem.AnyAccess,
}); err != nil {
ctx.Infof("Error mapping PT_LOAD segment %v anonymous memory: %v", phdr, err)
return err
}
}
return nil
}
// loadedELF describes an ELF that has been successfully loaded.
type loadedELF struct {
// os is the target OS of the ELF.
os abi.OS
// arch is the target architecture of the ELF.
arch arch.Arch
// entry is the entry point of the ELF.
entry usermem.Addr
// start is the end of the ELF.
start usermem.Addr
// end is the end of the ELF.
end usermem.Addr
// interpter is the path to the ELF interpreter.
interpreter string
// phdrAddr is the address of the ELF program headers.
phdrAddr usermem.Addr
// phdrSize is the size of a single program header in the ELF.
phdrSize int
// phdrNum is the number of program headers.
phdrNum int
// auxv contains a subset of ELF-specific auxiliary vector entries:
// * AT_PHDR
// * AT_PHENT
// * AT_PHNUM
// * AT_BASE
// * AT_ENTRY
auxv arch.Auxv
}
// loadParsedELF loads f into mm.
//
// info is the parsed elfInfo from the header.
//
// It does not load the ELF interpreter, or return any auxv entries.
//
// Preconditions:
// * f is an ELF file
func loadParsedELF(ctx context.Context, m *mm.MemoryManager, f *fs.File, info elfInfo, sharedLoadOffset usermem.Addr) (loadedELF, error) {
first := true
var start, end usermem.Addr
var interpreter string
for _, phdr := range info.phdrs {
switch phdr.Type {
case elf.PT_LOAD:
vaddr := usermem.Addr(phdr.Vaddr)
if first {
first = false
start = vaddr
}
if vaddr < end {
ctx.Infof("PT_LOAD headers out-of-order. %#x < %#x", vaddr, end)
return loadedELF{}, syserror.ENOEXEC
}
var ok bool
end, ok = vaddr.AddLength(phdr.Memsz)
if !ok {
ctx.Infof("PT_LOAD header size overflows. %#x + %#x", vaddr, phdr.Memsz)
return loadedELF{}, syserror.ENOEXEC
}
case elf.PT_INTERP:
if phdr.Filesz < 2 {
ctx.Infof("PT_INTERP path too small: %v", phdr.Filesz)
return loadedELF{}, syserror.ENOEXEC
}
if phdr.Filesz > linux.PATH_MAX {
ctx.Infof("PT_INTERP path too big: %v", phdr.Filesz)
return loadedELF{}, syserror.ENOEXEC
}
path := make([]byte, phdr.Filesz)
_, err := readFull(ctx, f, usermem.BytesIOSequence(path), int64(phdr.Off))
if err != nil {
// If an interpreter was specified, it should exist.
ctx.Infof("Error reading PT_INTERP path: %v", err)
return loadedELF{}, syserror.ENOEXEC
}
if path[len(path)-1] != 0 {
ctx.Infof("PT_INTERP path not NUL-terminated: %v", path)
return loadedELF{}, syserror.ENOEXEC
}
// Strip NUL-terminator and everything beyond from
// string. Note that there may be a NUL-terminator
// before len(path)-1.
interpreter = string(path[:bytes.IndexByte(path, '\x00')])
if interpreter == "" {
// Linux actually attempts to open_exec("\0").
// open_exec -> do_open_execat fails to check
// that name != '\0' before calling
// do_filp_open, which thus opens the working
// directory. do_open_execat returns EACCES
// because the directory is not a regular file.
//
// We bypass that nonsense and simply
// short-circuit with EACCES. Those this does
// mean that there may be some edge cases where
// the open path would return a different
// error.
ctx.Infof("PT_INTERP path is empty: %v", path)
return loadedELF{}, syserror.EACCES
}
}
}
// Shared objects don't have fixed load addresses. We need to pick a
// base address big enough to fit all segments, so we first create a
// mapping for the total size just to find a region that is big enough.
//
// It is safe to unmap it immediately without racing with another mapping
// because we are the only one in control of the MemoryManager.
//
// Note that the vaddr of the first PT_LOAD segment is ignored when
// choosing the load address (even if it is non-zero). The vaddr does
// become an offset from that load address.
var offset usermem.Addr
if info.sharedObject {
totalSize := end - start
totalSize, ok := totalSize.RoundUp()
if !ok {
ctx.Infof("ELF PT_LOAD segments too big")
return loadedELF{}, syserror.ENOEXEC
}
var err error
offset, err = m.MMap(ctx, memmap.MMapOpts{
Length: uint64(totalSize),
Addr: sharedLoadOffset,
Private: true,
})
if err != nil {
ctx.Infof("Error allocating address space for shared object: %v", err)
return loadedELF{}, err
}
if err := m.MUnmap(ctx, offset, uint64(totalSize)); err != nil {
panic(fmt.Sprintf("Failed to unmap base address: %v", err))
}
start, ok = start.AddLength(uint64(offset))
if !ok {
panic(fmt.Sprintf("Start %#x + offset %#x overflows?", start, offset))
}
end, ok = end.AddLength(uint64(offset))
if !ok {
panic(fmt.Sprintf("End %#x + offset %#x overflows?", end, offset))
}
info.entry, ok = info.entry.AddLength(uint64(offset))
if !ok {
ctx.Infof("Entrypoint %#x + offset %#x overflows? Is the entrypoint within a segment?", info.entry, offset)
return loadedELF{}, err
}
}
// Map PT_LOAD segments.
for _, phdr := range info.phdrs {
switch phdr.Type {
case elf.PT_LOAD:
if phdr.Memsz == 0 {
// No need to load segments with size 0, but
// they exist in some binaries.
continue
}
if err := mapSegment(ctx, m, f, &phdr, offset); err != nil {
ctx.Infof("Failed to map PT_LOAD segment: %+v", phdr)
return loadedELF{}, err
}
}
}
// This assumes that the first segment contains the ELF headers. This
// may not be true in a malformed ELF, but Linux makes the same
// assumption.
phdrAddr, ok := start.AddLength(info.phdrOff)
if !ok {
ctx.Warningf("ELF start address %#x + phdr offset %#x overflows", start, info.phdrOff)
phdrAddr = 0
}
return loadedELF{
os: info.os,
arch: info.arch,
entry: info.entry,
start: start,
end: end,
interpreter: interpreter,
phdrAddr: phdrAddr,
phdrSize: info.phdrSize,
phdrNum: len(info.phdrs),
}, nil
}
// loadInitialELF loads f into mm.
//
// It creates an arch.Context for the ELF and prepares the mm for this arch.
//
// It does not load the ELF interpreter, or return any auxv entries.
//
// Preconditions:
// * f is an ELF file
// * f is the first ELF loaded into m
func loadInitialELF(ctx context.Context, m *mm.MemoryManager, fs *cpuid.FeatureSet, f *fs.File) (loadedELF, arch.Context, error) {
info, err := parseHeader(ctx, f)
if err != nil {
ctx.Infof("Failed to parse initial ELF: %v", err)
return loadedELF{}, nil, err
}
// Check Image Compatibility.
if arch.Host != info.arch {
ctx.Warningf("Found mismatch for platform %s with ELF type %s", arch.Host.String(), info.arch.String())
return loadedELF{}, nil, syserror.ENOEXEC
}
// Create the arch.Context now so we can prepare the mmap layout before
// mapping anything.
ac := arch.New(info.arch, fs)
l, err := m.SetMmapLayout(ac, limits.FromContext(ctx))
if err != nil {
ctx.Warningf("Failed to set mmap layout: %v", err)
return loadedELF{}, nil, err
}
// PIELoadAddress tries to move the ELF out of the way of the default
// mmap base to ensure that the initial brk has sufficient space to
// grow.
le, err := loadParsedELF(ctx, m, f, info, ac.PIELoadAddress(l))
return le, ac, err
}
// loadInterpreterELF loads f into mm.
//
// The interpreter must be for the same OS/Arch as the initial ELF.
//
// It does not return any auxv entries.
//
// Preconditions:
// * f is an ELF file
func loadInterpreterELF(ctx context.Context, m *mm.MemoryManager, f *fs.File, initial loadedELF) (loadedELF, error) {
info, err := parseHeader(ctx, f)
if err != nil {
if err == syserror.ENOEXEC {
// Bad interpreter.
err = syserror.ELIBBAD
}
return loadedELF{}, err
}
if info.os != initial.os {
ctx.Infof("Initial ELF OS %v and interpreter ELF OS %v differ", initial.os, info.os)
return loadedELF{}, syserror.ELIBBAD
}
if info.arch != initial.arch {
ctx.Infof("Initial ELF arch %v and interpreter ELF arch %v differ", initial.arch, info.arch)
return loadedELF{}, syserror.ELIBBAD
}
// The interpreter is not given a load offset, as its location does not
// affect brk.
return loadParsedELF(ctx, m, f, info, 0)
}
// loadELF loads args.File into the Task address space.
//
// If loadELF returns ErrSwitchFile it should be called again with the returned
// path and argv.
//
// Preconditions:
// * args.File is an ELF file
func loadELF(ctx context.Context, args LoadArgs) (loadedELF, arch.Context, error) {
bin, ac, err := loadInitialELF(ctx, args.MemoryManager, args.Features, args.File)
if err != nil {
ctx.Infof("Error loading binary: %v", err)
return loadedELF{}, nil, err
}
var interp loadedELF
if bin.interpreter != "" {
// Even if we do not allow the final link of the script to be
// resolved, the interpreter should still be resolved if it is
// a symlink.
args.ResolveFinal = true
// Refresh the traversal limit.
*args.RemainingTraversals = linux.MaxSymlinkTraversals
args.Filename = bin.interpreter
d, i, err := openPath(ctx, args)
if err != nil {
ctx.Infof("Error opening interpreter %s: %v", bin.interpreter, err)
return loadedELF{}, nil, err
}
defer i.DecRef()
// We don't need the Dirent.
d.DecRef()
interp, err = loadInterpreterELF(ctx, args.MemoryManager, i, bin)
if err != nil {
ctx.Infof("Error loading interpreter: %v", err)
return loadedELF{}, nil, err
}
if interp.interpreter != "" {
// No recursive interpreters!
ctx.Infof("Interpreter requires an interpreter")
return loadedELF{}, nil, syserror.ENOEXEC
}
}
// ELF-specific auxv entries.
bin.auxv = arch.Auxv{
arch.AuxEntry{linux.AT_PHDR, bin.phdrAddr},
arch.AuxEntry{linux.AT_PHENT, usermem.Addr(bin.phdrSize)},
arch.AuxEntry{linux.AT_PHNUM, usermem.Addr(bin.phdrNum)},
arch.AuxEntry{linux.AT_ENTRY, bin.entry},
}
if bin.interpreter != "" {
bin.auxv = append(bin.auxv, arch.AuxEntry{linux.AT_BASE, interp.start})
// Start in the interpreter.
// N.B. AT_ENTRY above contains the *original* entry point.
bin.entry = interp.entry
} else {
// Always add AT_BASE even if there is no interpreter.
bin.auxv = append(bin.auxv, arch.AuxEntry{linux.AT_BASE, 0})
}
return bin, ac, nil
}