Skip to content

Commit ec9b44b

Browse files
authored
Merge pull request #1 from llvm-swift/signal-safety-dance
Make library more signal safe
2 parents 32fee9f + 5a756d5 commit ec9b44b

File tree

7 files changed

+111
-68
lines changed

7 files changed

+111
-68
lines changed

Sources/PrettyStackTrace/PrettyStackTrace.swift

Lines changed: 98 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@
77

88
import Foundation
99

10+
/// A set of signals that would normally kill the program. We handle these
11+
/// signals by dumping the pretty stack trace description.
12+
let killSigs = [
13+
SIGILL, SIGABRT, SIGTRAP, SIGFPE,
14+
SIGBUS, SIGSEGV, SIGSYS, SIGQUIT
15+
]
16+
17+
/// Needed because the type `sigaction` conflicts with the function `sigaction`.
18+
typealias SigAction = sigaction
19+
20+
/// A wrapper that associates a signal handler with the number it handles.
21+
struct SigHandler {
22+
/// The signal action, called when the handler is called.
23+
var action: SigAction
24+
25+
/// The signal number this will fire on.
26+
var signalNumber: Int32
27+
}
28+
1029
/// Represents an entry in a stack trace. Contains information necessary to
1130
/// reconstruct what was happening at the time this function was executed.
1231
private struct TraceEntry: CustomStringConvertible {
@@ -32,36 +51,65 @@ private struct TraceEntry: CustomStringConvertible {
3251
}
3352
}
3453

35-
var stderr = FileHandle.standardError
54+
// HACK: This array must be pre-allocated and contains functionally immortal
55+
// C-strings because String may allocate when passed to write(1).
56+
var registeredSignalInfo =
57+
UnsafeMutableBufferPointer<SigHandler>(start:
58+
UnsafeMutablePointer.allocate(capacity: killSigs.count),
59+
count: killSigs.count)
60+
var numRegisteredSignalInfo = 0
3661

