diff --git a/compiler-rt/test/hwasan/TestCases/use-after-scope-setjmp.cpp b/compiler-rt/test/hwasan/TestCases/use-after-scope-setjmp.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/hwasan/TestCases/use-after-scope-setjmp.cpp @@ -0,0 +1,59 @@ +// RUN: %clangxx_hwasan -mllvm -hwasan-use-stack-safety=0 -mllvm -hwasan-use-after-scope -O2 %s -o %t && \ +// RUN: %run %t 2>&1 + +// REQUIRES: aarch64-target-arch +// REQUIRES: stable-runtime + +#include +#include +#include +#include + +#include +#include + +volatile const char *stackbuf = nullptr; +jmp_buf jbuf; + +__attribute__((noinline)) bool jump() { + // Fool the compiler so it cannot deduce noreturn. + if (getpid() != 0) { + longjmp(jbuf, 1); + return true; + } + return false; +} + +bool target() { + switch (setjmp(jbuf)) { + case 1: + return false; + default: + break; + } + + while (true) { + char buf[4096]; + stackbuf = buf; + if (!jump()) { + break; + }; + } + return true; +} + +int main() { + target(); + + void *untagged = __hwasan_tag_pointer(stackbuf, 0); + if (stackbuf == untagged) { + // The buffer wasn't tagged in the first place, so the test will not work + // as expected. + return 2; + } + if (__hwasan_test_shadow(untagged, 4096) != -1) { + return 1; + } + + return 0; +} diff --git a/llvm/lib/Transforms/Instrumentation/HWAddressSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/HWAddressSanitizer.cpp --- a/llvm/lib/Transforms/Instrumentation/HWAddressSanitizer.cpp +++ b/llvm/lib/Transforms/Instrumentation/HWAddressSanitizer.cpp @@ -304,6 +304,7 @@ static bool isStandardLifetime(const AllocaInfo &AllocaInfo, const DominatorTree &DT); bool instrumentStack( + bool ShouldDetectUseAfterScope, MapVector &AllocasToInstrument, SmallVector &UnrecognizedLifetimes, DenseMap> &AllocaDbgMap, @@ -1359,6 +1360,7 @@ } bool HWAddressSanitizer::instrumentStack( + bool ShouldDetectUseAfterScope, MapVector &AllocasToInstrument, SmallVector &UnrecognizedLifetimes, DenseMap> &AllocaDbgMap, @@ -1410,7 +1412,7 @@ }; bool StandardLifetime = UnrecognizedLifetimes.empty() && isStandardLifetime(Info, GetDT()); - if (DetectUseAfterScope && StandardLifetime) { + if (ShouldDetectUseAfterScope && StandardLifetime) { IntrinsicInst *Start = Info.LifetimeStart[0]; IRB.SetInsertPoint(Start->getNextNode()); tagAlloca(IRB, AI, Tag, Size); @@ -1505,8 +1507,14 @@ SmallVector LandingPadVec; SmallVector UnrecognizedLifetimes; DenseMap> AllocaDbgMap; + bool CallsReturnTwice = false; for (auto &BB : F) { for (auto &Inst : BB) { + if (CallInst *CI = dyn_cast(&Inst)) { + if (CI->canReturnTwice()) { + CallsReturnTwice = true; + } + } if (InstrumentStack) { if (AllocaInst *AI = dyn_cast(&Inst)) { if (isInterestingAlloca(*AI)) @@ -1590,7 +1598,12 @@ if (!AllocasToInstrument.empty()) { Value *StackTag = ClGenerateTagsWithCalls ? nullptr : getStackBaseTag(EntryIRB); - instrumentStack(AllocasToInstrument, UnrecognizedLifetimes, AllocaDbgMap, + // Calls to functions that may return twice (e.g. setjmp) confuse the + // postdominator analysis, and will leave us to keep memory tagged after + // function return. Work around this by always untagging at every return + // statement if return_twice functions are called. + instrumentStack(DetectUseAfterScope && !CallsReturnTwice, + AllocasToInstrument, UnrecognizedLifetimes, AllocaDbgMap, RetVec, StackTag, GetDT, GetPDT); } // Pad and align each of the allocas that we instrumented to stop small diff --git a/llvm/test/Instrumentation/HWAddressSanitizer/use-after-scope-setjmp.ll b/llvm/test/Instrumentation/HWAddressSanitizer/use-after-scope-setjmp.ll new file mode 100644 --- /dev/null +++ b/llvm/test/Instrumentation/HWAddressSanitizer/use-after-scope-setjmp.ll @@ -0,0 +1,43 @@ +; RUN: opt -passes=hwasan -hwasan-use-stack-safety=0 -hwasan-use-after-scope -S < %s | FileCheck %s +target datalayout = "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128" +target triple = "aarch64-unknown-linux-android29" + +@stackbuf = dso_local local_unnamed_addr global i8* null, align 8 +@jbuf = dso_local global [32 x i64] zeroinitializer, align 8 + +declare void @may_jump() + +define dso_local noundef i1 @_Z6targetv() sanitize_hwaddress { +entry: + %buf = alloca [4096 x i8], align 1 + %call = call i32 @setjmp(i64* noundef getelementptr inbounds ([32 x i64], [32 x i64]* @jbuf, i64 0, i64 0)) + switch i32 %call, label %while.body [ + i32 1, label %return + i32 2, label %sw.bb1 + ] + +sw.bb1: ; preds = %entry + br label %return + +while.body: ; preds = %entry + %0 = getelementptr inbounds [4096 x i8], [4096 x i8]* %buf, i64 0, i64 0 + call void @llvm.lifetime.start.p0i8(i64 4096, i8* nonnull %0) #10 + store i8* %0, i8** @stackbuf, align 8 + ; may_jump may call longjmp, going back to the switch (and then the return), + ; bypassing the lifetime.end. This is why we need to untag on the return, + ; rather than the lifetime.end. + call void @may_jump() + call void @llvm.lifetime.end.p0i8(i64 4096, i8* nonnull %0) #10 + br label %return + +; CHECK-LABEL: return: +; CHECK: void @llvm.memset.p0i8.i64({{.*}}, i8 0, i64 256, i1 false) +return: ; preds = %entry, %while.body, %sw.bb1 + %retval.0 = phi i1 [ true, %while.body ], [ true, %sw.bb1 ], [ false, %entry ] + ret i1 %retval.0 +} + +declare i32 @setjmp(i64* noundef) returns_twice + +declare void @llvm.lifetime.start.p0i8(i64 immarg, i8* nocapture) +declare void @llvm.lifetime.end.p0i8(i64 immarg, i8* nocapture)