Skip to content

Commit

Permalink
Merge pull request #948 from pickware/svenmuennich/update-cwlprecondi…
Browse files Browse the repository at this point in the history
…tiontesting-for-apple-silicon

Add support for precondition testing on Apple Silicon
  • Loading branch information
ikesyo authored Dec 1, 2021
2 parents ea66249 + b03a829 commit 0bf627c
Show file tree
Hide file tree
Showing 13 changed files with 108 additions and 74 deletions.
2 changes: 1 addition & 1 deletion Cartfile.private
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "mattgallagher/CwlCatchException" ~> 2.0
github "mattgallagher/CwlPreconditionTesting" ~> 2.0.1
github "mattgallagher/CwlPreconditionTesting" ~> 2.1
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "mattgallagher/CwlCatchException" "2.0.0"
github "mattgallagher/CwlPreconditionTesting" "2.0.1"
github "mattgallagher/CwlPreconditionTesting" "2.1.0"
18 changes: 15 additions & 3 deletions Carthage/Checkouts/CwlPreconditionTesting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,25 @@ For an extended discussion of this code, please see the Cocoa with Love article:

## Requirements

From version 2.0.0-beta.1, building CwlPreconditionTesting requires Swift 5 or newer and the Swift Package Manager.
From version 2.0.0-beta.1, building CwlPreconditionTesting requires Swift 5 or newer and the Swift Package Manager, or CocoaPods.