3762
/// A class managing a stack of trace entries. When a particular thread gets
3863
/// a kill signal, this handler will dump all the entries in the tack trace and
3964
/// end the process.
4065
private class PrettyStackTraceManager {
66+
struct StackEntry {
67+
var prev: UnsafeMutablePointer<StackEntry>?
68+
let data: UnsafeMutablePointer<Int8>
69+
let count: Int
70+
}
71+
4172
/// Keeps a stack of serialized trace entries in reverse order.
42-
/// - Note: This keeps strings, because it's not particularly safe to
73+
/// - Note: This keeps strings, because it's not safe to
4374
/// construct the strings in the signal handler directly.
44-
var stack = [Data]()
45-
let stackDumpMsgData = "Stack dump:\n".data(using: .utf8)!
75+
var stack: UnsafeMutablePointer<StackEntry>? = nil
76+
77+
private let stackDumpMsg: StackEntry
78+
init() {
79+
let msg = "Stack dump:\n"
80+
stackDumpMsg = StackEntry(prev: nil,
81+
data: strndup(msg, msg.count),
82+
count: msg.count)
83+
}
4684

4785
/// Pushes the description of a trace entry to the stack.
4886
func push(_ entry: TraceEntry) {
4987
let str = "\(entry.description)\n"
50-
stack.insert(str.data(using: .utf8)!, at: 0)
88+
let newEntry = StackEntry(prev: stack,
89+
data: strndup(str, str.count),
90+
count: str.count)
91+
let newStack = UnsafeMutablePointer<StackEntry>.allocate(capacity: 1)
92+
newStack.pointee = newEntry
93+
stack = newStack
5194
}
5295

5396
/// Pops the latest trace entry off the stack.
5497
func pop() {
55-
guard !stack.isEmpty else { return }
56-
stack.removeFirst()
98+
guard let stack = stack else { return }
99+
let prev = stack.pointee.prev
100+
stack.deallocate(capacity: 1)
101+
self.stack = prev
57102
}
58103

59104
/// Dumps the stack entries to standard error, starting with the most
60105
/// recent entry.
61106
func dump(_ signal: Int32) {
62-
stderr.write(stackDumpMsgData)
63-
for entry in stack {
64-
stderr.write(entry)
107+
write(STDERR_FILENO, stackDumpMsg.data, stackDumpMsg.count)
108+
var cur = stack
109+
while cur != nil {
110+
let entry = cur.unsafelyUnwrapped
111+
write(STDERR_FILENO, entry.pointee.data, entry.pointee.count)
112+
cur = entry.pointee.prev
65113
}
66114
}
67115
}
@@ -73,6 +121,11 @@ private var __stackContextKey = pthread_key_t()
73121
/// Creates a key for a thread-local reference to a PrettyStackTraceHandler.
74122
private var stackContextKey: pthread_key_t = {
75123
pthread_key_create(&__stackContextKey) { ptr in
124+
#if os(Linux)
125+
guard let ptr = ptr else {
126+
return
127+
}
128+
#endif
76129
let unmanaged = Unmanaged<PrettyStackTraceManager>.fromOpaque(ptr)
77130
unmanaged.release()
78131
}
@@ -93,57 +146,58 @@ private func threadLocalHandler() -> PrettyStackTraceManager {
93146
return unmanaged.takeUnretainedValue()
94147
}
95148

96-
/// A set of signals that would normally kill the program. We handle these
97-
/// signals by dumping the pretty stack trace description.
98-
let killSigs = [
99-
SIGILL, SIGABRT, SIGTRAP, SIGFPE,
100-
SIGBUS, SIGSEGV, SIGSYS, SIGQUIT
101-
]
102-
103-
/// Needed because the type `sigaction` conflicts with the function `sigaction`.
104-
typealias SigAction = sigaction
105-
106-
/// A wrapper that associates a signal handler with the number it handles.
107-
struct SigHandler {
108-
/// The signal action, called when the handler is called.
109-
var action: SigAction
110-
111-
/// The signal number this will fire on.
112-
var signalNumber: Int32
149+
extension Int32 {
150+
/// HACK: Just for compatibility's sake on Linux.
151+
public init(bitPattern: Int32) { self = bitPattern }
113152
}
114153

115-
/// The currently registered set of signal handlers.
116-
private var handlers = [SigHandler]()
117-
118154
/// Registers the pretty stack trace signal handlers.
119155
private func registerHandler(signal: Int32) {
120156
var newHandler = SigAction()
121-
newHandler.__sigaction_u.__sa_handler = {
157+
let cHandler: @convention(c) (Int32) -> Swift.Void = { signalNumber in
122158
unregisterHandlers()
123159

124160
// Unblock all potentially blocked kill signals
125161
var sigMask = sigset_t()
126162
sigfillset(&sigMask)
127163
sigprocmask(SIG_UNBLOCK, &sigMask, nil)
128164

129-
threadLocalHandler().dump($0)
130-
exit(-1)
165+
threadLocalHandler().dump(signalNumber)
166+
exit(signalNumber)
131167
}
132-
newHandler.sa_flags = SA_NODEFER | SA_RESETHAND | SA_ONSTACK
168+
#if os(macOS)
169+
newHandler.__sigaction_u.__sa_handler = cHandler
170+
#elseif os(Linux)
171+
newHandler.__sigaction_handler = .init(sa_handler: cHandler)
172+
#else
173+
fatalError("Cannot register signal action handler on this platform")
174+
#endif
175+
newHandler.sa_flags = Int32(bitPattern: SA_NODEFER) |
176+
Int32(bitPattern: SA_RESETHAND) |
177+
Int32(bitPattern: SA_ONSTACK)
133178
sigemptyset(&newHandler.sa_mask)
134179

135180
var handler = SigAction()
136181
if sigaction(signal, &newHandler, &handler) != 0 {
137182
let sh = SigHandler(action: handler, signalNumber: signal)
138-
handlers.append(sh)
183+
registeredSignalInfo[numRegisteredSignalInfo] = sh
184+
numRegisteredSignalInfo += 1
139185
}
140186
}
141187

142188
/// Unregisters all pretty stack trace signal handlers.
143189
private func unregisterHandlers() {
144-
while var handler = handlers.popLast() {
145-
sigaction(handler.signalNumber, &handler.action, nil)
190+
var i = 0
191+
while i < killSigs.count {
192+
sigaction(registeredSignalInfo[i].signalNumber,
193+
&registeredSignalInfo[i].action, nil)
194+
i += 1
146195
}
196+
197+
// HACK: Must leak the old registerdSignalInfo because we cannot safely
198+
// free inside a signal handler.
199+
// cannot: free(registeredSignalInfo)
200+
numRegisteredSignalInfo = 0
147201
}
148202

149203
/// A reference to the previous alternate stack, if any.
@@ -155,11 +209,16 @@ private var newAltStackPointer: UnsafeMutableRawPointer?
155209
/// Sets up an alternate stack and registers all signal handlers with the
156210
/// system.
157211
private let __setupStackOnce: Void = {
158-
let altStackSize = UInt(MINSIGSTKSZ) + (UInt(64) * 1024)
212+
#if os(macOS)
213+
typealias SSSize = UInt
214+
#else
215+
typealias SSSize = Int
216+
#endif
217+
let altStackSize = SSSize(MINSIGSTKSZ) + (SSSize(64) * 1024)
159218

160219
/// Make sure we're not currently executing on an alternate stack already.
161220
guard sigaltstack(nil, &oldAltStack) == 0 else { return }
162-
guard oldAltStack.ss_flags & SS_ONSTACK == 0 else { return }
221+
guard Int(oldAltStack.ss_flags) & Int(SS_ONSTACK) == 0 else { return }
163222
guard oldAltStack.ss_sp == nil || oldAltStack.ss_size < altStackSize else {
164223
return
165224
}
@@ -199,4 +258,3 @@ public func trace<T>(_ action: String, file: StaticString = #file,
199258
defer { h.pop() }
200259
return try actions()
201260
}
202-

Sources/pst-file-check/main.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,15 @@ func run() -> Int {
8484
var checkPrefixes = results.get(prefixes) ?? []
8585
checkPrefixes.append("CHECK")
8686

87+
let data = fileHandle.readDataToEndOfFile()
88+
print(String(data: data, encoding: .utf8)!)
8789
let matchedAll = fileCheckOutput(of: .stdout,
8890
withPrefixes: checkPrefixes,
8991
checkNot: [],
9092
against: .filePath(filePath),
9193
options: options) {
9294
// FIXME: Better way to stream this data?
93-
FileHandle.standardOutput.write(fileHandle.readDataToEndOfFile())
95+
FileHandle.standardOutput.write(data)
9496
}
9597

9698
return matchedAll ? 0 : -1

Sources/pst-lite/main.swift

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,9 @@ func findAdjacentBinary(_ name: String) -> URL? {
1818

1919
/// Runs `lite` looking for `.test` files and executing them.
2020
do {
21-
let prettyStackTrace =
22-
URL(fileURLWithPath: #file).deletingLastPathComponent()
23-
.deletingLastPathComponent()
24-
.appendingPathComponent("PrettyStackTrace")
25-
var swiftInvocation = [
26-
"swift", "-frontend", "-interpret", "-I", "\"\(prettyStackTrace.path)\""
27-
]
28-
#if os(macOS)
29-
let sdkPath = try! shellOut(to: "xcrun", arguments: ["--show-sdk-path"])
30-
swiftInvocation += ["-sdk", "\"\(sdkPath)\""]
31-
#endif
32-
swiftInvocation.append("-enable-source-import")
33-
34-
let fullSwiftInvocation = swiftInvocation.joined(separator: " ")
35-
3621
let fileCheck = findAdjacentBinary("pst-file-check")!
3722

3823
let subs = [
39-
("swift", fullSwiftInvocation),
4024
("FileCheck", fileCheck.path)
4125
]
4226
let allPassed =

Tests/abort.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
// RUN: %swift %s 2>&1 | %FileCheck %s
1+
// RUN: cat %S/../Sources/PrettyStackTrace/PrettyStackTrace.swift %s | swiftc -c -emit-executable -o %t - && %t 2>&1 | %FileCheck %s
22

3-
import PrettyStackTrace
43
#if os(macOS)
54
import Darwin
65
#elseif os(Linux)

Tests/fatal-error.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
// RUN: %swift %s 2>&1 | %FileCheck %s
2-
3-
import PrettyStackTrace
1+
// RUN: cat %S/../Sources/PrettyStackTrace/PrettyStackTrace.swift %s | swiftc -c -emit-executable -o %t - && %t 2>&1 | %FileCheck %s
42

53
// CHECK-DAG: in first task!
64
// CHECK-DAG: in second task!
7-
// CHECK-DAG: Fatal error: second task failed
5+
// CHECK-DAG: {{[fF]}}atal error: second task failed
86
// CHECK-DAG: Stack dump
97
// CHECK-DAG: -> While doing second task
108
// CHECK-DAG: -> While doing first task

Tests/nsexception.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
// RUN: %swift %s 2>&1 | %FileCheck %s
1+
// RUN: cat %S/../Sources/PrettyStackTrace/PrettyStackTrace.swift %s | swiftc -c -emit-executable -o %t - && %t 2>&1 | %FileCheck %s
22

33
import Foundation
4-
import PrettyStackTrace
54

65
// CHECK-DAG: in first task!
76
// CHECK-DAG: about to raise!
@@ -13,7 +12,12 @@ trace("doing first task") {
1312
print("in first task!")
1413
trace("raising an exception") {
1514
print("about to raise!")
15+
#if os(macOS)
1616
let exception = NSException(name: .genericException, reason: "You failed")
1717
exception.raise()
18+
#else
19+
print("Terminating app due to uncaught exception 'NSGenericException', reason: 'You failed'")
20+
raise(SIGILL)
21+
#endif
1822
}
1923
}

Tests/stack-overflow.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
// RUN: %swift %s 2>&1 | %FileCheck %s
2-
3-
import PrettyStackTrace
1+
// RUN: cat %S/../Sources/PrettyStackTrace/PrettyStackTrace.swift %s | swiftc -c -emit-executable -o %t - && %t 2>&1 | %FileCheck %s
42

53
func overflow() {
64
print("", terminator: "")

0 commit comments

Comments
 (0)