ios: Add forbidden allocator to integration tests.

Override malloc_default_zone and malloc_default_purgeable_zone
with allocators that exit when called from the signal or Mach exception
threads in XCUITests, to verify the allocator is not used by the
InProcessHandler. Check stderr for error messages to confirm failures.

Change-Id: I1bb92e57504d71bbf6c6eaad3571c814e8a6934c
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/3488826
Reviewed-by: Joshua Peraza <jperaza@chromium.org>
Commit-Queue: Justin Cohen <justincohen@chromium.org>
This commit is contained in:
Justin Cohen 2022-03-08 20:09:09 -05:00 committed by Crashpad LUCI CQ
parent 3c4e37178d
commit 12b35ebde8
10 changed files with 413 additions and 18 deletions

View File

@ -567,6 +567,9 @@ class CrashpadClient {
//! \brief Inject a callback into Mach handling. Intended to be used by
//! tests to trigger a reentrant exception.
static void SetMachExceptionCallbackForTesting(void (*callback)());
//! \brief Returns the thread id of the Mach exception thread, used by tests.
static uint64_t GetThreadIdForTesting();
#endif
#if BUILDFLAG(IS_APPLE) || DOXYGEN

View File

@ -153,6 +153,8 @@ class CrashHandler : public Thread,
in_process_handler_.SetMachExceptionCallbackForTesting(callback);
}
uint64_t GetThreadIdForTesting() { return Thread::GetThreadIdForTesting(); }
private:
CrashHandler() = default;
@ -423,4 +425,10 @@ void CrashpadClient::SetMachExceptionCallbackForTesting(void (*callback)()) {
crash_handler->SetMachExceptionCallbackForTesting(callback);
}
uint64_t CrashpadClient::GetThreadIdForTesting() {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
return crash_handler->GetThreadIdForTesting();
}
} // namespace crashpad

View File

@ -103,6 +103,9 @@
NSNumber* report_exception;
XCTAssertTrue([rootObject_ pendingReportException:&report_exception]);
XCTAssertEqual(report_exception.unsignedIntValue, exception);
NSString* stderrContents = [rootObject_ stderrContents];
XCTAssertFalse([stderrContents containsString:@"allocator used in handler."]);
}
- (void)testEDO {
@ -322,8 +325,6 @@
}
- (void)testCrashInHandlerReentrant {
app_.launchArguments = @[ @"--redirect-stderr-to-file" ];
[app_ launch];
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
rootObject_ = [EDOClientService rootObjectWithPort:12345];
@ -344,4 +345,24 @@
XCTAssertTrue([stderrContents containsString:errmsg]);
}
- (void)testFailureWhenHandlerAllocates {
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
rootObject_ = [EDOClientService rootObjectWithPort:12345];
[rootObject_ allocateWithForbiddenAllocators];
// Confirm the app is not running.
XCTAssertTrue([app_ waitForState:XCUIApplicationStateNotRunning timeout:15]);
XCTAssertTrue(app_.state == XCUIApplicationStateNotRunning);
[app_ launch];
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
rootObject_ = [EDOClientService rootObjectWithPort:12345];
XCTAssertEqual([rootObject_ pendingReportCount], 0);
NSString* stderrContents = [rootObject_ stderrContents];
XCTAssertTrue([stderrContents containsString:@"allocator used in handler."]);
}
@end

View File

@ -35,6 +35,8 @@ static_library("app_host_sources") {
"cptest_application_delegate.mm",
"cptest_crash_view_controller.h",
"cptest_crash_view_controller.mm",
"handler_forbidden_allocators.cc",
"handler_forbidden_allocators.h",
"main.mm",
]
configs += [ "../../..:crashpad_config" ]

View File

