Commit c1086315 authored by Michael Lippautz's avatar Michael Lippautz Committed by Commit Bot

cppgc: Conservative stack scanning

Adds support for conservative stack scanning on x64. The trampolines
saving callee-saved registers are compiled using clang (non-Windows)
and MASM (Windows). This is using the default toolchain for assembly
in Chromium/V8.

This differs from Oilpan in Chromium where x86 and x64 are compiled
using NASM [1]. V8 does not yet require this dependency and building
the trampolines natively avoids it. (NASM also requires separate
blocks for x64 Windows and non-Windows.) On non-x86/x64 platforms
Chromium also uses clang, so there's little benefit in keeping the
dependency.

The trampolines are tested when building with clang.

Other platforms follow in separate CLs.

[1] https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/platform/heap/asm/SaveRegisters_x86.asm

Change-Id: Ice2e23e44391aa94147abe75ee0b5afac458b8f8
Bug: chromium:1056170
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2124319
Commit-Queue: Michael Lippautz <mlippautz@chromium.org>
Reviewed-by: 's avatarOmer Katz <omerkatz@chromium.org>
Reviewed-by: 's avatarHannes Payer <hpayer@chromium.org>
Reviewed-by: 's avatarUlan Degenbaev <ulan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#66913}
parent 35c21ba3
......@@ -3946,8 +3946,19 @@ v8_source_set("cppgc_base") {
"src/heap/cppgc/heap.cc",
"src/heap/cppgc/heap.h",
"src/heap/cppgc/platform.cc",
"src/heap/cppgc/sanitizers.h",
"src/heap/cppgc/stack.cc",
"src/heap/cppgc/stack.h",
]
if (target_cpu == "x64") {
if (is_win) {
sources += [ "src/heap/cppgc/asm/x64/push_registers_win.S" ]
} else {
sources += [ "src/heap/cppgc/asm/x64/push_registers.S" ]
}
}
configs = [ ":internal_config" ]
public_deps = [ ":v8_libbase" ]
......
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
.att_syntax
.text
#ifdef V8_TARGET_OS_MACOSX
.globl _PushAllRegistersAndIterateStack
_PushAllRegistersAndIterateStack:
#else // !V8_TARGET_OS_MACOSX
.type PushAllRegistersAndIterateStack, %function
.global PushAllRegistersAndIterateStack
.hidden PushAllRegistersAndIterateStack
PushAllRegistersAndIterateStack:
#endif // !V8_TARGET_OS_MACOSX
// Push all callee-saved registers to get them on the stack for conservative
// stack scanning.
//
// We maintain 16-byte alignment at calls. There is an 8-byte return address
// on the stack and we push 56 bytes which maintains 16-byte stack alignment
// at the call.
// Source: https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf
push $0xCDCDCD // Dummy for alignment.
push %rbx
push %rbp
push %r12
push %r13
push %r14
push %r15
// Pass 1st parameter (rdi) unchanged (this).
// Pass 2nd parameter (rsi) unchanged (StackVisitor*).
// Save 3rd parameter (rdx; callback)
mov %rdx, %r8
// Pass 3rd parameter as rsp (stack pointer).
mov %rsp, %rdx
// Call the callback.
call *%r8
// Pop the callee-saved registers. None of them were modified so no
// restoring is needed.
add $56, %rsp
ret
;; Copyright 2020 the V8 project authors. All rights reserved.
;; Use of this source code is governed by a BSD-style license that can be
;; found in the LICENSE file.
;; MASM syntax
;; https://docs.microsoft.com/en-us/cpp/assembler/masm/microsoft-macro-assembler-reference?view=vs-2019
public PushAllRegistersAndIterateStack
.code
PushAllRegistersAndIterateStack:
;; Push all callee-saved registers to get them on the stack for conservative
;; stack scanning.
;;
;; We maintain 16-byte alignment at calls. There is an 8-byte return address
;; on the stack and we push 72 bytes which maintains 16-byte stack alignment
;; at the call.
;; Source: https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention
push 0CDCDCDh ;; Dummy for alignment.
push rsi
push rdi
push rbx
push rbp
push r12
push r13
push r14
push r15
;; Pass 1st parameter (rcx) unchanged (this).
;; Pass 2nd parameter (rdx) unchanged (StackVisitor*).
;; Save 3rd parameter (r8; callback)
mov r9, r8
;; Pass 3rd parameter as rsp (stack pointer).
mov r8, rsp
;; Call the callback.
call r9
;; Pop the callee-saved registers. None of them were modified so no
;; restoring is needed.
add rsp, 72
ret
end
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef V8_HEAP_CPPGC_SANITIZERS_H_
#define V8_HEAP_CPPGC_SANITIZERS_H_
#include "src/base/macros.h"
//
// TODO(chromium:1056170): Find a place in base for sanitizer support.
//
#ifdef V8_USE_ADDRESS_SANITIZER
#include <sanitizer/asan_interface.h>
#define NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))
#else // !V8_USE_ADDRESS_SANITIZER
#define NO_SANITIZE_ADDRESS
#endif // V8_USE_ADDRESS_SANITIZER
#ifdef V8_USE_MEMORY_SANITIZER
#include <sanitizer/msan_interface.h>
#define MSAN_UNPOISON(addr, size) __msan_unpoison(addr, size)
#else // !V8_USE_MEMORY_SANITIZER
#define MSAN_UNPOISON(addr, size) ((void)(addr), (void)(size))
#endif // V8_USE_MEMORY_SANITIZER
#endif // V8_HEAP_CPPGC_SANITIZERS_H_
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/heap/cppgc/stack.h"
#include <limits>
#include "src/base/platform/platform.h"
#include "src/heap/cppgc/globals.h"
#include "src/heap/cppgc/sanitizers.h"
namespace cppgc {
namespace internal {
using IterateStackCallback = void (Stack::*)(StackVisitor*, intptr_t*) const;
extern "C" void PushAllRegistersAndIterateStack(const Stack*, StackVisitor*,
IterateStackCallback);
Stack::Stack(const void* stack_start) : stack_start_(stack_start) {}
bool Stack::IsOnStack(void* slot) const {
void* raw_slot = v8::base::Stack::GetStackSlot(slot);
return v8::base::Stack::GetCurrentStackPosition() <= raw_slot &&
raw_slot <= stack_start_;
}
namespace {
#ifdef V8_USE_ADDRESS_SANITIZER
// No ASAN support as accessing fake frames otherwise results in
// "stack-use-after-scope" warnings.
NO_SANITIZE_ADDRESS
void IterateAsanFakeFrameIfNecessary(StackVisitor* visitor,
void* asan_fake_stack,
const void* stack_start,
const void* stack_end, void* address) {
// When using ASAN fake stack a pointer to the fake frame is kept on the
// native frame. In case |addr| points to a fake frame of the current stack
// iterate the fake frame. Frame layout see
// https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterReturn
if (asan_fake_stack) {
void* fake_frame_begin;
void* fake_frame_end;
void* real_stack_frame = __asan_addr_is_in_fake_stack(
asan_fake_stack, address, &fake_frame_begin, &fake_frame_end);
if (real_stack_frame) {
// |address| points to a fake frame. Check that the fake frame is part
// of this stack.
if (stack_start >= real_stack_frame && real_stack_frame >= stack_end) {
// Iterate the fake frame.
for (void** current = reinterpret_cast<void**>(fake_frame_begin);
current < fake_frame_end; ++current) {
void* addr = *current;
if (addr == nullptr) continue;
visitor->VisitPointer(addr);
}
}
}
}
}
#endif // V8_USE_ADDRESS_SANITIZER
#ifdef V8_TARGET_ARCH_X64
void IterateSafeStackIfNecessary(StackVisitor* visitor) {
#if defined(__has_feature)
#if __has_feature(safe_stack)
// Source:
// https://github.com/llvm/llvm-project/blob/master/compiler-rt/lib/safestack/safestack.cpp
constexpr size_t kSafeStackAlignmentBytes = 16;
void* stack_end = __builtin___get_unsafe_stack_ptr();
void* stack_start = __builtin___get_unsafe_stack_top();
CHECK_GT(stack_start, stack_end);
CHECK_EQ(0u, reinterpret_cast<uintptr_t>(stack_end) &
(kSafeStackAlignmentBytes - 1));
CHECK_EQ(0u, reinterpret_cast<uintptr_t>(stack_start) &
(kSafeStackAlignmentBytes - 1));
void** current = reinterpret_cast<void**>(stack_end);
for (; current < stack_start; ++current) {
void* address = *current;
if (address == nullptr) continue;
visitor->VisitPointer(address);
}
#endif // __has_feature(safe_stack)
#endif // defined(__has_feature)
}
#endif // V8_TARGET_ARCH_X64
} // namespace
#ifdef V8_TARGET_ARCH_X64
void Stack::IteratePointers(StackVisitor* visitor) const {
PushAllRegistersAndIterateStack(this, visitor, &Stack::IteratePointersImpl);
// No need to deal with callee-saved registers as they will be kept alive by
// the regular conservative stack iteration.
IterateSafeStackIfNecessary(visitor);
}
#endif // V8_TARGET_ARCH_X64
// No ASAN support as method accesses redzones while walking the stack.
NO_SANITIZE_ADDRESS
void Stack::IteratePointersImpl(StackVisitor* visitor,
intptr_t* stack_end) const {
#ifdef V8_USE_ADDRESS_SANITIZER
void* asan_fake_stack = __asan_get_current_fake_stack();
#endif // V8_USE_ADDRESS_SANITIZER
// All supported platforms should have their stack aligned to at least
// sizeof(void*).
constexpr size_t kMinStackAlignment = sizeof(void*);
// Redzone should not contain any pointers as the iteration is always called
// from the assembly trampoline. If inline assembly is ever inlined through
// LTO this may become necessary.
constexpr size_t kRedZoneBytes = 128;
void** current = reinterpret_cast<void**>(
reinterpret_cast<uintptr_t>(stack_end - kRedZoneBytes));
CHECK_EQ(0u, reinterpret_cast<uintptr_t>(current) & (kMinStackAlignment - 1));
for (; current < stack_start_; ++current) {
// MSAN: Instead of unpoisoning the whole stack, the slot's value is copied
// into a local which is unpoisoned.
void* address = *current;
MSAN_UNPOISON(address, sizeof(address));
if (address == nullptr) continue;
visitor->VisitPointer(address);
#ifdef V8_USE_ADDRESS_SANITIZER
IterateAsanFakeFrameIfNecessary(visitor, asan_fake_stack, stack_start_,
stack_end, address);
#endif // V8_USE_ADDRESS_SANITIZER
}
}
} // namespace internal
} // namespace cppgc
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef V8_HEAP_CPPGC_STACK_H_
#define V8_HEAP_CPPGC_STACK_H_
#include "src/base/macros.h"
namespace cppgc {
namespace internal {
class StackVisitor {
public:
virtual void VisitPointer(const void* address) = 0;
};
// Abstraction over the stack. Supports handling of:
// - native stack;
// - ASAN/MSAN;
// - SafeStack: https://releases.llvm.org/10.0.0/tools/clang/docs/SafeStack.html
class V8_EXPORT_PRIVATE Stack final {
public:
explicit Stack(const void* stack_start);
// Returns true if |slot| is part of the stack and false otherwise.
bool IsOnStack(void* slot) const;
// Word-aligned iteration of the stack. Slot values are passed on to
// |visitor|.
//
// TODO(chromium:1056170): Implement all platforms.
#ifdef V8_TARGET_ARCH_X64
void IteratePointers(StackVisitor* visitor) const;
#endif // V8_TARGET_ARCH_X64
private:
void IteratePointersImpl(StackVisitor* visitor, intptr_t* stack_end) const;
const void* stack_start_;
};
} // namespace internal
} // namespace cppgc
#endif // V8_HEAP_CPPGC_STACK_H_
......@@ -49,6 +49,7 @@ v8_source_set("cppgc_unittests_sources") {
"heap/cppgc/garbage-collected_unittest.cc",
"heap/cppgc/gc-info_unittest.cc",
"heap/cppgc/heap-object-header_unittest.cc",
"heap/cppgc/stack_unittest.cc",
"heap/cppgc/tests.cc",
"heap/cppgc/tests.h",
]
......
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/heap/cppgc/stack.h"
#include <memory>
#include <ostream>
#include "include/v8config.h"
#include "src/base/platform/platform.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace cppgc {
namespace internal {
namespace {
class GCStackTest : public ::testing::Test {
protected:
void SetUp() override {
stack_.reset(new Stack(v8::base::Stack::GetStackStart()));
}
void TearDown() override { stack_.reset(); }
Stack* GetStack() const { return stack_.get(); }
private:
std::unique_ptr<Stack> stack_;
};
} // namespace
TEST_F(GCStackTest, IsOnStackForStackValue) {
void* dummy;
EXPECT_TRUE(GetStack()->IsOnStack(&dummy));
}
TEST_F(GCStackTest, IsOnStackForHeapValue) {
auto dummy = std::make_unique<int>();
EXPECT_FALSE(GetStack()->IsOnStack(dummy.get()));
}
// The following test uses inline assembly and has been checked to work on clang
// to verify that the stack-scanning trampoline pushes callee-saved registers.
//
// The test uses a macro loop as asm() can only be passed string literals.
//
// TODO(chromium:1056170): Add more platforms as backends are implemented.
#ifdef __clang__
#ifdef V8_TARGET_ARCH_X64
// All of x64 support conservative stack scanning.
#define CONSERVATIVE_STACK_SCAN_SUPPORTED 1
#ifdef V8_OS_WIN
// Excluded from test: rbp
#define FOR_ALL_CALLEE_SAVED_REGS(V) \
V("rdi") \
V("rsi") \
V("rbx") \
V("r12") \
V("r13") \
V("r14") \
V("r15")
#else // !V8_OS_WIN
// Excluded from test: rbp
#define FOR_ALL_CALLEE_SAVED_REGS(V) \
V("rbx") \
V("r12") \
V("r13") \
V("r14") \
V("r15")
#endif // !V8_OS_WIN
#endif // V8_TARGET_ARCH_X64
#endif // __clang__
#ifdef CONSERVATIVE_STACK_SCAN_SUPPORTED
namespace {
class StackScanner final : public StackVisitor {
public:
struct Container {
std::unique_ptr<int> value;
};
StackScanner() : container_(new Container{}) {
container_->value = std::make_unique<int>();
}
void VisitPointer(const void* address) final {
if (address == container_->value.get()) found_ = true;
}
void Reset() { found_ = false; }
bool found() const { return found_; }
int* needle() const { return container_->value.get(); }
private:
std::unique_ptr<Container> container_;
bool found_ = false;
};
} // namespace
TEST_F(GCStackTest, IteratePointersFindsOnStackValue) {
auto scanner = std::make_unique<StackScanner>();
// No check that the needle is initially not found as on some platforms it
// may be part of the redzone or temporaries after setting it up throuhg
// StackScanner.
{
int* volatile tmp = scanner->needle();
USE(tmp);
GetStack()->IteratePointers(scanner.get());
EXPECT_TRUE(scanner->found());
}
}
TEST_F(GCStackTest, IteratePointersFindsOnStackValuePotentiallyUnaligned) {
auto scanner = std::make_unique<StackScanner>();
// No check that the needle is initially not found as on some platforms it
// may be part of the redzone or temporaries after setting it up throuhg
// StackScanner.
{
char a = 'c';
USE(a);
int* volatile tmp = scanner->needle();
USE(tmp);
GetStack()->IteratePointers(scanner.get());
EXPECT_TRUE(scanner->found());
}
}
namespace {
void RecursivelyPassOnParameter(int* volatile p1, int* volatile p2,
int* volatile p3, int* volatile p4,
int* volatile p5, int* volatile p6,
int* volatile p7, int* volatile p8,
Stack* stack, StackVisitor* visitor) {
if (p1) {
RecursivelyPassOnParameter(nullptr, p1, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, stack, visitor);
} else if (p2) {
RecursivelyPassOnParameter(nullptr, nullptr, p2, nullptr, nullptr, nullptr,
nullptr, nullptr, stack, visitor);
} else if (p3) {
RecursivelyPassOnParameter(nullptr, nullptr, nullptr, p3, nullptr, nullptr,
nullptr, nullptr, stack, visitor);
} else if (p4) {
RecursivelyPassOnParameter(nullptr, nullptr, nullptr, nullptr, p4, nullptr,
nullptr, nullptr, stack, visitor);
} else if (p5) {
RecursivelyPassOnParameter(nullptr, nullptr, nullptr, nullptr, nullptr, p5,
nullptr, nullptr, stack, visitor);
} else if (p6) {
RecursivelyPassOnParameter(nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, p6, nullptr, stack, visitor);
} else if (p7) {
RecursivelyPassOnParameter(nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, p7, stack, visitor);
} else if (p8) {
stack->IteratePointers(visitor);
}
}
} // namespace
TEST_F(GCStackTest, IteratePointersFindsParameter) {
auto scanner = std::make_unique<StackScanner>();
// No check that the needle is initially not found as on some platforms it
// may be part of the redzone or temporaries after setting it up throuhg
// StackScanner.
RecursivelyPassOnParameter(nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, scanner->needle(), GetStack(),
scanner.get());
EXPECT_TRUE(scanner->found());
}
TEST_F(GCStackTest, IteratePointersFindsParameterInNestedFunction) {
auto scanner = std::make_unique<StackScanner>();
// No check that the needle is initially not found as on some platforms it
// may be part of the redzone or temporaries after setting it up throuhg
// StackScanner.
RecursivelyPassOnParameter(scanner->needle(), nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, GetStack(),
scanner.get());
EXPECT_TRUE(scanner->found());
}
TEST_F(GCStackTest, IteratePointersFindsCalleeSavedRegisters) {
auto scanner = std::make_unique<StackScanner>();
// No check that the needle is initially not found as on some platforms it
// may be part of the redzone or temporaries after setting it up throuhg
// StackScanner.
// First, clear all callee-saved registers.
#define CLEAR_REGISTER(reg) asm("mov $0, %%" reg : : : reg);
FOR_ALL_CALLEE_SAVED_REGS(CLEAR_REGISTER)
#undef CLEAR_REGISTER
// Keep local raw pointers to keep instruction sequences small below.
auto* local_stack = GetStack();
auto* local_scanner = scanner.get();
// Moves |local_scanner->needle()| into a callee-saved register, leaving the
// callee-saved register as the only register referencing the needle.
// (Ignoring implementation-dependent dirty registers/stack.)
#define KEEP_ALIVE_FROM_CALLEE_SAVED(reg) \
local_scanner->Reset(); \
/* This moves the temporary into the calee-saved register. */ \
asm("mov %0, %%" reg : : "r"(local_scanner->needle()) : reg); \
/* Register is unprotected from here till the actual invocation. */ \
local_stack->IteratePointers(local_scanner); \
EXPECT_TRUE(local_scanner->found()) \
<< "pointer in callee-saved register not found. register: " << reg \
<< std::endl; \
/* Clear out the register again */ \
asm("mov $0, %%" reg : : : reg);
FOR_ALL_CALLEE_SAVED_REGS(KEEP_ALIVE_FROM_CALLEE_SAVED)
#undef KEEP_ALIVE_FROM_CALLEE_SAVED
#undef FOR_ALL_CALLEE_SAVED_REGS
}
#endif // CONSERVATIVE_STACK_SCAN_SUPPORTED
} // namespace internal
} // namespace cppgc
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment