[cfe-dev] [RFC] Introducing a byte type to LLVM

George Mitenkov via cfe-dev cfe-dev at lists.llvm.org
Fri Jun 4 09:35:03 PDT 2021


Hi Johannes,

Sure! The underlying problem is that raw-memory access handlers are treated
as integers, while they are not really integers. Especially std::byte that
specifically
states that it has raw-memory access semantics. This semantic mismatch can
make
AA wrong and a pointer to escape.

Consider the following LLVM IR that copies a pointer:

%src8 = bitcast i8** %src to i8*

%dst8 = bitcast i8** %dst to i8*

call void @llvm.memcpy.p0i8.p0i8.i32(i8* %dst8, i8* %src8, i32 8, i1 false)

%load = load i8*, i8** %dst

%addr = ptrtoint i8* %load to i64

ret i64 %addr


If we optimize the call to memcpy, then the IR becomes


%src64 = bitcast i8** %src to i64*

%dst64 = bitcast i8** %dst to i64*

%addr = load i64, i64* %src64, align 1

store i64 %addr, i64* %dst64, align 1

ret i64 %addr


Since there is no "data" type in LLVM like a byte, the ptrtoint is
optimized out and the

pointer escapes. If we have used a pair of byte load/store that would not
happen.

The mentioned bug is just an example, but this pattern can be

replicated in other cases that optimize memcpy as integer load/stores and
drop ptrtoint,

undermining alias analysis. To illustrate further, consider loading a
pointer like:


%v1 = load i8*, %p

%v2 = load i8*, %p


Instcombine optimizes this into


%v1 = load i64, %p

%v2 = inttoptr %v1


changing the underlying provenance (again, not good for AA). If we had a
byte type instead,

then we could

1. preserve provenance

2. differentiate between integers and the raw data copied


This might be beneficial for some backends (e.g CHERI) that want to
differentiate between

true integers and bytes that can be integers. The optimizations for known
integers

can become more aggressive in this case.


Thanks,

George

On Fri, Jun 4, 2021 at 7:03 PM Johannes Doerfert <johannesdoerfert at gmail.com>
wrote:

> Hi George,
>
> On 6/4/21 10:24 AM, George Mitenkov via cfe-dev wrote:
> > Hi all,
> >
> > Together with Nuno Lopes and Juneyoung Lee we propose to add a new byte
> > type to LLVM to fix miscompilations due to load type punning. Please see
> > the proposal below. It would be great to hear the
> > feedback/comments/suggestions!
> >
> >
> > Motivation
> > ==========
> >
> > char and unsigned char are considered to be universal holders in C. They
> > can access raw memory and are used to implement memcpy. i8 is the LLVM’s
> > counterpart but it does not have such semantics, which is also not
> > desirable as it would disable many optimizations.
> >
> > We therefore propose to introduce a new byte type that would have a
> raw-memory
> > access semantics and can be differentiated from i8. This can help to fix
> > unsound optimizations and make the lowering of char, unsigned char or
> > std::byte correct. Currently, we denote the byte type as b<N>, where N is
> > the number of bits.
> >
> > In our semantics, byte type carries provenance and copying bytes does not
> > escape pointers, thereby benefiting alias analysis. If the memory
> contains
> > poison bits, then the result of loading a byte from it is bitwise poison.
>
> Could you elaborate on the motivation a bit. I'm not sure I follow.
> Maybe examples would be good. The only one I found is in
> https://bugs.llvm.org/show_bug.cgi?id=37469. For that one I'm
> wondering how much we would in reality loose if we make the required
> ptr2int explicit when people cast a pointer to an integer. In general,
> explicit encoding seems to me preferable and overall less work (new
> types kinda scare me TBH).
>
> ~ Johannes
>
>
> >
> >
> > Benefits
> > ========
> >
> > The small calls to memcpy and memmove that are transformed by Instcombine
> > into integer load/store pairs would be correctly transformed into the
> > loads/stores of the new byte type no matter what underlying value is
> being
> > copied (integral value or a pointer value).
> >
> >     1.
> >
> >     The new byte type solves the problem of copying the padded data,
> with no
> >     contamination of the loaded value due to poison bits of the padding.
> >     2.
> >
> >     When copying pointers as bytes implicit pointer-to-integer casts are
> >     avoided.The current setting of performing a memcpy using i8s leads to
> >     miscompilations (example: bug report 37469
> >     <https://bugs.llvm.org/show_bug.cgi?id=37469>) and is bad for alias
> >     analysis.
> >
> >
> >
> > Lowering of char, unsigned char and std::byte
> >
> > ======================================
> >
> > For any function that takes char, unsigned char or std::byte as
> arguments,
> > we lower these to the b8 instead of i8. The motivation comes from the
> fact
> > that any pointer can be explicitly copied in bytes, and each byte can be
> > passed as an argument to the copying function. Since a copy like that can
> > escape a pointer, we need to generate the byte type to avoid the issue.
> >
> > Example
> >
> > void foo(unsigned char arg1, char arg2) {...}
> >
> >              =>
> >
> > void @foo(zeroext b8 %arg1, signext b8 %arg2) {...}
> >
> >
> >
> > std::byte is defined as enum class byte : unsigned char {} and therefore
> > all bitwise operations over it can be lowered equivalently to operations
> on
> > unsigned char (operation is performed over zero-extended to i32 operands,
> > and the result is truncated back).
> >
> >
> > ==============================
> >
> > Byte Type Semantics (Version 1)
> >
> > ==============================
> >
> >
> > [allowed] alloca/load/store
> >
> > =====================
> >
> > The byte type is allowed to be allocated, stored and loaded. No semantic
> > changes needed.
> >
> > [allowed] zext/sext/trunc
> >
> > ====================
> >
> > In order to easily adapt the current replacement of i8 with b8, we want
> to
> > extend zext/sext/trunc instructions’ semantics to allow the first operand
> > as a byte type as well. This will reduce the number of instructions
> needed
> > for the cast.
> >
> > Example
> >
> > trunc i32 %int to b8
> >
> > zext i8 %char to b32
> >
> > [modified] bitcast
> >
> > ==============
> >
> > We modify the semantics of the bitcast to allow casts from and to the
> byte
> > types. If the byte has a value of the same kind (integral or a pointer)
> as
> > the other type, then the bitcast is a noop. Otherwise, the result is
> poison.
> >
> > Example
> >
> > bitcast i<N> %val to b<N>
> >
> > bitcast i<N>* %val to b64
> >
> > bitcast b<N> %byte to i<N>     [byte has an integral value => noop]
> >
> >
> >
> > [new] bytecast
> >
> > ============
> >
> > During IR generation, we cannot deduce whether the byte value contains a
> > pointer or an integral value, and therefore we cannot create a bitcast.
> >
> > Example
> >
> > int @cast(char c) { return (int) c; }
> >
> > Current
> >
> > Proposed
> >
> > i32 @cast(i8 signext %c) {
> >
> >    %1 = alloca i8, align 1
> >
> >    store i8 %c, i8* %1, align 1
> >
> >    %2 = load i8, i8* %1, align 1
> >
> >    %3 = sext i8 %2 to i32
> >
> >    ret i32 %3
> >
> > }
> >
> > i32 @cast(b8 %c) {
> >
> >    %1 = alloca b8
> >
> >    store b8 %c, b8* %11
> >
> >    %2 = load b8, b8* %1
> >
> >    %3 =    ?    b8 %2 to i32
> >
> >    ret i32 %3
> >
> > }
> >
> > In this example, the operation  ?  can be sext (b8 is i8) or can be
> > ptrtoint if the byte has a pointer value.
> >
> > We therefore introduce a new bytecast instruction. The frontend will
> always
> > produce a bytecast, which can then be optimized into a more specific cast
> > if necessary. Bytecast operands must have the same bitwidth (or size).
> >
> > The semantics of bytecast instruction is:
> >
> > bytecast b<N> %byte to T
> >
> > The kind of the value of the byte
> >
> > T
> >
> > Semantics
> >
> > Pointer
> >
> > Integral
> >
> > ptrtoint
> >
> > Pointer
> >
> > Pointer
> >
> > bitcast
> >
> > Integral
> >
> > Integral
> >
> > integral casts (zext, sext, etc.)
> >
> > Integral
> >
> > Pointer
> >
> > inttoptr
> >
> >
> >
> > Essentially, this instruction is a selection of the right casts. Once the
> > compiler has been able to prove the kind of the byte’s value, the
> bytecast
> > can be replaced with appropriate cast or become a noop.
> >
> > Example
> >
> > bytecast b64 %bytes to i32*
> >
> > bytecast b8 %byte to i8
> >
> >
> >
> > [disabled] and/or/xor
> >
> > =================
> >
> > We do not allow bitwise operations over byte types because it’s not
> trivial
> > to define all cases, particularly the ones involving pointers. If these
> > operations are useful for optimizations and/or some frontends, semantics
> > for these can be considered separately.
> >
> > If the operation is desired on the frontend level, then the default
> > generated code can always cast the byte type to an integer to operate on
> > integer values.
> >
> >
> >
> > [disabled] arithmetic
> >
> > =================
> >
> > Performing arithmetic operations over the byte type is similarly not
> > allowed (A valid question is what does it mean to add to bytes of raw
> > memory?). If we want to perform arithmetic, we need to cast a byte to an
> > integer (via sext/zext/trunc explicitly).
> >
> >
> >
> > [modified] comparison
> >
> > ==================
> >
> > We allow performing comparisons, as we may potentially want to compare
> the
> > ordering of the memory instances, check for null, etc. Comparison is also
> > needed since char types are commonly compared. We define the following
> > semantic of the byte type comparison.
> >
> > Case 1: The values of the bytes have the same kinds: compare the values.
> >
> > Case 2: The values of the bytes have the different kinds: do a bytecast
> of
> > the non-integral type to the other type and compare the integral values.
> >
> >
> > Example
> >
> > =======
> >
> > unsigned char sum(unsigned char a, unsigned char b) { return a + b; }
> >
> > Current
> >
> > Proposed
> >
> > zeroext i8 @sum(i8 zeroext %a, i8 zeroext %b) {
> >
> >    %1 = alloca i8
> >
> >    %2 = alloca i8
> >
> >    store i8 %a, i8* %1
> >
> >    store i8 %b, i8* %2
> >
> >    %3 = load i8, i8* %1
> >
> >    %4 = zext i8 %3 to i32
> >
> >    %5 = load i8, i8* %2
> >
> >    %6 = zext i8 %5 to i32
> >
> >    %7 = add nsw i32 %4, %6
> >
> >    %8 = trunc i32 %7 to i8
> >
> >    ret i8 %8
> >
> > }
> >
> > zeroext b8 @sum(b8 zeroext %a, b8 zeroext %b) {
> >
> >    %1 = alloca b8
> >
> >    %2 = alloca b8
> >
> >    store b8 %a, b8* %1
> >
> >    store b8 %b, b8* %2
> >
> >    %3 = load b8, b8* %1
> >
> >    %4 = bytecast b8 %3 to i8
> >
> >    %5 = zext i8 %4 to i32
> >
> >    %6 = load b8, b8* %2
> >
> >    %7 = bytecast b8 %6 to i8
> >
> >    %8 = zext i8 %7 to i32
> >
> >    %9 = add nsw i32 %5, %8
> >
> >    %10 = trunc i32 %9 to b8
> >
> >    ret b8 %10
> >
> > }
> >
> >
> >
> >
> >
> > Thanks,
> >
> > George
> >
> >
> > _______________________________________________
> > cfe-dev mailing list
> > cfe-dev at lists.llvm.org
> > https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-dev
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/cfe-dev/attachments/20210604/e98e435a/attachment-0001.html>


More information about the cfe-dev mailing list