@ -42,6 +42,7 @@
#include "test/file.h"
#import "test/ios/host/cptest_crash_view_controller.h"
#import "test/ios/host/cptest_shared_object.h"
#import "test/ios/host/handler_forbidden_allocators.h"
#include "util/file/filesystem.h"
#include "util/thread/thread.h"
@ -113,6 +114,7 @@ GetProcessSnapshotMinidumpFromSinglePending() {
@interface CPTestApplicationDelegate ()
- (void)processIntermediateDumps;
@property(copy, nonatomic) NSString* last_stderr_output;
@end
@implementation CPTestApplicationDelegate {
@ -135,12 +137,14 @@ GetProcessSnapshotMinidumpFromSinglePending() {
{"crashpad", "no"}};
}
if ([arguments containsObject:@"--redirect-stderr-to-file"]) {
CHECK(freopen(GetStderrOutputFile().value().c_str(), "a", stderr) !=
nullptr);
} else {
crashpad::test::RemoveFileIfExists(GetStderrOutputFile());
}
NSString* path =
[NSString stringWithUTF8String:GetStderrOutputFile().value().c_str()];
self.last_stderr_output =
[[NSString alloc] initWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:NULL];
crashpad::test::RemoveFileIfExists(GetStderrOutputFile());
CHECK(freopen(GetStderrOutputFile().value().c_str(), "a", stderr) != nullptr);
if (client_.StartCrashpadInProcessHandler(
GetDatabaseDir(), "", annotations)) {
@ -270,14 +274,17 @@ GetProcessSnapshotMinidumpFromSinglePending() {
}
- (void)crashKillAbort {
crashpad::test::ReplaceAllocatorsWithHandlerForbidden();
kill(getpid(), SIGABRT);
}
- (void)crashTrap {
crashpad::test::ReplaceAllocatorsWithHandlerForbidden();
__builtin_trap();
}
- (void)crashAbort {
crashpad::test::ReplaceAllocatorsWithHandlerForbidden();
abort();
}
@ -439,12 +446,15 @@ class CrashThread : public crashpad::Thread {
[self crashTrap];
}
- (void)allocateWithForbiddenAllocators {
crashpad::test::ReplaceAllocatorsWithHandlerForbidden();
(void)malloc(10);
}
- (NSString*)stderrContents {
NSString* path =
[NSString stringWithUTF8String:GetStderrOutputFile().value().c_str()];
return [[NSString alloc] initWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:NULL];
CPTestApplicationDelegate* delegate =
(CPTestApplicationDelegate*)UIApplication.sharedApplication.delegate;
return delegate.last_stderr_output;
}
@end

View File

@ -55,13 +55,16 @@
// Triggers an EXC_BAD_ACCESS exception and crash.
- (void)crashBadAccess;
// Triggers a crash with a call to kill(SIGABRT).
// Triggers a crash with a call to kill(SIGABRT). This crash runs with
// ReplaceAllocatorsWithHandlerForbidden.
- (void)crashKillAbort;
// Trigger a crash with a __builtin_trap.
// Trigger a crash with a __builtin_trap. This crash runs with
// ReplaceAllocatorsWithHandlerForbidden.
- (void)crashTrap;
// Trigger a crash with an abort().
// Trigger a crash with an abort(). This crash runs with
// ReplaceAllocatorsWithHandlerForbidden.
- (void)crashAbort;
// Trigger a crash with an uncaught exception.
@ -101,8 +104,12 @@
// exceptions.
- (void)crashInHandlerReentrant;
// Return the contents of the stderr file used when the host app is launch with
// the arguments --redirect-stderr-to-file
// Runs with ReplaceAllocatorsWithHandlerForbidden and allocates memory, testing
// that the handler forbidden allocator works.
- (void)allocateWithForbiddenAllocators;
// Return the contents of the stderr output from the previous run of the host
// application.
- (NSString*)stderrContents;
@end

View File

@ -0,0 +1,295 @@
// Copyright 2022 The Crashpad Authors. All rights reserved.
//
// 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.
#import "test/ios/host/handler_forbidden_allocators.h"
#include <CoreFoundation/CoreFoundation.h>
#include <malloc/malloc.h>
#include <pthread.h>
#include <limits>
#include "base/mac/mach_logging.h"
#include "client/crashpad_client.h"
#include "util/ios/raw_logging.h"
namespace crashpad {
namespace test {
namespace {
uint64_t g_main_thread = 0;
uint64_t g_mach_exception_thread = 0;
malloc_zone_t g_old_zone;
bool is_handler_thread() {
uint64_t thread_self;
pthread_threadid_np(pthread_self(), &thread_self);
return (thread_self == g_main_thread ||
thread_self == g_mach_exception_thread);
}
void* handler_forbidden_malloc(struct _malloc_zone_t* zone, size_t size) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG("handler_forbidden_malloc allocator used in handler.");
exit(EXIT_FAILURE);
}
return g_old_zone.malloc(zone, size);
}
void* handler_forbidden_calloc(struct _malloc_zone_t* zone,
size_t num_items,
size_t size) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG("handler_forbidden_calloc allocator used in handler.");
exit(EXIT_FAILURE);
}
return g_old_zone.calloc(zone, num_items, size);
}
void* handler_forbidden_valloc(struct _malloc_zone_t* zone, size_t size) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG("handler_forbidden_valloc allocator used in handler.");
exit(EXIT_FAILURE);
}
return g_old_zone.valloc(zone, size);
}
void handler_forbidden_free(struct _malloc_zone_t* zone, void* ptr) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG("handler_forbidden_free allocator used in handler.");
exit(EXIT_FAILURE);
}
g_old_zone.free(zone, ptr);
}
void* handler_forbidden_realloc(struct _malloc_zone_t* zone,
void* ptr,
size_t size) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG("handler_forbidden_realloc allocator used in handler.");
exit(EXIT_FAILURE);
}
return g_old_zone.realloc(zone, ptr, size);
}
void handler_forbidden_destroy(struct _malloc_zone_t* zone) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG("handler_forbidden_destroy allocator used in handler.");
exit(EXIT_FAILURE);
}
g_old_zone.destroy(zone);
}
void* handler_forbidden_memalign(struct _malloc_zone_t* zone,
size_t alignment,
size_t size) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG("handler_forbidden_memalign allocator used in handler.");
exit(EXIT_FAILURE);
}
return g_old_zone.memalign(zone, alignment, size);
}
unsigned handler_forbidden_batch_malloc(struct _malloc_zone_t* zone,
size_t size,
void** results,
unsigned num_requested) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG(
"handler_forbidden_batch_malloc allocator used in handler.");
exit(EXIT_FAILURE);
}
return g_old_zone.batch_malloc(zone, size, results, num_requested);
}
void handler_forbidden_batch_free(struct _malloc_zone_t* zone,
void** to_be_freed,
unsigned num_to_be_freed) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG("handler_forbidden_batch_free allocator used in handler.");
exit(EXIT_FAILURE);
}
g_old_zone.batch_free(zone, to_be_freed, num_to_be_freed);
}
void handler_forbidden_free_definite_size(struct _malloc_zone_t* zone,
void* ptr,
size_t size) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG(
"handler_forbidden_free_definite_size allocator used in handler.");
exit(EXIT_FAILURE);
}
g_old_zone.free_definite_size(zone, ptr, size);
}
size_t handler_forbidden_pressure_relief(struct _malloc_zone_t* zone,
size_t goal) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG(
"handler_forbidden_pressure_relief allocator used in handler.");
exit(EXIT_FAILURE);
}
return g_old_zone.pressure_relief(zone, goal);
}
boolean_t handler_forbidden_claimed_address(struct _malloc_zone_t* zone,
void* ptr) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG(
"handler_forbidden_claimed_address allocator used in handler.");
exit(EXIT_FAILURE);
}
return g_old_zone.claimed_address(zone, ptr);
}
size_t handler_forbidden_size(struct _malloc_zone_t* zone, const void* ptr) {
if (is_handler_thread()) {
CRASHPAD_RAW_LOG("handler_forbidden_size allocator used in handler.");
exit(EXIT_FAILURE);
}
return g_old_zone.size(zone, ptr);
}
bool DeprotectMallocZone(malloc_zone_t* default_zone,
vm_address_t* reprotection_start,
vm_size_t* reprotection_length,
vm_prot_t* reprotection_value) {
mach_port_t unused;
*reprotection_start = reinterpret_cast<vm_address_t>(default_zone);
struct vm_region_basic_info_64 info;
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
kern_return_t result = vm_region_64(mach_task_self(),
reprotection_start,
reprotection_length,
VM_REGION_BASIC_INFO_64,
reinterpret_cast<vm_region_info_t>(&info),
&count,
&unused);
if (result != KERN_SUCCESS) {
MACH_LOG(ERROR, result) << "vm_region_64";
return false;
}
// The kernel always returns a null object for VM_REGION_BASIC_INFO_64, but
// balance it with a deallocate in case this ever changes. See
// the VM_REGION_BASIC_INFO_64 case in vm_map_region() in 10.15's
// https://opensource.apple.com/source/xnu/xnu-6153.11.26/osfmk/vm/vm_map.c .
mach_port_deallocate(mach_task_self(), unused);
if (!(info.max_protection & VM_PROT_WRITE)) {
LOG(ERROR) << "Invalid max_protection " << info.max_protection;
return false;
}
// Does the region fully enclose the zone pointers? Possibly unwarranted
// simplification used: using the size of a full version 10 malloc zone rather
// than the actual smaller size if the passed-in zone is not version 10.
DCHECK_LE(*reprotection_start, reinterpret_cast<vm_address_t>(default_zone));
vm_size_t zone_offset = reinterpret_cast<vm_address_t>(default_zone) -
reinterpret_cast<vm_address_t>(*reprotection_start);
DCHECK_LE(zone_offset + sizeof(malloc_zone_t), *reprotection_length);
if (info.protection & VM_PROT_WRITE) {
// No change needed; the zone is already writable.
*reprotection_start = 0;
*reprotection_length = 0;
*reprotection_value = VM_PROT_NONE;
} else {
*reprotection_value = info.protection;
result = vm_protect(mach_task_self(),
*reprotection_start,
*reprotection_length,
false,
info.protection | VM_PROT_WRITE);
if (result != KERN_SUCCESS) {
MACH_LOG(ERROR, result) << "vm_protect";
return false;
}
}
return true;
}
void ReplaceZoneFunctions(malloc_zone_t* zone, const malloc_zone_t* functions) {
// Remove protection.
vm_address_t reprotection_start = 0;
vm_size_t reprotection_length = 0;
vm_prot_t reprotection_value = VM_PROT_NONE;
bool success = DeprotectMallocZone(
zone, &reprotection_start, &reprotection_length, &reprotection_value);
if (!success) {
return;
}
zone->size = functions->size;
zone->malloc = functions->malloc;
zone->calloc = functions->calloc;
zone->valloc = functions->valloc;
zone->free = functions->free;
zone->realloc = functions->realloc;
zone->destroy = functions->destroy;
zone->batch_malloc = functions->batch_malloc;
zone->batch_free = functions->batch_free;
zone->introspect = functions->introspect;
zone->memalign = functions->memalign;
zone->free_definite_size = functions->free_definite_size;
zone->pressure_relief = functions->pressure_relief;
zone->claimed_address = functions->claimed_address;
// Restore protection if it was active.
if (reprotection_start) {
kern_return_t result = vm_protect(mach_task_self(),
reprotection_start,
reprotection_length,
false,
reprotection_value);
if (result != KERN_SUCCESS) {
MACH_LOG(ERROR, result) << "vm_protect";
return;
}
}
}
} // namespace
void ReplaceAllocatorsWithHandlerForbidden() {
pthread_threadid_np(pthread_self(), &g_main_thread);
CrashpadClient crashpad_client;
g_mach_exception_thread = crashpad_client.GetThreadIdForTesting();
malloc_zone_t* default_zone = malloc_default_zone();
memcpy(&g_old_zone, default_zone, sizeof(g_old_zone));
malloc_zone_t new_functions = {};
new_functions.size = handler_forbidden_size;
new_functions.malloc = handler_forbidden_malloc;
new_functions.calloc = handler_forbidden_calloc;
new_functions.valloc = handler_forbidden_valloc;
new_functions.free = handler_forbidden_free;
new_functions.realloc = handler_forbidden_realloc;
new_functions.destroy = handler_forbidden_destroy;
new_functions.batch_malloc = handler_forbidden_batch_malloc;
new_functions.batch_free = handler_forbidden_batch_free;
new_functions.memalign = handler_forbidden_memalign;
new_functions.free_definite_size = handler_forbidden_free_definite_size;
new_functions.pressure_relief = handler_forbidden_pressure_relief;
new_functions.claimed_address = handler_forbidden_claimed_address;
ReplaceZoneFunctions(default_zone, &new_functions);
malloc_zone_t* purgeable_zone = malloc_default_purgeable_zone();
ReplaceZoneFunctions(purgeable_zone, &new_functions);
}
} // namespace test
} // namespace crashpad

