Skip to content

[rtsan] Introduce function-name-matches suppression #112108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 16, 2024

Conversation

cjappl
Copy link
Contributor

@cjappl cjappl commented Oct 12, 2024

Introduces a new types of suppression:

  1. function-name-matches - allows users to disable malloc, free, pthread_mutex_lock or similar. This could be helpful if a user thinks these are real-time safe on their OS. Also allows disabling of any function marked [[blocking]].

This is useful as a more performant "early outs" compared to the call-stack-contains suppression. call-stack-contains is inherently VERY costly, needing to inspect every frame of every stack for a matching string. This new suppression has an early out before we unwind the stack.

@llvmbot
Copy link
Member

llvmbot commented Oct 12, 2024

@llvm/pr-subscribers-compiler-rt-sanitizer

Author: Chris Apple (cjappl)

Changes

Introduces two new types of suppressions:

  1. intercepted-fn-name - allows users to disable malloc, free, pthread_mutex_lock or similar. This could be helpful if a user thinks these are real-time safe on their OS.
  2. blocking-fn-name - allows users to disable a function by name if it is marked [[clang::blocking]]. This could be useful when a user is interacting with a third party library, needing to disable a warning.

On top of this, these are useful as more performant "early outs" compared to the call-stack-contains suppression. call-stack-contains is inherently VERY costly, needing to inspect every frame of every stack for a matching string. These suppressions early out before we unwind the stack.


Full diff: https://github.com/llvm/llvm-project/pull/112108.diff

6 Files Affected:

  • (modified) compiler-rt/lib/rtsan/rtsan_assertions.h (+6)
  • (modified) compiler-rt/lib/rtsan/rtsan_checks.inc (+2)
  • (modified) compiler-rt/lib/rtsan/rtsan_suppressions.cpp (+22)
  • (modified) compiler-rt/lib/rtsan/rtsan_suppressions.h (+2)
  • (modified) compiler-rt/test/rtsan/stack_suppressions.cpp (+13-5)
  • (modified) compiler-rt/test/rtsan/stack_suppressions.cpp.supp (+4-2)
