This is an archive of the discontinued LLVM Phabricator instance.

[TSan] Make sure we only collect non-TSan frames for memory operations r=dvyukov,rsundahl,thetruestblue,wrotki,kubamracek!
ClosedPublic

Authored by yln on Mar 16 2023, 4:11 PM.

Details

Summary

A previous change [1] moved retrieval of the caller PC
(__builtin_return_address(0) via CALLERPC) from an
interface-boundary function into a shared helper function
ExternalAccess. If this function does not get inlined, we fail to
collect the appropriate caller PC for the "TSan interface boundary".

[1] https://reviews.llvm.org/D32360

Diff Detail

Event Timeline

yln created this revision.Mar 16 2023, 4:11 PM
Herald added a project: Restricted Project. · View Herald TranscriptMar 16 2023, 4:11 PM
Herald added a subscriber: Enna1. · View Herald Transcript
yln requested review of this revision.Mar 16 2023, 4:11 PM
Herald added a project: Restricted Project. · View Herald TranscriptMar 16 2023, 4:11 PM
Herald added a subscriber: Restricted Project. · View Herald Transcript
yln added inline comments.Mar 16 2023, 4:24 PM
compiler-rt/lib/tsan/rtl/tsan_external.cpp
62

Another potential solution would have been marking this function with ALWAYS_INLINE plus a comment explaining that we need it to ensure CALLERPC does the right thing. I chose the current solution since:

  • It makes things explicit
  • AFAIK, [no]inline attributes aren't guaranteed, but only hints; the compiler might discard them
dvyukov accepted this revision.Mar 17 2023, 1:57 AM
dvyukov added inline comments.
compiler-rt/lib/tsan/rtl/tsan_external.cpp
71

Not related to the current change.
Not sure why we do all of FuncEntry/InsertShadowStackFrameForTag/FuncExit, when we don't handle the memory access itself. I think it only has any effect if we handle the memory access which can lead to a race report now or in future.

kubamracek accepted this revision.Mar 17 2023, 11:05 AM
This revision is now accepted and ready to land.Mar 17 2023, 11:05 AM
yln added inline comments.Mar 17 2023, 11:54 AM
compiler-rt/lib/tsan/rtl/tsan_external.cpp
71

This is the change that added handling for the external tag: https://reviews.llvm.org/D32382

To make the TSan external API work with Swift and other use cases, we need to track "tags" for individual memory accesses. Since there is no space to store this information in shadow cells, let's use the thread traces for that. This patch stores the tag as an extra frame in the stack traces (by calling FuncEntry and FuncExit with the address of a registered tag), this extra frame is then stripped before printing the backtrace to stderr.

Not sure why we do all of FuncEntry/InsertShadowStackFrameForTag/FuncExit, when we don't handle the memory access itself. I think it only has any effect if we handle the memory access which can lead to a race report now or in future.

I don't know the answer to this question. @kubamracek, do you remember?

//   - __tsan_external_read/__tsan_external_write annotates the logical reads
//       and writes of the object at the specified address. 'caller_pc' should
//       be the PC of the library user, which the library can obtain with e.g.
//       `__builtin_return_address(0)`.

https://github.com/llvm/llvm-project/blob/4c106cfdf7cf7eec861ad3983a3dd9a9e8f3a8ae/compiler-rt/include/sanitizer/tsan_interface.h#L129

My understanding of of the handling of the caller PC is this. The caller of __tsan_external_[write]read can decide who to "blame" for the memory operation. User code would want to say "myself"; library code would want to say "my caller":

  • Calls from user code should provide caller_pc=nullptr so compiler-rt retrieves the caller pc (Swift compiler uses this)
  • Library code can retrieve it's caller and then pass it in

So this means we execute the following sequence when we are called from user code (caller_pc=nullptr):

InsertShadowStackFrameForTag(thr, (uptr)tag);
MemoryAccess(thr, tsan_caller_pc, (uptr)addr, 1, typ);
FuncExit(thr);

For calls from library code (caller_pc=<user code addr>):

FuncEntry(thr, caller_pc);
InsertShadowStackFrameForTag(thr, (uptr)tag);
if (... || !libignore()->IsIgnored(caller_pc, &in_ignored_lib))    // <user code addr> not in ignored
    MemoryAccess(thr, tsan_caller_pc, (uptr)addr, 1, typ);
FuncExit(thr);
FuncExit(thr);

So I think the FuncEntry/Exit are superfluous in case we are calling from an ignored module into an annotated library that wants to attribute the access to the calling user code. I am also not sure why we always use tsan_caller_pc even if we have a valid, "not ignored" user address for caller_pc.

I think the function could look like this:

void ExternalAccess(void *addr, uptr caller_pc, uptr tsan_caller_pc, void *tag,
                    AccessType typ) {
  CHECK_LT(tag, atomic_load(&used_tags, memory_order_relaxed));
  bool in_ignored_lib;
  if (caller_pc && libignore()->IsIgnored(caller_pc, &in_ignored_lib))
    return;  // <-- early return in case we don't do a memory access check

  ThreadState *thr = cur_thread();
  if (caller_pc) FuncEntry(thr, caller_pc);
  InsertShadowStackFrameForTag(thr, (uptr)tag);
  MemoryAccess(thr, (caller_pc ? caller_pc : tsan_caller_pc), (uptr)addr, 1, typ);  // <-- use caller_pc if we have it to remain consistent
  FuncExit(thr);
  if (caller_pc) FuncExit(thr);
}

@dvyukov, what do you think? (as a follow-up)

This revision was landed with ongoing or failed builds.Mar 17 2023, 5:40 PM
This revision was automatically updated to reflect the committed changes.

what do you think? (as a follow-up)

Makes sense.