View File

@ -0,0 +1,31 @@
// Copyright 2022 The Crashpad Authors. All rights reserved.
//
// 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.
#ifndef CRASHPAD_TEST_IOS_HANDLER_FORBIDDEN_ALLOCATIONS_H_
#define CRASHPAD_TEST_IOS_HANDLER_FORBIDDEN_ALLOCATIONS_H_
namespace crashpad {
namespace test {
// Override malloc_default_zone and malloc_default_purgeable_zone with functions
// that immediately exit if called from the same thread that this helper is
// called from or from the Crashpad Mach exception handler thread indicated by
// GetThreadIdForTesting. This is used to ensure the allocator is not used by
// the Crashpad InProcessHandler.
void ReplaceAllocatorsWithHandlerForbidden();
} // namespace test
} // namespace crashpad
#endif // CRASHPAD_TEST_IOS_HANDLER_FORBIDDEN_ALLOCATIONS_H_

View File

@ -19,10 +19,13 @@
#if BUILDFLAG(IS_POSIX)
#include <pthread.h>
#include <stdint.h>
#elif BUILDFLAG(IS_WIN)
#include <windows.h>
#endif // BUILDFLAG(IS_POSIX)
#include "build/build_config.h"
namespace crashpad {
//! \brief Basic thread abstraction. Users should derive from this
@ -44,6 +47,11 @@ class Thread {
//! Must paired with a call to Start().
void Join();
#if BUILDFLAG(IS_APPLE)
//! \brief Returns the thread id of the Thread pthread_t.
uint64_t GetThreadIdForTesting();
#endif // BUILDFLAG(IS_APPLE)
private:
//! \brief The thread entry point to be implemented by the subclass.
virtual void ThreadMain() = 0;

View File

@ -19,6 +19,7 @@
#include <ostream>
#include "base/check.h"
#include "build/build_config.h"
namespace crashpad {
@ -35,6 +36,15 @@ void Thread::Join() {
platform_thread_ = 0;
}
#if BUILDFLAG(IS_APPLE)
uint64_t Thread::GetThreadIdForTesting() {
uint64_t thread_self;
errno = pthread_threadid_np(pthread_self(), &thread_self);
PCHECK(errno == 0) << "pthread_threadid_np";
return thread_self;
}
#endif // BUILDFLAG(IS_APPLE)
// static
void* Thread::ThreadEntryThunk(void* argument) {
Thread* self = reinterpret_cast<Thread*>(argument);