diff --git a/compiler-rt/lib/rtsan/rtsan_assertions.h b/compiler-rt/lib/rtsan/rtsan_assertions.h
index 8183a8202478ff..2e7cb8ffc729a7 100644
--- a/compiler-rt/lib/rtsan/rtsan_assertions.h
+++ b/compiler-rt/lib/rtsan/rtsan_assertions.h
@@ -28,6 +28,12 @@ void ExpectNotRealtime(Context &context, const DiagnosticsInfo &info,
   if (context.InRealtimeContext() && !context.IsBypassed()) {
     ScopedBypass sb{context};
 
+    if ((info.type == DiagnosticsInfoType::InterceptedCall &&
+         IsInterceptorSuppressed(info.func_name)) ||
+        (info.type == DiagnosticsInfoType::BlockingCall &&
+         IsBlockingFnSuppressed(info.func_name)))
+      return;
+
     __sanitizer::BufferedStackTrace stack;
 
     // We use the unwind_on_fatal flag here because of precedent with other
diff --git a/compiler-rt/lib/rtsan/rtsan_checks.inc b/compiler-rt/lib/rtsan/rtsan_checks.inc
index f5f23e044bd5d7..d9639fda7d9816 100644
--- a/compiler-rt/lib/rtsan/rtsan_checks.inc
+++ b/compiler-rt/lib/rtsan/rtsan_checks.inc
@@ -17,3 +17,5 @@
 // SummaryKind should be a string literal.
 
 RTSAN_CHECK(CallStackContains, "call-stack-contains")
+RTSAN_CHECK(InterceptedFnName, "intercepted-fn-name")
+RTSAN_CHECK(BlockingFnName, "blocking-fn-name")
diff --git a/compiler-rt/lib/rtsan/rtsan_suppressions.cpp b/compiler-rt/lib/rtsan/rtsan_suppressions.cpp
index c5051dd1910236..a356f97517568d 100644
--- a/compiler-rt/lib/rtsan/rtsan_suppressions.cpp
+++ b/compiler-rt/lib/rtsan/rtsan_suppressions.cpp
@@ -92,3 +92,25 @@ bool __rtsan::IsStackTraceSuppressed(const StackTrace &stack) {
   }
   return false;
 }
+
+static bool IsFunctionSuppressed(const char *interceptor_name,
+                                 const char *flag_name) {
+  if (suppression_ctx == nullptr)
+    return false;
+
+  if (!suppression_ctx->HasSuppressionType(flag_name))
+    return false;
+
+  Suppression *s;
+  return suppression_ctx->Match(interceptor_name, flag_name, &s);
+}
+
+bool __rtsan::IsInterceptorSuppressed(const char *interceptor_name) {
+  return IsFunctionSuppressed(
+      interceptor_name, ConvertTypeToFlagName(ErrorType::InterceptedFnName));
+}
+
+bool __rtsan::IsBlockingFnSuppressed(const char *blocking_fn_name) {
+  return IsFunctionSuppressed(blocking_fn_name,
+                              ConvertTypeToFlagName(ErrorType::BlockingFnName));
+}
diff --git a/compiler-rt/lib/rtsan/rtsan_suppressions.h b/compiler-rt/lib/rtsan/rtsan_suppressions.h
index 45545f8c0e0b65..e2997c6e2e755b 100644
--- a/compiler-rt/lib/rtsan/rtsan_suppressions.h
+++ b/compiler-rt/lib/rtsan/rtsan_suppressions.h
@@ -18,5 +18,7 @@ namespace __rtsan {
 
 void InitializeSuppressions();
 bool IsStackTraceSuppressed(const __sanitizer::StackTrace &stack);
+bool IsInterceptorSuppressed(const char *interceptor_name);
+bool IsBlockingFnSuppressed(const char *blocking_fn_name);
 
 } // namespace __rtsan
diff --git a/compiler-rt/test/rtsan/stack_suppressions.cpp b/compiler-rt/test/rtsan/stack_suppressions.cpp
index 2aceedbb313b11..024b1e75fda806 100644
--- a/compiler-rt/test/rtsan/stack_suppressions.cpp
+++ b/compiler-rt/test/rtsan/stack_suppressions.cpp
@@ -1,4 +1,5 @@
 // RUN: %clangxx -fsanitize=realtime %s -o %t
+// RUN: %env_rtsan_opts=halt_on_error=false %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-NOSUPPRESSIONS
 // RUN: %env_rtsan_opts=suppressions='%s.supp' not %run %t 2>&1 | FileCheck %s
 // UNSUPPORTED: ios
 
@@ -22,13 +23,14 @@ void VectorViolations() {
   v.reserve(10);
 }
 
-void BlockFunc() [[clang::blocking]] { usleep(1); }
+void BlockFunc() [[clang::blocking]] { /* do something blocking */
+}
 
 void *process() [[clang::nonblocking]] {
-  void *ptr = MallocViolation();
-  VectorViolations();
-  BlockFunc();
-  free(ptr);
+  void *ptr = MallocViolation(); // Suppressed call-stack-contains
+  VectorViolations();            // Suppressed call-stack-contains with regex
+  BlockFunc();                   // Suppressed blocking-fn-name
+  free(ptr);                     // Suppressed intercepted-fn-name
 
   // This is the one that should abort the program
   // Everything else is suppressed
@@ -51,3 +53,9 @@ int main() {
 // CHECK-NOT: vector
 // CHECK-NOT: free
 // CHECK-NOT: BlockFunc
+
+// CHECK-NOSUPPRESSIONS: malloc
+// CHECK-NOSUPPRESSIONS: vector
+// CHECK-NOSUPPRESSIONS: free
+// CHECK-NOSUPPRESSIONS: BlockFunc
+// CHECK-NOSUPPRESSIONS: usleep
diff --git a/compiler-rt/test/rtsan/stack_suppressions.cpp.supp b/compiler-rt/test/rtsan/stack_suppressions.cpp.supp
index bec4db259a3e0e..6640f5690776d2 100644
--- a/compiler-rt/test/rtsan/stack_suppressions.cpp.supp
+++ b/compiler-rt/test/rtsan/stack_suppressions.cpp.supp
@@ -1,4 +1,6 @@
 call-stack-contains:MallocViolation
 call-stack-contains:std::*vector
-call-stack-contains:free
-call-stack-contains:BlockFunc
+
+intercepted-fn-name:free
+
+blocking-fn-name:BlockFunc

@@ -1,4 +1,5 @@
// RUN: %clangxx -fsanitize=realtime %s -o %t
// RUN: %env_rtsan_opts=halt_on_error=false %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-NOSUPPRESSIONS
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this check here to make sure that these would actually cause errors before suppressing them

@@ -17,3 +17,5 @@
// SummaryKind should be a string literal.

RTSAN_CHECK(CallStackContains, "call-stack-contains")
RTSAN_CHECK(InterceptedFnName, "intercepted-fn-name")
RTSAN_CHECK(BlockingFnName, "blocking-fn-name")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any feedback on the names?

Copy link

github-actions bot commented Oct 12, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@fmayer
Copy link
Contributor

fmayer commented Oct 14, 2024

High level question: why do we distinguish whether it's intercepted or blocking? Does the user care?

@cjappl
Copy link
Contributor Author

cjappl commented Oct 14, 2024

High level question: why do we distinguish whether it's intercepted or blocking? Does the user care?

Good question. I think the answer is "yes, the user cares".

A lot of this complexity (why we distinguish between intercepted and blocking) is to show the user a different error message:

For malloc, an intercepted call:

==51105==ERROR: RealtimeSanitizer: unsafe-library-call
Intercepted call to real-time unsafe function `malloc` in real-time context!

For a user defined [[clang::blocking]] call:

==51326==ERROR: RealtimeSanitizer: blocking-call
Call to blocking function `my_special_allocation()` in real-time context!

I think this is useful because it indicates whether or not this is directly controllable by code outside of glibc. If I get the second, I will know that some dev on my team, or library, marked this call as [[blocking]], and I should perhaps interrogate that source code to see if I agree with the attribute.

To simplify our code a lot, we could move to a unified error message, and treat both issues as the same, something like:

==51326==ERROR: RealtimeSanitizer: unsafe-call
Call to non-real-time-safe function `my_special_allocation()` in real-time context!
...
==51326==ERROR: RealtimeSanitizer: unsafe-call
Call to non-real-time-safe function `malloc()` in real-time context!

But I think that we might lose some important detail in the process. The user may behave differently based on if it is an intercepted function vs a [[blocking]] function.

We have discussed a bit if we want to have some notification of an unbound loop. While the details of this (or if it is even possible) are outside the scope of this comment, again I think it would be nice to be notified specifically of what went wrong, instead of a general error message:

==51326==ERROR: RealtimeSanitizer: unbound-loop
Potentially unbound loop called from real-time context!

WDYT?

@fmayer
Copy link
Contributor

fmayer commented Oct 14, 2024

High level question: why do we distinguish whether it's intercepted or blocking? Does the user care?

Good question. I think the answer is "yes, the user cares".

A lot of this complexity (why we distinguish between intercepted and blocking) is to show the user a different error message:

For malloc, an intercepted call:

==51105==ERROR: RealtimeSanitizer: unsafe-library-call
Intercepted call to real-time unsafe function `malloc` in real-time context!

For a user defined [[clang::blocking]] call:

==51326==ERROR: RealtimeSanitizer: blocking-call
Call to blocking function `my_special_allocation()` in real-time context!

I think this is useful because it indicates whether or not this is directly controllable by code outside of glibc. If I get the second, I will know that some dev on my team, or library, marked this call as [[blocking]], and I should perhaps interrogate that source code to see if I agree with the attribute.

To simplify our code a lot, we could move to a unified error message, and treat both issues as the same, something like:

==51326==ERROR: RealtimeSanitizer: unsafe-call
Call to non-real-time-safe function `my_special_allocation()` in real-time context!
...
==51326==ERROR: RealtimeSanitizer: unsafe-call
Call to non-real-time-safe function `malloc()` in real-time context!

But I think that we might lose some important detail in the process. The user may behave differently based on if it is an intercepted function vs a [[blocking]] function.

We have discussed a bit if we want to have some notification of an unbound loop. While the details of this (or if it is even possible) are outside the scope of this comment, again I think it would be nice to be notified specifically of what went wrong, instead of a general error message:

==51326==ERROR: RealtimeSanitizer: unbound-loop
Potentially unbound loop called from real-time context!

WDYT?

Right, I meant more in the context of this CL though. If I add a list the ignore list, why do I care if it's blocking or intercepted? I understand it might be useful to note why a function is considered "rt unsafe" in the error mesage.

@cjappl
Copy link
Contributor Author

cjappl commented Oct 14, 2024

Right, I meant more in the context of this CL though. If I add a list the ignore list, why do I care if it's blocking or intercepted?

Good question. @davidtrevelyan any thoughts? We could possibly change this to function-name, which would encapsulate both. I'm not opposed to this.

@davidtrevelyan
Copy link
Contributor

davidtrevelyan commented Oct 16, 2024

Right, I meant more in the context of this CL though. If I add a list the ignore list, why do I care if it's blocking or intercepted?

Good question. @davidtrevelyan any thoughts? We could possibly change this to function-name, which would encapsulate both. I'm not opposed to this.

Agreed - I think a single function-name suppressor would suffice - nice suggestion! I spent a while trying to think of a counter example and nothing came to mind.

@@ -17,3 +17,4 @@
// SummaryKind should be a string literal.

RTSAN_CHECK(CallStackContains, "call-stack-contains")
RTSAN_CHECK(FunctionNameIs, "function-name-is")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review the new name @davidtrevelyan and @fmayer - I chose one with a verb at the end to mirror call-stack-contains.

Other candidates I considered:

  • unsafe-function-name-is
  • function-name
  • function-name-matches

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like function-name-is if the intention is only to allow users to provide an exact function name match, character for character. But I think that would be difficult to use - how is the user supposed to reason about namespaces, mangling, parentheses on the end, overloads, etc.? I think at the minute I prefer function-name-matches with regex supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool that works for me. At least as written, we use the suppression built-in simple regexes (which I think is good, and we should keep it). I'll wait to see what fmayer says.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function-name-matches SGTM if it's a regex / glob

@cjappl cjappl requested a review from davidtrevelyan October 16, 2024 14:04
@cjappl cjappl changed the title [rtsan] Introduce blocking-fn-name and intercepted-fn-name suppressions [rtsan] Introduce function-name-is suppression Oct 16, 2024
@cjappl cjappl changed the title [rtsan] Introduce function-name-is suppression [rtsan] Introduce function-name-matches suppression Oct 16, 2024
@cjappl
Copy link
Contributor Author

cjappl commented Oct 16, 2024

I'm going to submit this when tests are green, as I think the three of us who have chatted about this are in good agreement. Happy to do anything else as post-commit

@cjappl cjappl merged commit 1efa662 into llvm:main Oct 16, 2024
7 checks passed
@cjappl cjappl deleted the function_blocking_suppressions branch October 16, 2024 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants