[llvm-dev] [RFC] Design of a TBAA sanitizer

Hal Finkel via llvm-dev llvm-dev at lists.llvm.org
Tue Apr 18 16:24:47 PDT 2017


Hi again,

While not using the compressed shadow representation discussed here, 
mostly because it can't handle the struct-path (offset) information in 
our TBAA, I've posted patches for an initial TBAA sanitizer implementation:

https://reviews.llvm.org/D32197 (Runtime)
https://reviews.llvm.org/D32198 (LLVM)
https://reviews.llvm.org/D32199 (Clang)

Please try it out / review / etc.

  -Hal

On 04/04/2017 03:13 PM, Hal Finkel wrote:
> Hi everyone,
>
> At EuroLLVM, Chandler and I chatted about the design for a potential 
> TBAA sanitizer. Here's my attempt to summarize:
>
> C/C++ have type-based aliasing rules, and LLVM's optimizer can exploit 
> these given TBAA metadata added by Clang. Roughly, a pointer of given 
> type cannot be used to access an object of a different type (with, of 
> course, certain exceptions). Unfortunately, there's a lot of code in 
> the wild that violates these rules (e.g. for type punning), and such 
> code often must be built with -fno-strict-aliasing. Performance is 
> often sacrificed as a result. Part of the problem is the difficulty of 
> finding TBAA violations. A sanitizer would help.
>
> A design goal of a TBAA sanitizer is to limit the shadow-memory 
> overhead of the implementation. ASan, for example, uses 1 bit per 
> byte. Here we're hoping to keep the overhead down to 2 bits per byte 
> for the TBAA sanitizing. We might be able to do this, while handling 
> all common types on the fast path, if we use both alignment and type 
> information. When accessing data of B bytes, 2*B bits of shadow memory 
> can be used. Thus, we'll get 2 bits for a one-byte type, 4 bits for a 
> two-byte type, etc. Moreover, we need appropriate holes in the 
> encoding space so that no type has a shadow encoding that overlaps 
> with an aligned part of a larger type's encoding.
> For example, we need to detect:
>
>   double f = ...; return *(int*) &f; // We should catch this.
>
> We might use the following encoding. The idea is that the common case, 
> for which we need a reasonable fast path, is that type types are 
> exactly equal. For this case, we want a simple comparison of the 
> shadow type encodings to be sufficient to validate the access. For 
> cases where the encodings don't match (and isn't zero to indicate an 
> unknown type), or for which there is no direct encoding for the access 
> type, a slow path must be used. All of this assumes that we're 
> validating the the pointer alignment first, and then checking the type 
> encodings.
>
> 1 Byte:
> 00 = 0  = unknown type
> 01 = 1 = hole
> 10 = 2 = hole
> 11 = 3  = all one-byte types (slow path, see note later on this)
>
> 2 Bytes:
> 0000 = 0 = unknown type
> 0101 = 5 = short
> 0110 = 6 = hole (A)
> 0111 = 7 = wchar_t (under some ABIs)
> 1001 = 9 = hole (B)
> 1010 = 10 = hole (C)
> 1011 = 11 = char16_t
> 1111 = 15 = all other types (slow path)
>
> It is important here to have wchar_t have a direct encoding here 
> because wchar_t is two bytes on Windows, and moreover, wchar_t is very 
> commonly used on Windows. The partial encoding overlap of wchar_t 
> (i.e. 0111) with the 11 one-byte-type encoding works because 11 always 
> indicates a slow-path check.
>
> 4 Bytes:
> 0000 0000 = 0 = unknown type
> A A = int
> A B = float
> B A  = pointer (under some ABIs)
> B B = long (under some ABIs)
> A 1111 = wchar_t (under some ABIs)
> B 1111 = char32_t
> A C = hole (D)
> C A = hole (E)
> B C = hole (F)
> C B = hole (G)
> C C = hole (H)
> 1111 1111 = 255 = all other types (slow path)
>
> 8 Bytes:
> 0000 0000 0000 0000 = 0 = unknown type
> D D = double
> D E = long (under some ABIs)
> E D = long long (under some ABIs)
> E E = long double (under some ABIs)
> D F = pointer (under some ABIs)
> F D = hole (I)
> E F = hole (J)
> F E = hole
> F F = hole
> ...
> 1111 1111 1111 1111 = 65535 = all other types
>
> 16 Bytes:
> 0 = unknown type
> | | = __int128_t
> I J = long long (under some ABIs)
> J I = long double (under some ABIs)
> J J = hole
> ...
> -1 = all other types
>
> For pointers, this scheme would consider all pointers to be the same 
> (regardless of pointee type). Doing otherwise would mostly requiring 
> putting pointer-type checking on the slow path (i.e. access via a 
> pointer pointer), and that could add considerable overhead. We might, 
> however, split out function pointers from other pointers. We could 
> provide a compile-time option to control the granularity of 
> pointer-type checks.
>
> Builtin vector types for which the vector element type has a direct 
> encoding also naturally have a direct encoding (the concatenation of 
> the encoding for the element type).
>
> Obviously the fact that we have no fast-path encodings for one-byte 
> types could be problematic. Note however that:
>
>  1. If a larger type is being used to access a smaller type (plus 
> more), the encodings won't match, so we always end up on the slow path.
>
>  2. If the access type is a one-byte type, we would want to validate 
> quickly. However, most common one-byte types are universally aliasing 
> (i.e. not subject to TBAA violations). Specifically, for C/C++, 
> pointers to char, unsigned char, signed char (C only), and std::byte, 
> can be used to access any part of any type. That leaves signed char 
> (C++ only), bool/_Bool, and enums with a [signed/unsigned] char base 
> type (C++ only, std::byte exempted) as pointee types we might wish to 
> validate. We'd always need to fall back to the slow path to validate 
> these. We could provide a compile-time option to disable such one-byte 
> access checking if necessary.
>
> How would the slow path work? First, the code needs to find the 
> beginning of the allocation. It can do this by scanning backwards in 
> the ASan shadow memory. Once located, we'll read a pointer to a 
> type-description structure from that "red zone" location. For dynamic 
> allocations, ASan's allocator will ensure such a space for the pointer 
> exists. For static allocations and globals, the compiler will ensure 
> it exists. The compiler will make sure that all constructors locate 
> this field and fill it in. Destructors can clear it. If two of these 
> type-description-structure pointers are equal, then we can conclude 
> that the types are equal. If not, then we need to interpret the 
> structure. The pointer itself might be to an interval map (to deal 
> with arrays, placement new, etc. - we can use the low bit of the 
> pointer to differentiate between an actual type-description structure 
> and an interval map), and the leaves of the interval map point to 
> actual type-description structures. The type-description structure is 
> an array of (offset, type) pairs, where the type field is also a 
> type-description-structure pointer. The type-description structures 
> themselves are comdat globals emitted in each relevant translation 
> unit, where the comdat key is formed using the mangled type name (and 
> size, etc.), and pointers to these symbols are then used to identify 
> the types.
>
> Thoughts?
>
> Thanks again,
> Hal
>

-- 
Hal Finkel
Lead, Compiler Technology and Programming Languages
Leadership Computing Facility
Argonne National Laboratory



More information about the llvm-dev mailing list