// 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. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "test/util/cleanup.h" #include "test/util/file_descriptor.h" #include "test/util/fs_util.h" #include "test/util/multiprocess_util.h" #include "test/util/posix_error.h" #include "test/util/proc_util.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" namespace gvisor { namespace testing { namespace { using ::testing::AnyOf; using ::testing::Eq; #ifndef __x86_64__ // The assembly stub and ELF internal details must be ported to other arches. #error "Test only supported on x86-64" #endif // __x86_64__ // amd64 stub that calls PTRACE_TRACEME and sends itself SIGSTOP. const char kPtraceCode[] = { // movq $101, %rax /* ptrace */ '\x48', '\xc7', '\xc0', '\x65', '\x00', '\x00', '\x00', // movq $0, %rsi /* PTRACE_TRACEME */ '\x48', '\xc7', '\xc6', '\x00', '\x00', '\x00', '\x00', // movq $0, %rdi '\x48', '\xc7', '\xc7', '\x00', '\x00', '\x00', '\x00', // movq $0, %rdx '\x48', '\xc7', '\xc2', '\x00', '\x00', '\x00', '\x00', // movq $0, %r10 '\x49', '\xc7', '\xc2', '\x00', '\x00', '\x00', '\x00', // syscall '\x0f', '\x05', // movq $39, %rax /* getpid */ '\x48', '\xc7', '\xc0', '\x27', '\x00', '\x00', '\x00', // syscall '\x0f', '\x05', // movq %rax, %rdi /* pid */ '\x48', '\x89', '\xc7', // movq $62, %rax /* kill */ '\x48', '\xc7', '\xc0', '\x3e', '\x00', '\x00', '\x00', // movq $19, %rsi /* SIGSTOP */ '\x48', '\xc7', '\xc6', '\x13', '\x00', '\x00', '\x00', // syscall '\x0f', '\x05', }; // Size of a syscall instruction. constexpr int kSyscallSize = 2; // This test suite tests executable loading in the kernel (ELF and interpreter // scripts). // Parameterized ELF types for 64 and 32 bit. template struct ElfTypes; template <> struct ElfTypes<64> { typedef Elf64_Ehdr ElfEhdr; typedef Elf64_Phdr ElfPhdr; }; template <> struct ElfTypes<32> { typedef Elf32_Ehdr ElfEhdr; typedef Elf32_Phdr ElfPhdr; }; template struct ElfBinary { using ElfEhdr = typename ElfTypes::ElfEhdr; using ElfPhdr = typename ElfTypes::ElfPhdr; ElfEhdr header = {}; std::vector phdrs; std::vector data; // UpdateOffsets updates p_offset, p_vaddr in all phdrs to account for the // space taken by the header and phdrs. // // It also updates header.e_phnum and adds the offset to header.e_entry to // account for the headers residing in the first PT_LOAD segment. // // Before calling UpdateOffsets each of those fields should be the appropriate // offset into data. void UpdateOffsets() { size_t offset = sizeof(header) + phdrs.size() * sizeof(ElfPhdr); header.e_entry += offset; header.e_phnum = phdrs.size(); for (auto& p : phdrs) { p.p_offset += offset; p.p_vaddr += offset; } } // AddInterpreter adds a PT_INTERP segment with the passed contents. // // A later call to UpdateOffsets is required to make the new phdr valid. void AddInterpreter(std::vector contents) { const int start = data.size(); data.insert(data.end(), contents.begin(), contents.end()); const int size = data.size() - start; ElfPhdr phdr = {}; phdr.p_type = PT_INTERP; phdr.p_offset = start; phdr.p_filesz = size; phdr.p_memsz = size; // "If [PT_INTERP] is present, it must precede any loadable segment entry." phdrs.insert(phdrs.begin(), phdr); } // Writes the header, phdrs, and data to fd. PosixError Write(int fd) const { int ret = WriteFd(fd, &header, sizeof(header)); if (ret < 0) { return PosixError(errno, "failed to write header"); } else if (ret != sizeof(header)) { return PosixError(EIO, absl::StrCat("short write of header: ", ret)); } for (auto const& p : phdrs) { ret = WriteFd(fd, &p, sizeof(p)); if (ret < 0) { return PosixError(errno, "failed to write phdr"); } else if (ret != sizeof(p)) { return PosixError(EIO, absl::StrCat("short write of phdr: ", ret)); } } ret = WriteFd(fd, data.data(), data.size()); if (ret < 0) { return PosixError(errno, "failed to write data"); } else if (ret != static_cast(data.size())) { return PosixError(EIO, absl::StrCat("short write of data: ", ret)); } return NoError(); } }; // Creates a new temporary executable ELF file in parent with elf as the // contents. template PosixErrorOr CreateElfWith(absl::string_view parent, ElfBinary const& elf) { ASSIGN_OR_RETURN_ERRNO( auto file, TempPath::CreateFileWith(parent, absl::string_view(), 0755)); ASSIGN_OR_RETURN_ERRNO(auto fd, Open(file.path(), O_RDWR)); RETURN_IF_ERRNO(elf.Write(fd.get())); return std::move(file); } // Creates a new temporary executable ELF file with elf as the contents. template PosixErrorOr CreateElfWith(ElfBinary const& elf) { return CreateElfWith(GetAbsoluteTestTmpdir(), elf); } // Wait for pid to stop, and assert that it stopped via SIGSTOP. PosixError WaitStopped(pid_t pid) { int status; int ret = RetryEINTR(waitpid)(pid, &status, 0); MaybeSave(); if (ret < 0) { return PosixError(errno, "wait failed"); } else if (ret != pid) { return PosixError(ESRCH, absl::StrCat("wait got ", ret, " want ", pid)); } if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { return PosixError(EINVAL, absl::StrCat("pid did not SIGSTOP; status = ", status)); } return NoError(); } // Returns a valid ELF that PTRACE_TRACEME and SIGSTOPs itself. // // UpdateOffsets must be called before writing this ELF. ElfBinary<64> StandardElf() { ElfBinary<64> elf; elf.header.e_ident[EI_MAG0] = ELFMAG0; elf.header.e_ident[EI_MAG1] = ELFMAG1; elf.header.e_ident[EI_MAG2] = ELFMAG2; elf.header.e_ident[EI_MAG3] = ELFMAG3; elf.header.e_ident[EI_CLASS] = ELFCLASS64; elf.header.e_ident[EI_DATA] = ELFDATA2LSB; elf.header.e_ident[EI_VERSION] = EV_CURRENT; elf.header.e_type = ET_EXEC; elf.header.e_machine = EM_X86_64; elf.header.e_version = EV_CURRENT; elf.header.e_phoff = sizeof(elf.header); elf.header.e_phentsize = sizeof(decltype(elf)::ElfPhdr); // TODO(gvisor.dev/issue/153): Always include a PT_GNU_STACK segment to // disable executable stacks. With this omitted the stack (and all PROT_READ) // mappings should be executable, but gVisor doesn't support that. decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_GNU_STACK; phdr.p_flags = PF_R | PF_W; elf.phdrs.push_back(phdr); phdr = {}; phdr.p_type = PT_LOAD; phdr.p_flags = PF_R | PF_X; phdr.p_offset = 0; phdr.p_vaddr = 0x40000; phdr.p_filesz = sizeof(kPtraceCode); phdr.p_memsz = phdr.p_filesz; elf.phdrs.push_back(phdr); elf.header.e_entry = phdr.p_vaddr; elf.data.assign(kPtraceCode, kPtraceCode + sizeof(kPtraceCode)); return elf; } // Test that a trivial binary executes. TEST(ElfTest, Execute) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); // Ensure it made it to SIGSTOP. ASSERT_NO_ERRNO(WaitStopped(child)); struct user_regs_struct regs; ASSERT_THAT(ptrace(PTRACE_GETREGS, child, 0, ®s), SyscallSucceeds()); // RIP is just beyond the final syscall instruction. EXPECT_EQ(regs.rip, elf.header.e_entry + sizeof(kPtraceCode)); EXPECT_THAT(child, ContainsMappings(std::vector({ {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, file.path().c_str()}, }))); } // StandardElf without data completes execve, but faults once running. TEST(ElfTest, MissingText) { ElfBinary<64> elf = StandardElf(); elf.data.clear(); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); int status; ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceedsWithValue(child)); // It runs off the end of the zeroes filling the end of the page. EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV) << status; } // Typical ELF with a data + bss segment TEST(ElfTest, DataSegment) { ElfBinary<64> elf = StandardElf(); // Create a standard ELF, but extend to 1.5 pages. The second page will be the // beginning of a multi-page data + bss segment. elf.data.resize(kPageSize + kPageSize / 2); decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_LOAD; phdr.p_flags = PF_R | PF_W; phdr.p_offset = kPageSize; phdr.p_vaddr = 0x41000; phdr.p_filesz = kPageSize / 2; // The header is going to push vaddr up by a few hundred bytes. Keep p_memsz a // bit less than 2 pages so this mapping doesn't extend beyond 0x43000. phdr.p_memsz = 2 * kPageSize - kPageSize / 2; elf.phdrs.push_back(phdr); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); EXPECT_THAT( child, ContainsMappings(std::vector({ // text page. {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, file.path().c_str()}, // data + bss page from file. {0x41000, 0x42000, true, true, false, true, kPageSize, 0, 0, 0, file.path().c_str()}, // bss page from anon. {0x42000, 0x43000, true, true, false, true, 0, 0, 0, 0, ""}, }))); } // Additonal pages beyond filesz honor (only) execute protections. // // N.B. Linux changed this in 4.11 (16e72e9b30986 "powerpc: do not make the // entire heap executable"). Previously, extra pages were always RW. TEST(ElfTest, ExtraMemPages) { // gVisor has the newer behavior. if (!IsRunningOnGvisor()) { auto version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion()); SKIP_IF(version.major < 4 || (version.major == 4 && version.minor < 11)); } ElfBinary<64> elf = StandardElf(); // Create a standard ELF, but extend to 1.5 pages. The second page will be the // beginning of a multi-page data + bss segment. elf.data.resize(kPageSize + kPageSize / 2); decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_LOAD; // RWX segment. The extra anon page will also be RWX. // // N.B. Linux uses clear_user to clear the end of the file-mapped page, which // respects the mapping protections. Thus if we map this RO with memsz > // (unaligned) filesz, then execve will fail with EFAULT. See padzero(elf_bss) // in fs/binfmt_elf.c:load_elf_binary. // // N.N.B.B. The above only applies to the last segment. For earlier segments, // the clear_user error is ignored. phdr.p_flags = PF_R | PF_W | PF_X; phdr.p_offset = kPageSize; phdr.p_vaddr = 0x41000; phdr.p_filesz = kPageSize / 2; // The header is going to push vaddr up by a few hundred bytes. Keep p_memsz a // bit less than 2 pages so this mapping doesn't extend beyond 0x43000. phdr.p_memsz = 2 * kPageSize - kPageSize / 2; elf.phdrs.push_back(phdr); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); EXPECT_THAT(child, ContainsMappings(std::vector({ // text page. {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, file.path().c_str()}, // data + bss page from file. {0x41000, 0x42000, true, true, true, true, kPageSize, 0, 0, 0, file.path().c_str()}, // extra page from anon. {0x42000, 0x43000, true, true, true, true, 0, 0, 0, 0, ""}, }))); } // An aligned segment with filesz == 0, memsz > 0 is anon-only. TEST(ElfTest, AnonOnlySegment) { ElfBinary<64> elf = StandardElf(); decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_LOAD; // RO segment. The extra anon page will be RW anyways. phdr.p_flags = PF_R; phdr.p_offset = 0; phdr.p_vaddr = 0x41000; phdr.p_filesz = 0; phdr.p_memsz = kPageSize; elf.phdrs.push_back(phdr); elf.UpdateOffsets(); // UpdateOffsets adjusts p_vaddr and p_offset by the header size, but we need // a page-aligned p_vaddr to get a truly anon-only page. elf.phdrs[2].p_vaddr = 0x41000; // N.B. p_offset is now unaligned, but Linux doesn't care since this is // anon-only. TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); EXPECT_THAT(child, ContainsMappings(std::vector({ // text page. {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, file.path().c_str()}, // anon page. {0x41000, 0x42000, true, true, false, true, 0, 0, 0, 0, ""}, }))); } // p_offset must have the same alignment as p_vaddr. TEST(ElfTest, UnalignedOffset) { ElfBinary<64> elf = StandardElf(); // Unaligned offset. elf.phdrs[1].p_offset += 1; elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); // execve(2) return EINVAL, but behavior varies between Linux and gVisor. // // On Linux, the new mm is committed before attempting to map into it. By the // time we hit EINVAL in the segment mmap, the old mm is gone. Linux returns // to an empty mm, which immediately segfaults. // // OTOH, gVisor maps into the new mm before committing it. Thus when it hits // failure, the caller is still intact to receive the error. if (IsRunningOnGvisor()) { ASSERT_EQ(execve_errno, EINVAL); } else { ASSERT_EQ(execve_errno, 0); int status; ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceedsWithValue(child)); EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV) << status; } } // Linux will allow PT_LOAD segments to overlap. TEST(ElfTest, DirectlyOverlappingSegments) { // NOTE(b/37289926): see PIEOutOfOrderSegments. SKIP_IF(IsRunningOnGvisor()); ElfBinary<64> elf = StandardElf(); // Same as the StandardElf mapping. decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_LOAD; // Add PF_W so we can differentiate this mapping from the first. phdr.p_flags = PF_R | PF_W | PF_X; phdr.p_offset = 0; phdr.p_vaddr = 0x40000; phdr.p_filesz = sizeof(kPtraceCode); phdr.p_memsz = phdr.p_filesz; elf.phdrs.push_back(phdr); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); EXPECT_THAT(child, ContainsMappings(std::vector({ {0x40000, 0x41000, true, true, true, true, 0, 0, 0, 0, file.path().c_str()}, }))); } // Linux allows out-of-order PT_LOAD segments. TEST(ElfTest, OutOfOrderSegments) { // NOTE(b/37289926): see PIEOutOfOrderSegments. SKIP_IF(IsRunningOnGvisor()); ElfBinary<64> elf = StandardElf(); decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_LOAD; phdr.p_flags = PF_R | PF_X; phdr.p_offset = 0; phdr.p_vaddr = 0x20000; phdr.p_filesz = sizeof(kPtraceCode); phdr.p_memsz = phdr.p_filesz; elf.phdrs.push_back(phdr); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); EXPECT_THAT(child, ContainsMappings(std::vector({ {0x20000, 0x21000, true, false, true, true, 0, 0, 0, 0, file.path().c_str()}, {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, file.path().c_str()}, }))); } // header.e_phoff is bound the end of the file. TEST(ElfTest, OutOfBoundsPhdrs) { ElfBinary<64> elf = StandardElf(); elf.header.e_phoff = 0x100000; elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); // On Linux 3.11, this caused EIO. On newer Linux, it causes ENOEXEC. EXPECT_THAT(execve_errno, AnyOf(Eq(ENOEXEC), Eq(EIO))); } // Claim there is a phdr beyond the end of the file, but don't include it. TEST(ElfTest, MissingPhdr) { ElfBinary<64> elf = StandardElf(); // Clear data so the file ends immediately after the phdrs. // N.B. Per ElfTest.MissingData, StandardElf without data completes execve // without error. elf.data.clear(); elf.UpdateOffsets(); // Claim that there is another phdr just beyond the end of the file. Of // course, it isn't accessible. elf.header.e_phnum++; TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); // On Linux 3.11, this caused EIO. On newer Linux, it causes ENOEXEC. EXPECT_THAT(execve_errno, AnyOf(Eq(ENOEXEC), Eq(EIO))); } // No headers at all, just the ELF magic. TEST(ElfTest, MissingHeader) { TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileMode(0755)); FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR)); const char kElfMagic[] = {0x7f, 'E', 'L', 'F'}; ASSERT_THAT(WriteFd(fd.get(), &kElfMagic, sizeof(kElfMagic)), SyscallSucceeds()); fd.reset(); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); EXPECT_EQ(execve_errno, ENOEXEC); } // Load a PIE ELF with a data + bss segment. TEST(ElfTest, PIE) { ElfBinary<64> elf = StandardElf(); elf.header.e_type = ET_DYN; // Create a standard ELF, but extend to 1.5 pages. The second page will be the // beginning of a multi-page data + bss segment. elf.data.resize(kPageSize + kPageSize / 2); elf.header.e_entry = 0x0; decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_LOAD; phdr.p_flags = PF_R | PF_W; phdr.p_offset = kPageSize; // Put the data segment at a bit of an offset. phdr.p_vaddr = 0x20000; phdr.p_filesz = kPageSize / 2; // The header is going to push vaddr up by a few hundred bytes. Keep p_memsz a // bit less than 2 pages so this mapping doesn't extend beyond 0x43000. phdr.p_memsz = 2 * kPageSize - kPageSize / 2; elf.phdrs.push_back(phdr); elf.UpdateOffsets(); // The first segment really needs to start at 0 for a normal PIE binary, and // thus includes the headers. const uint64 offset = elf.phdrs[1].p_offset; elf.phdrs[1].p_offset = 0x0; elf.phdrs[1].p_vaddr = 0x0; elf.phdrs[1].p_filesz += offset; elf.phdrs[1].p_memsz += offset; TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); // RIP tells us which page the first segment was loaded into. struct user_regs_struct regs; ASSERT_THAT(ptrace(PTRACE_GETREGS, child, 0, ®s), SyscallSucceeds()); const uint64 load_addr = regs.rip & ~(kPageSize - 1); EXPECT_THAT(child, ContainsMappings(std::vector({ // text page. {load_addr, load_addr + 0x1000, true, false, true, true, 0, 0, 0, 0, file.path().c_str()}, // data + bss page from file. {load_addr + 0x20000, load_addr + 0x21000, true, true, false, true, kPageSize, 0, 0, 0, file.path().c_str()}, // bss page from anon. {load_addr + 0x21000, load_addr + 0x22000, true, true, false, true, 0, 0, 0, 0, ""}, }))); } // PIE binary with a non-zero start address. // // This is non-standard for a PIE binary, but valid. The binary is still loaded // at an arbitrary address, not the first PT_LOAD vaddr. // // N.B. Linux changed this behavior in d1fd836dcf00d2028c700c7e44d2c23404062c90. // Previously, with "randomization" enabled, PIE binaries with a non-zero start // address would be be loaded at the address they specified because mmap was // passed the load address, which wasn't 0 as expected. // // This change is present in kernel v4.1+. TEST(ElfTest, PIENonZeroStart) { // gVisor has the newer behavior. if (!IsRunningOnGvisor()) { auto version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion()); SKIP_IF(version.major < 4 || (version.major == 4 && version.minor < 1)); } ElfBinary<64> elf = StandardElf(); elf.header.e_type = ET_DYN; // Create a standard ELF, but extend to 1.5 pages. The second page will be the // beginning of a multi-page data + bss segment. elf.data.resize(kPageSize + kPageSize / 2); decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_LOAD; phdr.p_flags = PF_R | PF_W; phdr.p_offset = kPageSize; // Put the data segment at a bit of an offset. phdr.p_vaddr = 0x60000; phdr.p_filesz = kPageSize / 2; // The header is going to push vaddr up by a few hundred bytes. Keep p_memsz a // bit less than 2 pages so this mapping doesn't extend beyond 0x43000. phdr.p_memsz = 2 * kPageSize - kPageSize / 2; elf.phdrs.push_back(phdr); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); // RIP tells us which page the first segment was loaded into. struct user_regs_struct regs; ASSERT_THAT(ptrace(PTRACE_GETREGS, child, 0, ®s), SyscallSucceeds()); const uint64 load_addr = regs.rip & ~(kPageSize - 1); // The ELF is loaded at an arbitrary address, not the first PT_LOAD vaddr. // // N.B. this is technically flaky, but Linux is *extremely* unlikely to pick // this as the start address, as it searches from the top down. EXPECT_NE(load_addr, 0x40000); EXPECT_THAT(child, ContainsMappings(std::vector({ // text page. {load_addr, load_addr + 0x1000, true, false, true, true, 0, 0, 0, 0, file.path().c_str()}, // data + bss page from file. {load_addr + 0x20000, load_addr + 0x21000, true, true, false, true, kPageSize, 0, 0, 0, file.path().c_str()}, // bss page from anon. {load_addr + 0x21000, load_addr + 0x22000, true, true, false, true, 0, 0, 0, 0, ""}, }))); } TEST(ElfTest, PIEOutOfOrderSegments) { // TODO(b/37289926): This triggers a bug in Linux where it computes the size // of the binary as 0x20000 - 0x40000 = 0xfffffffffffe0000, which obviously // fails to map. // // We test gVisor's behavior (of rejecting the binary) because I assert that // Linux is wrong and needs to be fixed. SKIP_IF(!IsRunningOnGvisor()); ElfBinary<64> elf = StandardElf(); elf.header.e_type = ET_DYN; // Create a standard ELF, but extend to 1.5 pages. The second page will be the // beginning of a multi-page data + bss segment. elf.data.resize(kPageSize + kPageSize / 2); decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_LOAD; phdr.p_flags = PF_R | PF_W; phdr.p_offset = kPageSize; // Put the data segment *before* the first segment. phdr.p_vaddr = 0x20000; phdr.p_filesz = kPageSize / 2; // The header is going to push vaddr up by a few hundred bytes. Keep p_memsz a // bit less than 2 pages so this mapping doesn't extend beyond 0x43000. phdr.p_memsz = 2 * kPageSize - kPageSize / 2; elf.phdrs.push_back(phdr); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); EXPECT_EQ(execve_errno, ENOEXEC); } // Standard dynamically linked binary with an ELF interpreter. TEST(ElfTest, ELFInterpreter) { ElfBinary<64> interpreter = StandardElf(); interpreter.header.e_type = ET_DYN; interpreter.header.e_entry = 0x0; interpreter.UpdateOffsets(); // The first segment really needs to start at 0 for a normal PIE binary, and // thus includes the headers. uint64 const offset = interpreter.phdrs[1].p_offset; // N.B. Since Linux 4.10 (0036d1f7eb95b "binfmt_elf: fix calculations for bss // padding"), Linux unconditionally zeroes the remainder of the highest mapped // page in an interpreter, failing if the protections don't allow write. Thus // we must mark this writeable. interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.phdrs[1].p_offset = 0x0; interpreter.phdrs[1].p_vaddr = 0x0; interpreter.phdrs[1].p_filesz += offset; interpreter.phdrs[1].p_memsz += offset; TempPath interpreter_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(interpreter)); ElfBinary<64> binary = StandardElf(); // Append the interpreter path. int const interp_data_start = binary.data.size(); for (char const c : interpreter_file.path()) { binary.data.push_back(c); } // NUL-terminate. binary.data.push_back(0); int const interp_data_size = binary.data.size() - interp_data_start; decltype(binary)::ElfPhdr phdr = {}; phdr.p_type = PT_INTERP; phdr.p_offset = interp_data_start; phdr.p_filesz = interp_data_size; phdr.p_memsz = interp_data_size; // "If [PT_INTERP] is present, it must precede any loadable segment entry." // // However, Linux allows it anywhere, so we just stick it at the end to make // sure out-of-order PT_INTERP is OK. binary.phdrs.push_back(phdr); binary.UpdateOffsets(); TempPath binary_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(binary)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(ForkAndExec( binary_file.path(), {binary_file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); // RIP tells us which page the first segment of the interpreter was loaded // into. struct user_regs_struct regs; ASSERT_THAT(ptrace(PTRACE_GETREGS, child, 0, ®s), SyscallSucceeds()); const uint64 interp_load_addr = regs.rip & ~(kPageSize - 1); EXPECT_THAT( child, ContainsMappings(std::vector({ // Main binary {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, binary_file.path().c_str()}, // Interpreter {interp_load_addr, interp_load_addr + 0x1000, true, true, true, true, 0, 0, 0, 0, interpreter_file.path().c_str()}, }))); } // Test parameter to ElfInterpterStaticTest cases. The first item is a suffix to // add to the end of the interpreter path in the PT_INTERP segment and the // second is the expected execve(2) errno. using ElfInterpreterStaticParam = std::tuple, int>; class ElfInterpreterStaticTest : public ::testing::TestWithParam {}; // Statically linked ELF with a statically linked ELF interpreter. TEST_P(ElfInterpreterStaticTest, Test) { const std::vector segment_suffix = std::get<0>(GetParam()); const int expected_errno = std::get<1>(GetParam()); ElfBinary<64> interpreter = StandardElf(); // See comment in ElfTest.ELFInterpreter. interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.UpdateOffsets(); TempPath interpreter_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(interpreter)); ElfBinary<64> binary = StandardElf(); // The PT_LOAD segment conflicts with the interpreter's PT_LOAD segment. The // interpreter's will be mapped directly over the binary's. // Interpreter path plus the parameterized suffix in the PT_INTERP segment. const std::string path = interpreter_file.path(); std::vector segment(path.begin(), path.end()); segment.insert(segment.end(), segment_suffix.begin(), segment_suffix.end()); binary.AddInterpreter(segment); binary.UpdateOffsets(); TempPath binary_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(binary)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(ForkAndExec( binary_file.path(), {binary_file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, expected_errno); if (expected_errno == 0) { ASSERT_NO_ERRNO(WaitStopped(child)); EXPECT_THAT(child, ContainsMappings(std::vector({ // Interpreter. {0x40000, 0x41000, true, true, true, true, 0, 0, 0, 0, interpreter_file.path().c_str()}, }))); } } INSTANTIATE_TEST_SUITE_P( Cases, ElfInterpreterStaticTest, ::testing::ValuesIn({ // Simple NUL-terminator to run the interpreter as normal. std::make_tuple(std::vector({'\0'}), 0), // Add some garbage to the segment followed by a NUL-terminator. This is // ignored. std::make_tuple(std::vector({'\0', 'b', '\0'}), 0), // Add some garbage to the segment without a NUL-terminator. Linux will // reject // this. std::make_tuple(std::vector({'\0', 'b'}), ENOEXEC), })); // Test parameter to ElfInterpterBadPathTest cases. The first item is the // contents of the PT_INTERP segment and the second is the expected execve(2) // errno. using ElfInterpreterBadPathParam = std::tuple, int>; class ElfInterpreterBadPathTest : public ::testing::TestWithParam {}; TEST_P(ElfInterpreterBadPathTest, Test) { const std::vector segment = std::get<0>(GetParam()); const int expected_errno = std::get<1>(GetParam()); ElfBinary<64> binary = StandardElf(); binary.AddInterpreter(segment); binary.UpdateOffsets(); TempPath binary_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(binary)); int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(ForkAndExec( binary_file.path(), {binary_file.path()}, {}, nullptr, &execve_errno)); EXPECT_EQ(execve_errno, expected_errno); } INSTANTIATE_TEST_SUITE_P( Cases, ElfInterpreterBadPathTest, ::testing::ValuesIn({ // NUL-terminated fake path in the PT_INTERP segment. std::make_tuple(std::vector({'/', 'f', '/', 'b', '\0'}), ENOENT), // ELF interpreter not NUL-terminated. std::make_tuple(std::vector({'/', 'f', '/', 'b'}), ENOEXEC), // ELF interpreter path omitted entirely. // // fs/binfmt_elf.c:load_elf_binary returns ENOEXEC if p_filesz is < 2 // bytes. std::make_tuple(std::vector({'\0'}), ENOEXEC), // ELF interpreter path = "\0". // // fs/binfmt_elf.c:load_elf_binary returns ENOEXEC if p_filesz is < 2 // bytes, so add an extra byte to pass that check. // // load_elf_binary -> 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. std::make_tuple(std::vector({'\0', '\0'}), EACCES), })); // Relative path to ELF interpreter. TEST(ElfTest, ELFInterpreterRelative) { ElfBinary<64> interpreter = StandardElf(); interpreter.header.e_type = ET_DYN; interpreter.header.e_entry = 0x0; interpreter.UpdateOffsets(); // The first segment really needs to start at 0 for a normal PIE binary, and // thus includes the headers. uint64 const offset = interpreter.phdrs[1].p_offset; // See comment in ElfTest.ELFInterpreter. interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.phdrs[1].p_offset = 0x0; interpreter.phdrs[1].p_vaddr = 0x0; interpreter.phdrs[1].p_filesz += offset; interpreter.phdrs[1].p_memsz += offset; TempPath interpreter_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(interpreter)); auto cwd = ASSERT_NO_ERRNO_AND_VALUE(GetCWD()); auto interpreter_relative = ASSERT_NO_ERRNO_AND_VALUE(GetRelativePath(cwd, interpreter_file.path())); ElfBinary<64> binary = StandardElf(); // NUL-terminated path in the PT_INTERP segment. std::vector segment(interpreter_relative.begin(), interpreter_relative.end()); segment.push_back(0); binary.AddInterpreter(segment); binary.UpdateOffsets(); TempPath binary_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(binary)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(ForkAndExec( binary_file.path(), {binary_file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); // RIP tells us which page the first segment of the interpreter was loaded // into. struct user_regs_struct regs; ASSERT_THAT(ptrace(PTRACE_GETREGS, child, 0, ®s), SyscallSucceeds()); const uint64 interp_load_addr = regs.rip & ~(kPageSize - 1); EXPECT_THAT( child, ContainsMappings(std::vector({ // Main binary {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, binary_file.path().c_str()}, // Interpreter {interp_load_addr, interp_load_addr + 0x1000, true, true, true, true, 0, 0, 0, 0, interpreter_file.path().c_str()}, }))); } // ELF interpreter architecture doesn't match the binary. TEST(ElfTest, ELFInterpreterWrongArch) { ElfBinary<64> interpreter = StandardElf(); interpreter.header.e_machine = EM_PPC64; interpreter.header.e_type = ET_DYN; interpreter.header.e_entry = 0x0; interpreter.UpdateOffsets(); // The first segment really needs to start at 0 for a normal PIE binary, and // thus includes the headers. uint64 const offset = interpreter.phdrs[1].p_offset; // See comment in ElfTest.ELFInterpreter. interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.phdrs[1].p_offset = 0x0; interpreter.phdrs[1].p_vaddr = 0x0; interpreter.phdrs[1].p_filesz += offset; interpreter.phdrs[1].p_memsz += offset; TempPath interpreter_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(interpreter)); ElfBinary<64> binary = StandardElf(); // NUL-terminated path in the PT_INTERP segment. const std::string path = interpreter_file.path(); std::vector segment(path.begin(), path.end()); segment.push_back(0); binary.AddInterpreter(segment); binary.UpdateOffsets(); TempPath binary_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(binary)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(ForkAndExec( binary_file.path(), {binary_file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, ELIBBAD); } // No execute permissions on the binary. TEST(ElfTest, NoExecute) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); ASSERT_THAT(chmod(file.path().c_str(), 0644), SyscallSucceeds()); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); EXPECT_EQ(execve_errno, EACCES); } // Execute, but no read permissions on the binary works just fine. TEST(ElfTest, NoRead) { // TODO(gvisor.dev/issue/160): gVisor's backing filesystem may prevent the // sentry from reading the executable. SKIP_IF(IsRunningOnGvisor()); ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); ASSERT_THAT(chmod(file.path().c_str(), 0111), SyscallSucceeds()); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); ASSERT_NO_ERRNO(WaitStopped(child)); // TODO(gvisor.dev/issue/160): A task with a non-readable executable is marked // non-dumpable, preventing access to proc files. gVisor does not implement // this behavior. } // No execute permissions on the ELF interpreter. TEST(ElfTest, ElfInterpreterNoExecute) { ElfBinary<64> interpreter = StandardElf(); interpreter.header.e_type = ET_DYN; interpreter.header.e_entry = 0x0; interpreter.UpdateOffsets(); // The first segment really needs to start at 0 for a normal PIE binary, and // thus includes the headers. uint64 const offset = interpreter.phdrs[1].p_offset; // See comment in ElfTest.ELFInterpreter. interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.phdrs[1].p_offset = 0x0; interpreter.phdrs[1].p_vaddr = 0x0; interpreter.phdrs[1].p_filesz += offset; interpreter.phdrs[1].p_memsz += offset; TempPath interpreter_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(interpreter)); ElfBinary<64> binary = StandardElf(); // NUL-terminated path in the PT_INTERP segment. const std::string path = interpreter_file.path(); std::vector segment(path.begin(), path.end()); segment.push_back(0); binary.AddInterpreter(segment); binary.UpdateOffsets(); TempPath binary_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(binary)); ASSERT_THAT(chmod(interpreter_file.path().c_str(), 0644), SyscallSucceeds()); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(interpreter_file.path(), {interpreter_file.path()}, {}, &child, &execve_errno)); EXPECT_EQ(execve_errno, EACCES); } // Execute a basic interpreter script. TEST(InterpreterScriptTest, Execute) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); // Use /tmp explicitly to ensure the path is short enough. TempPath binary = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith("/tmp", elf)); TempPath script = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( GetAbsoluteTestTmpdir(), absl::StrCat("#!", binary.path()), 0755)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(script.path(), {script.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); EXPECT_NO_ERRNO(WaitStopped(child)); } // Whitespace after #!. TEST(InterpreterScriptTest, Whitespace) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); // Use /tmp explicitly to ensure the path is short enough. TempPath binary = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith("/tmp", elf)); TempPath script = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( GetAbsoluteTestTmpdir(), absl::StrCat("#! \t \t", binary.path()), 0755)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(script.path(), {script.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); EXPECT_NO_ERRNO(WaitStopped(child)); } // Interpreter script is missing execute permission. TEST(InterpreterScriptTest, InterpreterScriptNoExecute) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); // Use /tmp explicitly to ensure the path is short enough. TempPath binary = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith("/tmp", elf)); TempPath script = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( GetAbsoluteTestTmpdir(), absl::StrCat("#!", binary.path()), 0644)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(script.path(), {script.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, EACCES); } // Binary interpreter script refers to is missing execute permission. TEST(InterpreterScriptTest, BinaryNoExecute) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); // Use /tmp explicitly to ensure the path is short enough. TempPath binary = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith("/tmp", elf)); ASSERT_THAT(chmod(binary.path().c_str(), 0644), SyscallSucceeds()); TempPath script = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( GetAbsoluteTestTmpdir(), absl::StrCat("#!", binary.path()), 0755)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(script.path(), {script.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, EACCES); } // Linux will load interpreter scripts five levels deep, but no more. TEST(InterpreterScriptTest, MaxRecursion) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); // Use /tmp explicitly to ensure the path is short enough. TempPath binary = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith("/tmp", elf)); TempPath script1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( "/tmp", absl::StrCat("#!", binary.path()), 0755)); TempPath script2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( "/tmp", absl::StrCat("#!", script1.path()), 0755)); TempPath script3 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( "/tmp", absl::StrCat("#!", script2.path()), 0755)); TempPath script4 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( "/tmp", absl::StrCat("#!", script3.path()), 0755)); TempPath script5 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( "/tmp", absl::StrCat("#!", script4.path()), 0755)); TempPath script6 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( "/tmp", absl::StrCat("#!", script5.path()), 0755)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(script6.path(), {script6.path()}, {}, &child, &execve_errno)); // Too many levels of recursion. EXPECT_EQ(execve_errno, ELOOP); // The next level up is OK. auto cleanup2 = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(script5.path(), {script5.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); EXPECT_NO_ERRNO(WaitStopped(child)); } // Interpreter script with a relative path. TEST(InterpreterScriptTest, RelativePath) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); TempPath binary = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith("/tmp", elf)); auto cwd = ASSERT_NO_ERRNO_AND_VALUE(GetCWD()); auto binary_relative = ASSERT_NO_ERRNO_AND_VALUE(GetRelativePath(cwd, binary.path())); TempPath script = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( GetAbsoluteTestTmpdir(), absl::StrCat("#!", binary_relative), 0755)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(script.path(), {script.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); EXPECT_NO_ERRNO(WaitStopped(child)); } // Interpreter script with .. in a path component. TEST(InterpreterScriptTest, UncleanPath) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); // Use /tmp explicitly to ensure the path is short enough. TempPath binary = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith("/tmp", elf)); TempPath script = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( GetAbsoluteTestTmpdir(), absl::StrCat("#!/tmp/../", binary.path()), 0755)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(script.path(), {script.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); EXPECT_NO_ERRNO(WaitStopped(child)); } // Passed interpreter script is a symlink. TEST(InterpreterScriptTest, Symlink) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); // Use /tmp explicitly to ensure the path is short enough. TempPath binary = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith("/tmp", elf)); TempPath script = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( GetAbsoluteTestTmpdir(), absl::StrCat("#!", binary.path()), 0755)); TempPath link = ASSERT_NO_ERRNO_AND_VALUE( TempPath::CreateSymlinkTo(GetAbsoluteTestTmpdir(), script.path())); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(link.path(), {link.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); EXPECT_NO_ERRNO(WaitStopped(child)); } // Interpreter script points to a symlink loop. TEST(InterpreterScriptTest, SymlinkLoop) { std::string const link1 = NewTempAbsPathInDir("/tmp"); std::string const link2 = NewTempAbsPathInDir("/tmp"); ASSERT_THAT(symlink(link2.c_str(), link1.c_str()), SyscallSucceeds()); auto remove_link1 = Cleanup( [&link1] { EXPECT_THAT(unlink(link1.c_str()), SyscallSucceeds()); }); ASSERT_THAT(symlink(link1.c_str(), link2.c_str()), SyscallSucceeds()); auto remove_link2 = Cleanup( [&link2] { EXPECT_THAT(unlink(link2.c_str()), SyscallSucceeds()); }); TempPath script = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( GetAbsoluteTestTmpdir(), absl::StrCat("#!", link1), 0755)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(script.path(), {script.path()}, {}, &child, &execve_errno)); EXPECT_EQ(execve_errno, ELOOP); } // Binary is a symlink loop. TEST(ExecveTest, SymlinkLoop) { std::string const link1 = NewTempAbsPathInDir("/tmp"); std::string const link2 = NewTempAbsPathInDir("/tmp"); ASSERT_THAT(symlink(link2.c_str(), link1.c_str()), SyscallSucceeds()); auto remove_link = Cleanup( [&link1] { EXPECT_THAT(unlink(link1.c_str()), SyscallSucceeds()); }); ASSERT_THAT(symlink(link1.c_str(), link2.c_str()), SyscallSucceeds()); auto remove_link2 = Cleanup( [&link2] { EXPECT_THAT(unlink(link2.c_str()), SyscallSucceeds()); }); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(link1, {link1}, {}, &child, &execve_errno)); EXPECT_EQ(execve_errno, ELOOP); } // Binary is a directory. TEST(ExecveTest, Directory) { pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec("/tmp", {"/tmp"}, {}, &child, &execve_errno)); EXPECT_EQ(execve_errno, EACCES); } // Pass a valid binary as a directory (extra / on the end). TEST(ExecveTest, BinaryAsDirectory) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); std::string const path = absl::StrCat(file.path(), "/"); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(path, {path}, {}, &child, &execve_errno)); EXPECT_EQ(execve_errno, ENOTDIR); } // The initial brk value is after the page at the end of the binary. TEST(ExecveTest, BrkAfterBinary) { ElfBinary<64> elf = StandardElf(); elf.UpdateOffsets(); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(elf)); pid_t child; int execve_errno; auto cleanup = ASSERT_NO_ERRNO_AND_VALUE( ForkAndExec(file.path(), {file.path()}, {}, &child, &execve_errno)); ASSERT_EQ(execve_errno, 0); // Ensure it made it to SIGSTOP. ASSERT_NO_ERRNO(WaitStopped(child)); struct user_regs_struct regs; ASSERT_THAT(ptrace(PTRACE_GETREGS, child, 0, ®s), SyscallSucceeds()); // RIP is just beyond the final syscall instruction. Rewind to execute a brk // syscall. regs.rip -= kSyscallSize; regs.rax = __NR_brk; regs.rdi = 0; ASSERT_THAT(ptrace(PTRACE_SETREGS, child, 0, ®s), SyscallSucceeds()); // Resume the child, waiting for syscall entry. ASSERT_THAT(ptrace(PTRACE_SYSCALL, child, 0, 0), SyscallSucceeds()); int status; ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceedsWithValue(child)); ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) << "status = " << status; // Execute the syscall. ASSERT_THAT(ptrace(PTRACE_SYSCALL, child, 0, 0), SyscallSucceeds()); ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceedsWithValue(child)); ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) << "status = " << status; ASSERT_THAT(ptrace(PTRACE_GETREGS, child, 0, ®s), SyscallSucceeds()); // brk is after the text page. // // The kernel does brk randomization, so we can't be sure what the exact // address will be, but it is always beyond the final page in the binary. // i.e., it does not start immediately after memsz in the middle of a page. // Userspace may expect to use that space. EXPECT_GE(regs.rax, 0x41000); } } // namespace } // namespace testing } // namespace gvisor