Skip to content

Commit 60e3860

Browse files
authored
Merge pull request swiftlang#12030 from slavapestov/sourcekit-fuzzer
Preliminary implementation of a code completion fuzzer
2 parents 5490877 + 3b54234 commit 60e3860

File tree

3 files changed

+204
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)