For use with older versions of Swift or other package managers, [use version 1.2.0 or older](https://github.com/mattgallagher/CwlPreconditionTesting/tree/1.2.0).

## Adding to your project

### Swift Package Manager

Add the following to the `dependencies` array in your "Package.swift" file:

.package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", from: Version("2.0.0-beta.1"))
.package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", from: Version("2.0.0"))

Or by adding `https://github.com/mattgallagher/CwlPreconditionTesting.git`, version 2.0.0 or later, to the list of Swift packages for any project in Xcode.

### CocoaPods

Or by adding `https://github.com/mattgallagher/CwlPreconditionTesting.git`, version 2.0.0-beta.1 or later, to the list of Swift packages for any project in Xcode.
CocoaPods is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate CwlPreconditionTesting into your Xcode project using CocoaPods, specify it in your Podfile:

pod 'CwlPreconditionTesting', '~> 2.0'

## Usage

Expand All @@ -45,3 +53,7 @@ let e = catchBadInstruction {
```

**Warning**: this POSIX version can't be used when lldb is attached since lldb's Mach exception handler blocks the SIGILL from ever occurring. You should disable the "Debug Executable" setting for the tests in Xcode. The POSIX version of the signal handler is also whole process (rather than correctly scoped to the thread where the "catch" occurs).

## Thanks

Includes contributions from @abbeycode, @dnkoutso, @jeffh and @ikesyo. Extra thanks to @saagarjha for help with the ARM64 additions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@
#import "CwlMachBadInstructionHandler.h"

@protocol BadInstructionReply <NSObject>
+(NSNumber *)receiveReply:(NSValue *)value;
+(int)receiveReply:(bad_instruction_exception_reply_t)reply;
@end

/// A basic function that receives callbacks from mach_exc_server and relays them to the Swift implemented BadInstructionException.catch_mach_exception_raise_state.
kern_return_t catch_mach_exception_raise_state(mach_port_t exception_port, exception_type_t exception, const mach_exception_data_t code, mach_msg_type_number_t codeCnt, int *flavor, const thread_state_t old_state, mach_msg_type_number_t old_stateCnt, thread_state_t new_state, mach_msg_type_number_t *new_stateCnt) {
bad_instruction_exception_reply_t reply = { exception_port, exception, code, codeCnt, flavor, old_state, old_stateCnt, new_state, new_stateCnt };
Class badInstructionClass = NSClassFromString(@"BadInstructionException");
NSValue *value = [NSValue valueWithBytes: &reply objCType: @encode(bad_instruction_exception_reply_t)];
return [[badInstructionClass performSelector: @selector(receiveReply:) withObject: value] intValue];
return [badInstructionClass receiveReply:reply];
}

// The mach port should be configured so that this function is never used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//

#if arch(x86_64)
#if arch(x86_64) || arch(arm64)

import Foundation

Expand All @@ -35,34 +35,45 @@ import Foundation
// Treat it like a loaded shotgun. Don't point it at your face.

// This function is called from the signal handler to shut down the thread and return 1 (indicating a SIGILL was received).
private func callThreadExit() {
let callThreadExit = {
pthread_exit(UnsafeMutableRawPointer(bitPattern: 1))
}
} as @convention(c) () -> Void

// When called, this signal handler simulates a function call to `callThreadExit`
private func sigIllHandler(code: Int32, info: UnsafeMutablePointer<__siginfo>?, uap: UnsafeMutableRawPointer?) -> Void {
guard let context = uap?.assumingMemoryBound(to: ucontext64_t.self) else { return }
guard let context = uap?.bindMemory(to: ucontext64_t.self, capacity: 1) else { return }

#if arch(x86_64)
// 1. Decrement the stack pointer
context.pointee.uc_mcontext64.pointee.__ss.__rsp -= __uint64_t(MemoryLayout<Int>.size)
context.pointee.uc_mcontext64.pointee.__ss.__rsp -= UInt64(MemoryLayout<Int>.size)

// 2. Save the old Instruction Pointer to the stack.
let rsp = context.pointee.uc_mcontext64.pointee.__ss.__rsp
if let ump = UnsafeMutablePointer<__uint64_t>(bitPattern: UInt(rsp)) {
if let ump = UnsafeMutablePointer<UInt64>(bitPattern: UInt(rsp)) {
ump.pointee = rsp
}

// 3. Set the Instruction Pointer to the new function's address
var f: @convention(c) () -> Void = callThreadExit
withUnsafePointer(to: &f) { $0.withMemoryRebound(to: __uint64_t.self, capacity: 1) { ptr in
context.pointee.uc_mcontext64.pointee.__ss.__rip = ptr.pointee
} }
context.pointee.uc_mcontext64.pointee.__ss.__rip = unsafeBitCast(callThreadExit, to: UInt64.self)
#elseif arch(arm64)
// 1. Set the link register to the current address.
context.pointee.uc_mcontext64.pointee.__ss.__lr = context.pointee.uc_mcontext64.pointee.__ss.__pc

// 2. Set the Instruction Pointer to the new function's address.
context.pointee.uc_mcontext64.pointee.__ss.__pc = unsafeBitCast(callThreadExit, to: UInt64.self)
#endif
}

/// Without Mach exceptions or the Objective-C runtime, there's nothing to put in the exception object. It's really just a boolean – either a SIGILL was caught or not.
public class BadInstructionException {
}

#if arch(x86_64)
public let nativeSignal = SIGILL
#elseif arch(arm64)
public let nativeSignal = SIGTRAP
#endif

/// Run the provided block. If a POSIX SIGILL is received, handle it and return a BadInstructionException (which is just an empty object in this POSIX signal version). Otherwise return nil.
/// NOTE: This function is only intended for use in test harnesses – use in a distributed build is almost certainly a bad choice. If a SIGILL is received, the block will be interrupted using a C `longjmp`. The risks associated with abrupt jumps apply here: most Swift functions are *not* interrupt-safe. Memory may be leaked and the program will not necessarily be left in a safe state.
/// - parameter block: a function without parameters that will be run
Expand All @@ -74,13 +85,13 @@ public func catchBadInstruction(block: @escaping () -> Void) -> BadInstructionEx
var sigActionNew = sigaction(__sigaction_u: action, sa_mask: sigset_t(), sa_flags: SA_SIGINFO)

// Install the signal action
if sigaction(SIGILL, &sigActionNew, &sigActionPrev) != 0 {
if sigaction(nativeSignal, &sigActionNew, &sigActionPrev) != 0 {
fatalError("Sigaction error: \(errno)")
}

defer {
// Restore the previous signal action
if sigaction(SIGILL, &sigActionPrev, nil) != 0 {
if sigaction(nativeSignal, &sigActionPrev, nil) != 0 {
fatalError("Sigaction error: \(errno)")
}
}
Expand All @@ -90,7 +101,7 @@ public func catchBadInstruction(block: @escaping () -> Void) -> BadInstructionEx
// Run the block on its own thread
var handlerThread: pthread_t? = nil
let e = pthread_create(&handlerThread, nil, { arg in
(arg.assumingMemoryBound(to: (() -> Void).self).pointee)()
arg.bindMemory(to: (() -> Void).self, capacity: 1).pointee()
return nil
}, blockPtr)
precondition(e == 0, "Unable to create thread")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//

#if (os(macOS) || os(iOS)) && arch(x86_64)
#if (os(macOS) || os(iOS)) && (arch(x86_64) || arch(arm64))

import Foundation

#if SWIFT_PACKAGE
import CwlMachBadInstructionHandler
#endif

private func raiseBadInstructionException() {
var raiseBadInstructionException = {
BadInstructionException().raise()
}
} as @convention(c) () -> Void

/// A simple NSException subclass. It's not required to subclass NSException (since the exception type is represented in the name) but this helps for identifying the exception through runtime type.
@objc(BadInstructionException)
Expand All @@ -45,44 +45,47 @@ public class BadInstructionException: NSException {

/// An Objective-C callable function, invoked from the `mach_exc_server` callback function `catch_mach_exception_raise_state` to push the `raiseBadInstructionException` function onto the stack.
@objc(receiveReply:)
public class func receiveReply(_ value: NSValue) -> NSNumber {
var reply = bad_instruction_exception_reply_t(exception_port: 0, exception: 0, code: nil, codeCnt: 0, flavor: nil, old_state: nil, old_stateCnt: 0, new_state: nil, new_stateCnt: nil)
withUnsafeMutablePointer(to: &reply) { value.getValue(UnsafeMutableRawPointer($0)) }

let old_state: UnsafePointer<natural_t> = reply.old_state!
public class func receiveReply(_ reply: bad_instruction_exception_reply_t) -> CInt {
let old_state = UnsafeRawPointer(reply.old_state!).bindMemory(to: NativeThreadState.self, capacity: 1)
let old_stateCnt: mach_msg_type_number_t = reply.old_stateCnt
let new_state: thread_state_t = reply.new_state!
let new_state = UnsafeMutableRawPointer(reply.new_state!).bindMemory(to: NativeThreadState.self, capacity: 1)
let new_stateCnt: UnsafeMutablePointer<mach_msg_type_number_t> = reply.new_stateCnt!

// Make sure we've been given enough memory
if old_stateCnt != x86_THREAD_STATE64_COUNT || new_stateCnt.pointee < x86_THREAD_STATE64_COUNT {
return NSNumber(value: KERN_INVALID_ARGUMENT)
guard
old_stateCnt == nativeThreadStateCount,
new_stateCnt.pointee >= nativeThreadStateCount
else {
return KERN_INVALID_ARGUMENT
}

// Read the old thread state
var state = old_state.withMemoryRebound(to: x86_thread_state64_t.self, capacity: 1) { return $0.pointee }
// 0. Copy over the state.
new_state.pointee = old_state.pointee

#if arch(x86_64)
// 1. Decrement the stack pointer
state.__rsp -= __uint64_t(MemoryLayout<Int>.size)
new_state.pointee.__rsp -= UInt64(MemoryLayout<Int>.size)

// 2. Save the old Instruction Pointer to the stack.
if let pointer = UnsafeMutablePointer<__uint64_t>(bitPattern: UInt(state.__rsp)) {
pointer.pointee = state.__rip
} else {
return NSNumber(value: KERN_INVALID_ARGUMENT)
guard let pointer = UnsafeMutablePointer<UInt64>(bitPattern: UInt(new_state.pointee.__rsp)) else {
return KERN_INVALID_ARGUMENT
}

pointer.pointee = old_state.pointee.__rip

// 3. Set the Instruction Pointer to the new function's address
var f: @convention(c) () -> Void = raiseBadInstructionException
withUnsafePointer(to: &f) {
state.__rip = $0.withMemoryRebound(to: __uint64_t.self, capacity: 1) { return $0.pointee }
}
new_state.pointee.__rip = unsafeBitCast(raiseBadInstructionException, to: UInt64.self)

// Write the new thread state
new_state.withMemoryRebound(to: x86_thread_state64_t.self, capacity: 1) { $0.pointee = state }
new_stateCnt.pointee = x86_THREAD_STATE64_COUNT
#elseif arch(arm64)
// 1. Set the link register to the current address.
new_state.pointee.__lr = old_state.pointee.__pc

// 2. Set the Instruction Pointer to the new function's address.
new_state.pointee.__pc = unsafeBitCast(raiseBadInstructionException, to: UInt64.self)
#endif

new_stateCnt.pointee = nativeThreadStateCount

return NSNumber(value: KERN_SUCCESS)
return KERN_SUCCESS
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//

#if (os(macOS) || os(iOS)) && arch(x86_64)
#if (os(macOS) || os(iOS)) && (arch(x86_64) || arch(arm64))

import Foundation
import Swift
Expand Down Expand Up @@ -108,8 +108,8 @@ private func machMessageHandler(_ arg: UnsafeMutableRawPointer) -> UnsafeMutable
reply.Head.msgh_local_port = UInt32(MACH_PORT_NULL)
reply.Head.msgh_remote_port = request.Head.msgh_remote_port
reply.Head.msgh_size = UInt32(MemoryLayout<reply_mach_exception_raise_state_t>.size)
reply.NDR = mach_ndr_record()

reply.NDR = mach_ndr_record()
if !handledfirstException {
// Use the MiG generated server to invoke our handler for the request and fill in the rest of the reply structure
guard request.withMsgHeaderPointer(in: { requestPtr in reply.withMsgHeaderPointer { replyPtr in
Expand Down Expand Up @@ -181,12 +181,12 @@ public func catchBadInstruction(in block: @escaping () -> Void) -> BadInstructio
let currentExceptionPtr = context.currentExceptionPort
try kernCheck { context.withUnsafeMutablePointers { masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr in
// 3. Apply the mach port as the handler for this thread
thread_swap_exception_ports(mach_thread_self(), EXC_MASK_BAD_INSTRUCTION, currentExceptionPtr, Int32(bitPattern: UInt32(EXCEPTION_STATE) | MACH_EXCEPTION_CODES), x86_THREAD_STATE64, masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr)
thread_swap_exception_ports(mach_thread_self(), nativeMachExceptionMask, currentExceptionPtr, Int32(bitPattern: UInt32(EXCEPTION_STATE) | MACH_EXCEPTION_CODES), nativeThreadState, masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr)
} }

defer { context.withUnsafeMutablePointers { masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr in
// 6. Unapply the mach port
_ = thread_swap_exception_ports(mach_thread_self(), EXC_MASK_BAD_INSTRUCTION, 0, EXCEPTION_DEFAULT, THREAD_STATE_NONE, masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr)
_ = thread_swap_exception_ports(mach_thread_self(), nativeMachExceptionMask, 0, EXCEPTION_DEFAULT, THREAD_STATE_NONE, masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr)
} }

try withUnsafeMutablePointer(to: &context) { c throws in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//

#if (os(macOS) || os(iOS)) && arch(x86_64)
#if (os(macOS) || os(iOS)) && (arch(x86_64) || arch(arm64))

import Darwin

Expand All @@ -35,13 +35,22 @@ public func MACH_MSGH_BITS(_ remote: UInt32, _ local: UInt32) -> UInt32 { return
// From /usr/include/mach/exception_types.h
// #define EXC_BAD_INSTRUCTION 2 /* Instruction failed */
// #define EXC_MASK_BAD_INSTRUCTION (1 << EXC_BAD_INSTRUCTION)
public let EXC_BAD_INSTRUCTION: UInt32 = 2
public let EXC_MASK_BAD_INSTRUCTION: UInt32 = 1 << EXC_BAD_INSTRUCTION
//public let EXC_MASK_BAD_INSTRUCTION: UInt32 = 1 << EXC_BAD_INSTRUCTION

#if arch(x86_64)
// From /usr/include/mach/i386/thread_status.h
// #define x86_THREAD_STATE64_COUNT ((mach_msg_type_number_t) \
// ( sizeof (x86_thread_state64_t) / sizeof (int) ))
public let x86_THREAD_STATE64_COUNT = UInt32(MemoryLayout<x86_thread_state64_t>.size / MemoryLayout<Int32>.size)
// #define x86_threadStateCount ((mach_msg_type_number_t) \
// ( sizeof (x86_NativeThreadState) / sizeof (int) ))
public let nativeThreadState = x86_THREAD_STATE64
public let nativeThreadStateCount = UInt32(MemoryLayout<x86_thread_state64_t>.size / MemoryLayout<CInt>.size)
typealias NativeThreadState = x86_thread_state64_t
public let nativeMachExceptionMask = exception_mask_t(EXC_MASK_BAD_INSTRUCTION)
#elseif arch(arm64)
public let nativeThreadState = ARM_THREAD_STATE64
public let nativeThreadStateCount = UInt32(MemoryLayout<arm_thread_state64_t>.size / MemoryLayout<UInt32>.size)
typealias NativeThreadState = arm_thread_state64_t
public let nativeMachExceptionMask = exception_mask_t(EXC_MASK_BREAKPOINT)
#endif

public let EXC_TYPES_COUNT = 14
public struct execTypesCountTuple<T: ExpressibleByIntegerLiteral> {
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git",
"state": {
"branch": null,
"revision": "0630439888c94657a235ffcd5977d6047ef3c87b",
"version": "2.0.1"
"revision": "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688",
"version": "2.1.0"
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ let package = Package(
.library(name: "Nimble", targets: ["Nimble"]),
],
dependencies: [
.package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", .upToNextMajor(from: "2.1.0")),
],
targets: [
.target(
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,6 @@ expect(reachedPoint2) == false
Notes:

* This feature is only available in Swift.
* It is only supported for `x86_64` binaries, meaning _you cannot run this matcher on iOS devices, only simulators_.
* The tvOS simulator is supported, but using a different mechanism, requiring you to turn off the `Debug executable` scheme setting for your tvOS scheme's Test configuration.

## Swift Error Handling
Expand Down
7 changes: 4 additions & 3 deletions Sources/Nimble/Matchers/ThrowAssertion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public func catchBadInstruction(block: @escaping () -> Void) -> BadInstructionEx

public func throwAssertion<Out>() -> Predicate<Out> {
return Predicate { actualExpression in
#if arch(x86_64) && (canImport(Darwin) || canImport(Glibc))
#if (arch(x86_64) || arch(arm64)) && (canImport(Darwin) || canImport(Glibc))
let message = ExpectationMessage.expectedTo("throw an assertion")

var actualError: Error?
Expand Down Expand Up @@ -122,8 +122,9 @@ public func throwAssertion<Out>() -> Predicate<Out> {
}
#else
let message = """
The throwAssertion Nimble matcher can only run on x86_64 platforms.
You can silence this error by placing the test case inside an #if arch(x86_64) conditional statement.
The throwAssertion Nimble matcher can only run on x86_64 and arm64 platforms.
You can silence this error by placing the test case inside an #if arch(x86_64) || arch(arm64) conditional \
statement.
"""
fatalError(message)
#endif
Expand Down
Loading

0 comments on commit 0bf627c

Please sign in to comment.