diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp index 7fa6942029816..9aefcddb934cc 100644 --- a/clang/lib/CodeGen/BackendUtil.cpp +++ b/clang/lib/CodeGen/BackendUtil.cpp @@ -991,12 +991,17 @@ void EmitAssemblyHelper::RunOptimizationPipeline( FPM.addPass(BoundsCheckingPass()); }); - if (LangOpts.Sanitize.has(SanitizerKind::Realtime)) + if (LangOpts.Sanitize.has(SanitizerKind::Realtime)) { + PB.registerLoopOptimizerEndEPCallback( + [](LoopPassManager &LPM, OptimizationLevel Level) { + LPM.addPass(RealtimeSanitizerLoopPass()); + }); PB.registerScalarOptimizerLateEPCallback( [](FunctionPassManager &FPM, OptimizationLevel Level) { RealtimeSanitizerOptions Opts; FPM.addPass(RealtimeSanitizerPass(Opts)); }); + } // Don't add sanitizers if we are here from ThinLTO PostLink. That already // done on PreLink stage. diff --git a/compiler-rt/test/rtsan/bound_loop.cpp b/compiler-rt/test/rtsan/bound_loop.cpp new file mode 100644 index 0000000000000..e57e0ef7eb628 --- /dev/null +++ b/compiler-rt/test/rtsan/bound_loop.cpp @@ -0,0 +1,72 @@ +// RUN: %clangxx -fsanitize=realtime %s -o %t -O3 +// RUN: %run %t 2>&1 | FileCheck %s --allow-empty +// RUN: %clangxx -fsanitize=realtime %s -o %t -O2 +// RUN: %run %t 2>&1 | FileCheck %s --allow-empty +// RUN: %clangxx -fsanitize=realtime %s -o %t -O1 +// RUN: %run %t 2>&1 | FileCheck %s --allow-empty + +// RUN: %clangxx -fsanitize=realtime %s -o %t -O0 +// RUN: %run %t 2>&1 | FileCheck %s --allow-empty + +// UNSUPPORTED: ios + +// Intent: Ensure basic bound audio loops don't trigger rtsan. + +#include +#include + +#include + +void FillBufferInterleaved(int *buffer, int sample_count, int channel_count) + [[clang::nonblocking]] { + for (int sample = 0; sample < sample_count; sample++) + for (int channel = 0; channel < channel_count; channel++) + buffer[sample * channel_count + channel] = sample; +} + +void Deinterleave(int *buffer, int sample_count, int channel_count, + int *scratch_buffer) [[clang::nonblocking]] { + for (int channel = 0; channel < channel_count; channel++) + for (int sample = 0; sample < sample_count; sample++) { + int interleaved_index = sample * channel_count + channel; + int deinterleaved_index = channel * sample_count + sample; + scratch_buffer[deinterleaved_index] = buffer[interleaved_index]; + } + + for (int i = 0; i < sample_count * channel_count; i++) + buffer[i] = scratch_buffer[i]; +} + +int main() { + const int sample_count = 10; + const int channel_count = 2; + int buffer[channel_count * sample_count]; + + FillBufferInterleaved(buffer, sample_count, channel_count); + + assert(buffer[0] == 0); + assert(buffer[1] == 0); + assert(buffer[2] == 1); + assert(buffer[3] == 1); + + assert(buffer[18] == 9); + assert(buffer[19] == 9); + + int scratch_buffer[channel_count * sample_count]; + + Deinterleave(buffer, sample_count, channel_count, scratch_buffer); + + assert(buffer[0] == 0); + assert(buffer[1] == 1); + assert(buffer[8] == 8); + assert(buffer[9] == 9); + + assert(buffer[10] == 0); + assert(buffer[11] == 1); + assert(buffer[18] == 8); + assert(buffer[19] == 9); + + return 0; +} + +// CHECK-NOT: {{.*Real-time violation.*}} diff --git a/compiler-rt/test/rtsan/infinite_loop.cpp b/compiler-rt/test/rtsan/infinite_loop.cpp new file mode 100644 index 0000000000000..8689533510150 --- /dev/null +++ b/compiler-rt/test/rtsan/infinite_loop.cpp @@ -0,0 +1,20 @@ +// RUN: %clangxx -fsanitize=realtime %s -o %t +// RUN: not %run %t 2>&1 | FileCheck %s +// UNSUPPORTED: ios + +// Intent: Ensure we detect infinite loops + +#include +#include + +void infinity() [[clang::nonblocking]] { + while (true) + ; +} + +int main() { + infinity(); + return 0; + // CHECK: {{.*Real-time violation.*}} + // CHECK: {{.*infinity*}} +} diff --git a/compiler-rt/test/rtsan/spinlock.cpp b/compiler-rt/test/rtsan/spinlock.cpp new file mode 100644 index 0000000000000..26451524ec42c --- /dev/null +++ b/compiler-rt/test/rtsan/spinlock.cpp @@ -0,0 +1,56 @@ +// RUN: %clangxx -DCAS_SPINLOCK -fsanitize=realtime %s -o %t +// RUN: not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-ALL --check-prefix=CHECK-CAS +// RUN: %clangxx -DTEST_AND_SET_SPINLOCK -fsanitize=realtime %s -o %t +// RUN: not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-ALL --check-prefix=CHECK-TEST-AND-SET +// UNSUPPORTED: ios + +#include + +#include + +class SpinLockTestAndSet { +public: + void lock() { + while (lock_flag.test_and_set(std::memory_order_acquire)) { + // Busy-wait (spin) until the lock is acquired + } + } + + void unlock() { lock_flag.clear(std::memory_order_release); } + +private: + std::atomic_flag lock_flag = ATOMIC_FLAG_INIT; +}; + +class SpinLockCompareExchange { +public: + void lock() { + bool expected = false; + while (!lock_flag.compare_exchange_weak( + expected, true, std::memory_order_acquire, std::memory_order_relaxed)) { + } + } + + void unlock() { lock_flag.store(false, std::memory_order_release); } + +private: + std::atomic lock_flag{false}; +}; + +int lock_violation() [[clang::nonblocking]] { +#if defined(TEST_AND_SET_SPINLOCK) + SpinLockTestAndSet lock; +#elif defined(CAS_SPINLOCK) + SpinLockCompareExchange lock; +#else +# error "No spinlock defined" +#endif + lock.lock(); + return 0; +} + +int main() [[clang::nonblocking]] { lock_violation(); } + +// CHECK-ALL: {{.*Real-time violation.*}} +// CHECK-CAS: {{.*SpinLockCompareExchange::lock.*}} +// CHECK-TEST-AND-SET: {{.*SpinLockTestAndSet::lock.*}} diff --git a/compiler-rt/test/rtsan/unbound_loop.cpp b/compiler-rt/test/rtsan/unbound_loop.cpp new file mode 100644 index 0000000000000..49e84a98f45f9 --- /dev/null +++ b/compiler-rt/test/rtsan/unbound_loop.cpp @@ -0,0 +1,48 @@ +// RUN: %clangxx -fsanitize=realtime %s -o %t -O3 +// RUN: not %run %t 2>&1 | FileCheck %s --allow-empty +// RUN: %clangxx -fsanitize=realtime %s -o %t -O2 +// RUN: not %run %t 2>&1 | FileCheck %s --allow-empty +// RUN: %clangxx -fsanitize=realtime %s -o %t -O1 +// RUN: not %run %t 2>&1 | FileCheck %s --allow-empty + +// RUN: %clangxx -fsanitize=realtime %s -o %t -O0 +// RUN: not %run %t 2>&1 | FileCheck %s --allow-empty + +// UNSUPPORTED: ios + +// Intent: Ensure basic bound audio loops don't trigger rtsan. + +#include +#include + +#include + +void BadScalarEvolution(int *buffer, int sample_count, int channel_count) + [[clang::nonblocking]] { + + int sample = 0; + while (sample < sample_count) { + int channel = 0; + while (channel < channel_count) { + buffer[sample * channel_count + channel] = sample; + channel++; + } + sample++; + + // NOTE! Here is the "bug" that causes the loop to be unbounded. + sample_count++; + } +} + +int main() { + const int sample_count = 10; + const int channel_count = 2; + int buffer[channel_count * sample_count]; + + BadScalarEvolution(buffer, sample_count, channel_count); + + return 0; +} + +// CHECK: {{.*Real-time violation.*}} +// CHECK-NEXT {{.*BadScalarEvolution.*}} diff --git a/llvm/include/llvm/Transforms/Instrumentation/RealtimeSanitizer.h b/llvm/include/llvm/Transforms/Instrumentation/RealtimeSanitizer.h index f2ce1636551ce..04fdc40edbd28 100644 --- a/llvm/include/llvm/Transforms/Instrumentation/RealtimeSanitizer.h +++ b/llvm/include/llvm/Transforms/Instrumentation/RealtimeSanitizer.h @@ -20,6 +20,7 @@ #define LLVM_TRANSFORMS_INSTRUMENTATION_REALTIMESANITIZER_H #include "llvm/IR/PassManager.h" +#include "llvm/Transforms/Scalar/LoopPassManager.h" namespace llvm { @@ -33,6 +34,12 @@ class RealtimeSanitizerPass : public PassInfoMixin { static bool isRequired() { return true; } }; +struct RealtimeSanitizerLoopPass : PassInfoMixin { + PreservedAnalyses run(Loop &L, LoopAnalysisManager &AM, + LoopStandardAnalysisResults &AR, LPMUpdater &U); + static bool isRequired() { return true; } +}; + } // namespace llvm #endif // LLVM_TRANSFORMS_INSTRUMENTATION_REALTIMESANITIZER_H diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def index 4f5f680a6e953..80427697d0158 100644 --- a/llvm/lib/Passes/PassRegistry.def +++ b/llvm/lib/Passes/PassRegistry.def @@ -659,6 +659,7 @@ LOOP_PASS("print", DDGAnalysisPrinterPass(dbgs())) LOOP_PASS("print", IVUsersPrinterPass(dbgs())) LOOP_PASS("print", LoopCachePrinterPass(dbgs())) LOOP_PASS("print", LoopNestPrinterPass(dbgs())) +LOOP_PASS("sanitize-unbound-loops", RealtimeSanitizerLoopPass()) #undef LOOP_PASS #ifndef LOOP_PASS_WITH_PARAMS diff --git a/llvm/lib/Transforms/Instrumentation/RealtimeSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/RealtimeSanitizer.cpp index 7854cf4f2c625..28efdfa77ec79 100644 --- a/llvm/lib/Transforms/Instrumentation/RealtimeSanitizer.cpp +++ b/llvm/lib/Transforms/Instrumentation/RealtimeSanitizer.cpp @@ -13,36 +13,49 @@ // //===----------------------------------------------------------------------===// -#include "llvm/IR/Analysis.h" +#include "llvm/Analysis/LoopInfo.h" +#include "llvm/Analysis/ScalarEvolution.h" +#include "llvm/Demangle/Demangle.h" +#include "llvm/IR/Function.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/Module.h" +#include "llvm/Support/raw_ostream.h" #include "llvm/Transforms/Instrumentation/RealtimeSanitizer.h" using namespace llvm; -static void insertCallBeforeInstruction(Function &Fn, Instruction &Instruction, - const char *FunctionName) { - LLVMContext &Context = Fn.getContext(); - FunctionType *FuncType = FunctionType::get(Type::getVoidTy(Context), false); +static void insertCallBeforeInstruction(Function &CallingFn, + IRBuilder<> &Builder, + const char *FunctionName, + ArrayRef FunctionArgs) { + std::vector FunctionArgTypes; + FunctionArgTypes.reserve(FunctionArgs.size()); + for (Value *Arg : FunctionArgs) + FunctionArgTypes.push_back(Arg->getType()); + + FunctionType *FuncType = FunctionType::get( + Type::getVoidTy(CallingFn.getContext()), FunctionArgTypes, false); FunctionCallee Func = - Fn.getParent()->getOrInsertFunction(FunctionName, FuncType); - IRBuilder<> Builder{&Instruction}; - Builder.CreateCall(Func, {}); + CallingFn.getParent()->getOrInsertFunction(FunctionName, FuncType); + Builder.CreateCall(Func, FunctionArgs); } static void insertCallAtFunctionEntryPoint(Function &Fn, const char *InsertFnName) { - insertCallBeforeInstruction(Fn, Fn.front().front(), InsertFnName); + IRBuilder<> Builder{&Fn.front().front()}; + insertCallBeforeInstruction(Fn, Builder, InsertFnName, std::nullopt); } static void insertCallAtAllFunctionExitPoints(Function &Fn, const char *InsertFnName) { for (auto &BB : Fn) for (auto &I : BB) - if (isa(&I)) - insertCallBeforeInstruction(Fn, I, InsertFnName); + if (isa(&I)) { + IRBuilder<> Builder{&I}; + insertCallBeforeInstruction(Fn, Builder, InsertFnName, std::nullopt); + } } RealtimeSanitizerPass::RealtimeSanitizerPass( @@ -61,3 +74,43 @@ PreservedAnalyses RealtimeSanitizerPass::run(Function &F, return PreservedAnalyses::all(); } + +PreservedAnalyses +RealtimeSanitizerLoopPass::run(Loop &L, LoopAnalysisManager &AM, + LoopStandardAnalysisResults &AR, LPMUpdater &U) { + BasicBlock *Context = + L.getLoopPreheader() ? L.getLoopPreheader() : L.getHeader(); + assert(Context && "Loop has no preheader or header block"); + + Function *F = Context->getParent(); + assert(F && "Loop has no parent function"); + + const bool HasNoExits = L.hasNoExitBlocks(); + const bool CannotPredictLoopCount = + isa(AR.SE.getConstantMaxBackedgeTakenCount(&L)) || + isa(AR.SE.getBackedgeTakenCount(&L)); + const bool LoopIsPotentiallyUnbound = HasNoExits || CannotPredictLoopCount; + + if (LoopIsPotentiallyUnbound) { + IRBuilder<> Builder{&Context->back()}; + + std::string ReasonStr = + demangle(F->getName().str()) + " contains a possibly unbounded loop "; + + if (HasNoExits) + ReasonStr += "(reason: no exit blocks)."; + else if (CannotPredictLoopCount) + ReasonStr += "(reason: backedge taken count cannot be computed)."; + else + assert(false); + + Value *Reason = Builder.CreateGlobalStringPtr(ReasonStr); + insertCallBeforeInstruction(*F, Builder, "__rtsan_expect_not_realtime", + {Reason}); + + // TODO: What is preserved here?? + return PreservedAnalyses::none(); + } + + return PreservedAnalyses::all(); +} diff --git a/llvm/test/Instrumentation/RealtimeSanitizer/rtsan_bound_loop.ll b/llvm/test/Instrumentation/RealtimeSanitizer/rtsan_bound_loop.ll new file mode 100644 index 0000000000000..ac2c8e3ffeed6 --- /dev/null +++ b/llvm/test/Instrumentation/RealtimeSanitizer/rtsan_bound_loop.ll @@ -0,0 +1,43 @@ +; RUN: opt < %s -passes='sroa,sanitize-unbound-loops' -S | FileCheck %s + + +define void @procces(ptr noundef %buffer, i32 noundef %size) #0 { +entry: + %buffer.addr = alloca ptr, align 8 + %size.addr = alloca i32, align 4 + %i = alloca i32, align 4 + store ptr %buffer, ptr %buffer.addr, align 8 + store i32 %size, ptr %size.addr, align 4 + store i32 0, ptr %i, align 4 + br label %for.cond + +for.cond: ; preds = %for.inc, %entry + %0 = load i32, ptr %i, align 4 + %1 = load i32, ptr %size.addr, align 4 + %cmp = icmp slt i32 %0, %1 + br i1 %cmp, label %for.body, label %for.end + +for.body: ; preds = %for.cond + %2 = load i32, ptr %i, align 4 + %conv = sitofp i32 %2 to float + %3 = load ptr, ptr %buffer.addr, align 8 + %4 = load i32, ptr %i, align 4 + %idxprom = sext i32 %4 to i64 + %arrayidx = getelementptr inbounds float, ptr %3, i64 %idxprom + store float %conv, ptr %arrayidx, align 4 + br label %for.inc + +for.inc: ; preds = %for.body + %5 = load i32, ptr %i, align 4 + %inc = add nsw i32 %5, 1 + store i32 %inc, ptr %i, align 4 + br label %for.cond + +for.end: ; preds = %for.cond + ret void +} + +attributes #0 = { sanitize_realtime } + +; In this simple loop, we should not insert rtsan_expect_not_realtime +; CHECK-NOT: call{{.*}}@__rtsan_expect_not_realtime diff --git a/llvm/test/Instrumentation/RealtimeSanitizer/rtsan_cas_spinlock.ll b/llvm/test/Instrumentation/RealtimeSanitizer/rtsan_cas_spinlock.ll new file mode 100644 index 0000000000000..76058b7a57f73 --- /dev/null +++ b/llvm/test/Instrumentation/RealtimeSanitizer/rtsan_cas_spinlock.ll @@ -0,0 +1,28 @@ +; RUN: opt < %s -passes='sanitize-unbound-loops' -S | FileCheck %s + +%class.SpinLockTestAndSet = type { %"struct.std::__1::atomic_flag" } +%"struct.std::__1::atomic_flag" = type { %"struct.std::__1::__cxx_atomic_impl" } +%"struct.std::__1::__cxx_atomic_impl" = type { %"struct.std::__1::__cxx_atomic_base_impl" } +%"struct.std::__1::__cxx_atomic_base_impl" = type { i8 } + +define noundef i32 @main() local_unnamed_addr #0 { +entry: + %spinlock = alloca %class.SpinLockTestAndSet, align 1 + call void @llvm.lifetime.start.p0(i64 1, ptr nonnull %spinlock) + store i8 0, ptr %spinlock, align 1 + br label %while.cond.i + +while.cond.i: ; preds = %while.cond.i, %entry + %0 = atomicrmw xchg ptr %spinlock, i8 1 acquire, align 1 + %extract.t2.i.i = trunc i8 %0 to i1 + br i1 %extract.t2.i.i, label %while.cond.i, label %SpinlockTestAndSet.exit + +SpinlockTestAndSet.exit: ; preds = %while.cond.i + store atomic i8 0, ptr %spinlock release, align 1 + call void @llvm.lifetime.end.p0(i64 1, ptr nonnull %spinlock) + ret i32 0 +} + +attributes #0 = { sanitize_realtime } + +; CHECK: call{{.*}}@__rtsan_expect_not_realtime diff --git a/llvm/test/Instrumentation/RealtimeSanitizer/rtsan_infinite_loop.ll b/llvm/test/Instrumentation/RealtimeSanitizer/rtsan_infinite_loop.ll new file mode 100644 index 0000000000000..e542e9f1c0d5f --- /dev/null +++ b/llvm/test/Instrumentation/RealtimeSanitizer/rtsan_infinite_loop.ll @@ -0,0 +1,14 @@ +; RUN: opt < %s -passes='sanitize-unbound-loops' -S | FileCheck %s + +define void @process() #0 { +entry: + br label %while.body + +while.body: ; preds = %entry, %while.body + br label %while.body +} + +attributes #0 = { sanitize_realtime } + +; CHECK: call{{.*}}@__rtsan_expect_not_realtime +; CHECK-NEXT: br label %while.body