Skip to content

Commit 5446d33

Browse files
authored
Merge pull request #28126 from danliew-apple/rdar_56346688
[Sanitizers] Add Driver/Frontend option to enable sanitizer instrumentation that supports error recovery
2 parents 0a3a130 + 63e7290 commit 5446d33

File tree

13 files changed

+223
-2
lines changed

13 files changed

+223
-2
lines changed

include/swift/AST/DiagnosticsFrontend.def

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ ERROR(error_option_requires_sanitizer, none,
7171
"option '%0' requires a sanitizer to be enabled. Use -sanitize= to "
7272
"enable a sanitizer", (StringRef))
7373

74+
WARNING(warning_option_requires_specific_sanitizer, none,
75+
"option '%0' has no effect when '%1' sanitizer is disabled. Use -sanitize=%1 to "
76+
"enable the sanitizer", (StringRef, StringRef))
77+
7478
ERROR(error_option_missing_required_argument, none,
7579
"option '%0' is missing a required argument (%1)", (StringRef, StringRef))
7680

include/swift/AST/IRGenOptions.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ class IRGenOptions {
106106
/// Which sanitizer is turned on.
107107
OptionSet<SanitizerKind> Sanitizers;
108108

109+
/// Which sanitizer(s) have recovery instrumentation enabled.
110+
OptionSet<SanitizerKind> SanitizersWithRecoveryInstrumentation;
111+
109112
/// Path prefixes that should be rewritten in debug info.
110113
PathRemapper DebugPrefixMap;
111114

@@ -239,6 +242,7 @@ class IRGenOptions {
239242
: DWARFVersion(2), OutputKind(IRGenOutputKind::LLVMAssembly),
240243
Verify(true), OptMode(OptimizationMode::NotSet),
241244
Sanitizers(OptionSet<SanitizerKind>()),
245+
SanitizersWithRecoveryInstrumentation(OptionSet<SanitizerKind>()),
242246
DebugInfoLevel(IRGenDebugInfoLevel::None),
243247
DebugInfoFormat(IRGenDebugInfoFormat::None),
244248
DisableClangModuleSkeletonCUs(false), UseJIT(false),

include/swift/Option/Options.td

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,15 @@ def sanitize_EQ : CommaJoined<["-"], "sanitize=">,
889889
Flags<[FrontendOption, NoInteractiveOption]>, MetaVarName<"<check>">,
890890
HelpText<"Turn on runtime checks for erroneous behavior.">;
891891

892+
def sanitize_recover_EQ
893+
: CommaJoined<["-"], "sanitize-recover=">,
894+
Flags<[FrontendOption, NoInteractiveOption]>,
895+
MetaVarName<"<check>">,
896+
HelpText<"Specify which sanitizer runtime checks (see -sanitize=) will "
897+
"generate instrumentation that allows error recovery. Listed "
898+
"checks should be comma separated. Default behavior is to not "
899+
"allow error recovery.">;
900+
892901
def sanitize_coverage_EQ : CommaJoined<["-"], "sanitize-coverage=">,
893902
Flags<[FrontendOption, NoInteractiveOption]>,
894903
MetaVarName<"<type>">,

include/swift/Option/SanitizerOptions.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ OptionSet<SanitizerKind> parseSanitizerArgValues(
3636
const llvm::Triple &Triple, DiagnosticEngine &Diag,
3737
llvm::function_ref<bool(llvm::StringRef, bool)> sanitizerRuntimeLibExists);
3838

39+
OptionSet<SanitizerKind> parseSanitizerRecoverArgValues(
40+
const llvm::opt::Arg *A, const OptionSet<SanitizerKind> &enabledSanitizers,
41+
DiagnosticEngine &Diags, bool emitWarnings);
42+
3943
/// Parses a -sanitize-coverage= argument's value.
4044
llvm::SanitizerCoverageOptions parseSanitizerCoverageArgValue(
4145
const llvm::opt::Arg *A,

lib/Driver/Driver.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,6 +1636,14 @@ void Driver::buildOutputInfo(const ToolChain &TC, const DerivedArgList &Args,
16361636
return TC.sanitizerRuntimeLibExists(Args, sanitizerName, shared);
16371637
});
16381638

1639+
if (const Arg *A = Args.getLastArg(options::OPT_sanitize_recover_EQ)) {
1640+
// Just validate the args. The frontend will parse these again and actually
1641+
// use them. To avoid emitting warnings multiple times we surpress warnings
1642+
// here but not in the frontend.
1643+
(void)parseSanitizerRecoverArgValues(A, OI.SelectedSanitizers, Diags,
1644+
/*emitWarnings=*/false);
1645+
}
1646+
16391647
if (const Arg *A = Args.getLastArg(options::OPT_sanitize_coverage_EQ)) {
16401648

16411649
// Check that the sanitizer coverage flags are supported if supplied.

lib/Driver/ToolChains.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ static void addCommonFrontendArgs(const ToolChain &TC, const OutputInfo &OI,
214214
inputArgs.AddLastArg(arguments, options::OPT_profile_coverage_mapping);
215215
inputArgs.AddLastArg(arguments, options::OPT_warnings_as_errors);
216216
inputArgs.AddLastArg(arguments, options::OPT_sanitize_EQ);
217+
inputArgs.AddLastArg(arguments, options::OPT_sanitize_recover_EQ);
217218
inputArgs.AddLastArg(arguments, options::OPT_sanitize_coverage_EQ);
218219
inputArgs.AddLastArg(arguments, options::OPT_static);
219220
inputArgs.AddLastArg(arguments, options::OPT_swift_version);

lib/Frontend/CompilerInvocation.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,12 @@ static bool ParseSILArgs(SILOptions &Opts, ArgList &Args,
908908
IRGenOpts.Sanitizers = Opts.Sanitizers;
909909
}
910910

911+
if (const Arg *A = Args.getLastArg(options::OPT_sanitize_recover_EQ)) {
912+
IRGenOpts.SanitizersWithRecoveryInstrumentation =
913+
parseSanitizerRecoverArgValues(A, Opts.Sanitizers, Diags,
914+
/*emitWarnings=*/true);
915+
}
916+
911917
if (auto A = Args.getLastArg(OPT_enable_verify_exclusivity,
912918
OPT_disable_verify_exclusivity)) {
913919
Opts.VerifyExclusivity

lib/IRGen/IRGen.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,14 @@ static void addSwiftMergeFunctionsPass(const PassManagerBuilder &Builder,
126126

127127
static void addAddressSanitizerPasses(const PassManagerBuilder &Builder,
128128
legacy::PassManagerBase &PM) {
129-
PM.add(createAddressSanitizerFunctionPass());
130-
PM.add(createModuleAddressSanitizerLegacyPassPass());
129+
auto &BuilderWrapper =
130+
static_cast<const PassManagerBuilderWrapper &>(Builder);
131+
auto recover =
132+
bool(BuilderWrapper.IRGOpts.SanitizersWithRecoveryInstrumentation &
133+
SanitizerKind::Address);
134+
PM.add(createAddressSanitizerFunctionPass(/*CompileKernel=*/false, recover));
135+
PM.add(createModuleAddressSanitizerLegacyPassPass(/*CompileKernel=*/false,
136+
recover));
131137
}
132138

133139
static void addThreadSanitizerPass(const PassManagerBuilder &Builder,

lib/Option/SanitizerOptions.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,49 @@ OptionSet<SanitizerKind> swift::parseSanitizerArgValues(
185185
return sanitizerSet;
186186
}
187187

188+
OptionSet<SanitizerKind> swift::parseSanitizerRecoverArgValues(
189+
const llvm::opt::Arg *A, const OptionSet<SanitizerKind> &enabledSanitizers,
190+
DiagnosticEngine &Diags, bool emitWarnings) {
191+
OptionSet<SanitizerKind> sanitizerRecoverSet;
192+
193+
// Find the sanitizer kind.
194+
for (const char *arg : A->getValues()) {
195+
Optional<SanitizerKind> optKind = parse(arg);
196+
197+
// Unrecognized sanitizer option
198+
if (!optKind.hasValue()) {
199+
Diags.diagnose(SourceLoc(), diag::error_unsupported_option_argument,
200+
A->getOption().getPrefixedName(), arg);
201+
continue;
202+
}
203+
SanitizerKind kind = optKind.getValue();
204+
205+
// Only support ASan for now.
206+
if (kind != SanitizerKind::Address) {
207+
Diags.diagnose(SourceLoc(), diag::error_unsupported_option_argument,
208+
A->getOption().getPrefixedName(), arg);
209+
continue;
210+
}
211+
212+
// Check that the sanitizer is enabled.
213+
if (!(enabledSanitizers & kind)) {
214+
SmallString<128> b;
215+
if (emitWarnings) {
216+
Diags.diagnose(SourceLoc(),
217+
diag::warning_option_requires_specific_sanitizer,
218+
(A->getOption().getPrefixedName() + toStringRef(kind))
219+
.toStringRef(b),
220+
toStringRef(kind));
221+
}
222+
continue;
223+
}
224+
225+
sanitizerRecoverSet |= kind;
226+
}
227+
228+
return sanitizerRecoverSet;
229+
}
230+
188231
std::string swift::getSanitizerList(const OptionSet<SanitizerKind> &Set) {
189232
std::string list;
190233
#define SANITIZER(_, kind, name, file) \

test/Driver/sanitize_recover.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// RUN: not %swiftc_driver -driver-print-jobs -sanitize=address -sanitize-recover=foo %s 2>&1 | %FileCheck -check-prefix=SAN_RECOVER_INVALID_ARG %s
2+
// RUN: not %swiftc_driver -driver-print-jobs -sanitize=address -sanitize-recover=thread %s 2>&1 | %FileCheck -check-prefix=SAN_RECOVER_UNSUPPORTED_ARG %s
3+
// RUN: %swiftc_driver -v -sanitize-recover=address %s -o %t 2>&1 | %FileCheck -check-prefix=SAN_RECOVER_MISSING_INSTRUMENTATION_OPTION %s
4+
// RUN: %swiftc_driver -driver-print-jobs -sanitize=address -sanitize-recover=address %s 2>&1 | %FileCheck -check-prefix=ASAN_WITH_RECOVER %s
5+
// RUN: %swiftc_driver -driver-print-jobs -sanitize=address %s 2>&1 | %FileCheck -check-prefix=ASAN_WITHOUT_RECOVER --implicit-check-not='-sanitize-recover=address' %s
6+
7+
// SAN_RECOVER_INVALID_ARG: unsupported argument 'foo' to option '-sanitize-recover='
8+
// SAN_RECOVER_UNSUPPORTED_ARG: unsupported argument 'thread' to option '-sanitize-recover='
9+
10+
// SAN_RECOVER_MISSING_INSTRUMENTATION_OPTION: warning: option '-sanitize-recover=address' has no effect when 'address' sanitizer is disabled. Use -sanitize=address to enable the sanitizer
11+
// SAN_RECOVER_MISSING_INSTRUMENTATION_OPTION-NOT: warning: option '-sanitize-recover=address' has no effect when 'address' sanitizer is disabled. Use -sanitize=address to enable the sanitizer
12+
13+
// ASAN_WITH_RECOVER: swift
14+
// ASAN_WITH_RECOVER-DAG: -sanitize=address
15+
// ASAN_WITH_RECOVER-DAG: -sanitize-recover=address
16+
17+
// ASAN_WITHOUT_RECOVER: swift
18+
// ASAN_WITHOUT_RECOVER: -sanitize=address
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// RUN: %target-swift-frontend -emit-ir -sanitize=address -sanitize-recover=address %s | %FileCheck %s -check-prefix=ASAN_RECOVER
2+
// RUN: %target-swift-frontend -emit-ir -sanitize=address %s | %FileCheck %s -check-prefix=ASAN_NO_RECOVER
3+
// RUN: %target-swift-frontend -emit-ir -sanitize-recover=address %s | %FileCheck %s -check-prefix=NO_ASAN_RECOVER
4+
5+
// ASAN_RECOVER: declare void @__asan_loadN_noabort(
6+
// ASAN_NO_RECOVER: declare void @__asan_loadN(
7+
8+
let size:Int = 128;
9+
let x = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
10+
x.initialize(repeating: 0, count: size)
11+
x.advanced(by: 0).pointee = 5;
12+
print("Read first element:\(x.advanced(by: 0).pointee)")
13+
x.deallocate();
14+
15+
// There should be no ASan instrumentation in this case.
16+
// NO_ASAN_RECOVER-NOT: declare void @__asan_load

test/Sanitizers/asan_interface.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// This file is a swift bridging header to ASan's interface. It exists so
2+
// we don't need to worry about where the header lives and instead let Clang
3+
// figure out where the header lives.
4+
#include "sanitizer/asan_interface.h"

test/Sanitizers/asan_recover.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// REQUIRES: executable_test
2+
// REQUIRES: asan_runtime
3+
// UNSUPPORTED: windows
4+
5+
// Check with recovery instrumentation and runtime option to continue execution.
6+
// RUN: %target-swiftc_driver %s -target %sanitizers-target-triple -g -sanitize=address -sanitize-recover=address -import-objc-header %S/asan_interface.h -emit-ir -o %t.asan_recover.ll
7+
// RUN: %FileCheck -check-prefix=CHECK-IR -input-file=%t.asan_recover.ll %s
8+
// RUN: %target-swiftc_driver %s -target %sanitizers-target-triple -g -sanitize=address -sanitize-recover=address -import-objc-header %S/asan_interface.h -o %t_asan_recover
9+
// RUN: %target-codesign %t_asan_recover
10+
// RUN: env %env-ASAN_OPTIONS=halt_on_error=0 %target-run %t_asan_recover > %t_asan_recover.stdout 2> %t_asan_recover.stderr
11+
// RUN: %FileCheck --check-prefixes=CHECK-COMMON-STDERR,CHECK-RECOVER-STDERR -input-file=%t_asan_recover.stderr %s
12+
// RUN: %FileCheck --check-prefixes=CHECK-COMMON-STDOUT,CHECK-RECOVER-STDOUT -input-file=%t_asan_recover.stdout %s
13+
14+
// Check with recovery instrumentation but without runtime option to continue execution.
15+
// RUN: not env %env-ASAN_OPTIONS=abort_on_error=0,halt_on_error=1 %target-run %t_asan_recover > %t_asan_no_runtime_recover.stdout 2> %t_asan_no_runtime_recover.stderr
16+
// RUN: %FileCheck --check-prefixes=CHECK-COMMON-STDERR -input-file=%t_asan_no_runtime_recover.stderr %s
17+
// RUN: %FileCheck --check-prefixes=CHECK-COMMON-STDOUT,CHECK-NO-RECOVER-STDOUT -input-file=%t_asan_no_runtime_recover.stdout %s
18+
19+
// Check that without recovery instrumentation and runtime option to continue execution that error recovery does not happen.
20+
// RUN: %target-swiftc_driver %s -target %sanitizers-target-triple -g -sanitize=address -import-objc-header %S/asan_interface.h -o %t_asan_no_recover
21+
// RUN: %target-codesign %t_asan_no_recover
22+
// RUN: not env %env-ASAN_OPTIONS=abort_on_error=0,halt_on_error=0 %target-run %t_asan_no_recover > %t_asan_no_recover.stdout 2> %t_asan_no_recover.stderr
23+
// RUN: %FileCheck --check-prefixes=CHECK-COMMON-STDERR -input-file=%t_asan_no_recover.stderr %s
24+
// RUN: %FileCheck --check-prefixes=CHECK-COMMON-STDOUT,CHECK-NO-RECOVER-STDOUT -input-file=%t_asan_no_recover.stdout %s
25+
26+
// We need to test reads via instrumentation not via runtime so try to check
27+
// for calls to unwanted interceptor/runtime functions.
28+
// CHECK-IR-NOT: call {{.+}} @__asan_memcpy
29+
// CHECK-IR-NOT: call {{.+}} @memcpy
30+
31+
// FIXME: We need this so we can flush stdout but this won't
32+
// work on other Platforms (e.g. Windows).
33+
#if os(Linux)
34+
import Glibc
35+
#else
36+
import Darwin.C
37+
#endif
38+
39+
// CHECK-COMMON-STDOUT: START
40+
print("START")
41+
fflush(stdout)
42+
43+
let size:Int = 128;
44+
45+
// In this test we need multiple issues to occur that ASan can detect.
46+
// Allocating a buffer and artificially poisoning it seems like the best way to
47+
// test this because there's no undefined behavior happening. Hopefully this
48+
// means that the program behaviour after ASan catches an issue should be
49+
// consistent. If we did something different like two use-after-free issues the
50+
// behaviour could be very unpredicatable resulting in a flakey test.
51+
var x = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
52+
x.initialize(repeating: 0, count: size)
53+
__asan_poison_memory_region(UnsafeMutableRawPointer(x), size)
54+
55+
// Perform accesses that ASan will catch. Note it is important here that
56+
// the reads are performed **in** the instrumented code so that the
57+
// instrumentation catches the access to poisoned memory. I tried doing:
58+
//
59+
// ```
60+
// var x = x.advanced(by: 0).pointee
61+
// print(x)
62+
// ```
63+
//
64+
// However, this generated code that called into memcpy rather than performing
65+
// a direct read which meant that ASan caught an issue via its interceptors
66+
// rather than from instrumentation, which does not test the right thing here.
67+
//
68+
// Doing:
69+
//
70+
// ```
71+
// print("Read first element:\(x.advanced(by: 0).pointee)")
72+
// ```
73+
//
74+
// seems to do the right thing right now but this seems really fragile.
75+
76+
// First error
77+
// NOTE: Testing for stackframe `#0` should ensure that the poison read
78+
// happened in instrumentation and not in an interceptor.
79+
// CHECK-COMMON-STDERR: AddressSanitizer: use-after-poison
80+
// CHECK-COMMON-STDERR: #0 0x{{.+}} in main {{.*}}asan_recover.swift:[[@LINE+1]]
81+
print("Read first element:\(x.advanced(by: 0).pointee)")
82+
fflush(stdout)
83+
// CHECK-RECOVER-STDOUT: Read first element:0
84+
85+
// Second error
86+
// CHECK-RECOVER-STDERR: AddressSanitizer: use-after-poison
87+
// CHECK-RECOVER-STDERR: #0 0x{{.+}} in main {{.*}}asan_recover.swift:[[@LINE+1]]
88+
print("Read second element:\(x.advanced(by: 1).pointee)")
89+
fflush(stdout)
90+
// CHECK-RECOVER-STDOUT: Read second element:0
91+
92+
__asan_unpoison_memory_region(UnsafeMutableRawPointer(x), size)
93+
94+
x.deallocate();
95+
// CHECK-NO-RECOVER-STDOUT-NOT: DONE
96+
// CHECK-RECOVER-STDOUT: DONE
97+
print("DONE")
98+
fflush(stdout)

0 commit comments

Comments
 (0)