forked from swiftlang/swift
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsourcekit_fuzzer.swift
195 lines (161 loc) · 5.49 KB
/
sourcekit_fuzzer.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// --- sourcekit_fuzzer.swift - a simple code completion fuzzer ---------------
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
// ----------------------------------------------------------------------------
//
// The idea here is we start with a source file and proceed to place the cursor
// at random locations in the file, eventually visiting all locations exactly
// once in a shuffled random order.
//
// If completion at a location crashes, we run the test case through 'creduce'
// to find a minimal reproducer that also crashes (possibly with a different
// crash, but in practice all the examples I've seen continue to crash in the
// same way as creduce performs its reduction).
//
// Once creduce fully reduces a test case, we save it to a file named
// 'crash-NNN.swift', with a RUN: line suitable for placing the test case in
// 'validation-tests/IDE/crashers_2'.
//
// The overall script execution stops once all source locations in the file
// have been tested.
//
// You must first install creduce <https://embed.cs.utah.edu/creduce/>
// somewhere in your $PATH. Then, run this script as follows:
//
// swift utils/sourcekit_fuzzer/sourcekit_fuzzer.swift <build dir> <source file>
//
// - <build dir> is your Swift build directory (the one with subdirectories
// named swift-macosx-x86_64 and llvm-macosx-x86_64).
//
// - <source file> is the source file to fuzz. Try any complex but
// self-contained Swift file that exercises a variety of language features;
// for example, I've had good results with the files in test/Prototypes/.
//
// TODO:
// - Add fuzzing for CursorInfo and RangeInfo
// - Get it running on Linux
// - Better error handling
// - More user-friendly output
import Darwin
import Foundation
// https://stackoverflow.com/questions/24026510/how-do-i-shuffle-an-array-in-swift/24029847
extension MutableCollection {
/// Shuffles the contents of this collection.
mutating func shuffle() {
let c = count
guard c > 1 else { return }
for (firstUnshuffled , unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) {
let d: Int = numericCast(arc4random_uniform(numericCast(unshuffledCount)))
guard d != 0 else { continue }
let i = index(firstUnshuffled, offsetBy: d)
swapAt(firstUnshuffled, i)
}
}
}
extension String {
func write(to path: String) throws {
try write(to: URL(fileURLWithPath: path), atomically: true, encoding: String.Encoding.utf8)
}
}
// Gross
enum ProcessError : Error {
case failed
}
func run(_ args: [String]) throws -> Int32 {
var pid: pid_t = 0
let argv = args.map {
$0.withCString(strdup)
}
defer { argv.forEach { free($0) } }
let envp = ProcessInfo.processInfo.environment.map {
"\($0.0)=\($0.1)".withCString(strdup)
}
defer { envp.forEach { free($0) } }
let result = posix_spawn(&pid, argv[0], nil, nil, argv + [nil], envp + [nil])
if result != 0 { throw ProcessError.failed }
var stat: Int32 = 0
waitpid(pid, &stat, 0)
return stat
}
var arguments = CommandLine.arguments
// Workaround for behavior of CommandLine in script mode, where we don't drop
// the filename argument from the list.
if arguments.first == "sourcekit_fuzzer.swift" {
arguments = Array(arguments[1...])
}
if arguments.count != 2 {
print("Usage: sourcekit_fuzzer <build directory> <file>")
exit(1)
}
let buildDir = arguments[0]
let notPath = "\(buildDir)/llvm-macosx-x86_64/bin/not"
let swiftIdeTestPath = "\(buildDir)/swift-macosx-x86_64/bin/swift-ide-test"
let creducePath = "/usr/local/bin/creduce"
let file = arguments[1]
let contents = try! String(contentsOfFile: file)
var offsets = Array(0...contents.count)
offsets.shuffle()
var good = 0
var bad = 0
for offset in offsets {
print("TOTAL FAILURES: \(bad) out of \(bad + good)")
let index = contents.index(contents.startIndex, offsetBy: offset)
let prefix = contents[..<index]
let suffix = contents[index...]
let newContents = String(prefix + "#^A^#" + suffix)
let sourcePath = "out\(offset).swift"
try! newContents.write(to: sourcePath)
let shellScriptPath = "out\(offset).sh"
let shellScript = """
#!/bin/sh
\(notPath) --crash \(swiftIdeTestPath) -code-completion -code-completion-token=A -source-filename=\(sourcePath)
"""
try! shellScript.write(to: shellScriptPath)
defer {
unlink(shellScriptPath)
unlink(sourcePath)
}
do {
let result = chmod(shellScriptPath, 0o700)
if result != 0 {
print("chmod failed")
exit(1)
}
}
do {
let result = try! run(["./\(shellScriptPath)"])
if result != 0 {
good += 1
continue
}
}
do {
// Because we invert the exit code with 'not', an exit code for 0 actually
// indicates failure
print("Failed at offset \(offset)")
print("Reducing...")
let result = try! run([creducePath, shellScriptPath, sourcePath])
if result != 0 {
print("creduce failed")
exit(1)
}
bad += 1
}
do {
let reduction = try! String(contentsOfFile: sourcePath)
let testcasePath = "crash-\(bad).swift"
let testcase = """
// RUN: \(notPath) --crash \(swiftIdeTestPath) -code-completion -code-completion-token=A -source-filename=%s
// REQUIRES: asserts
\(reduction)
"""
try! testcase.write(to: testcasePath)
}
}