Cookies   I display ads to cover the expenses. See the privacy policy for more information. You can keep or reject the ads.

Video thumbnail
- So, welcome everybody.
So, today we're gonna talk about undefined behavior.
I will be your guide.
My name is Piotr Padlewski.
We're gonna talk about what is undefined behavior,
why it sucks and how to fight with,
like how to mitigate issue
and how to find undefined behavior,
and why do we actually need it?
- [Man] I thought it was awesome.
- Yeah, it is.
- [Man] But now you're saying it sucks.
- I mean you know, it sucks, but it's awesome.
(man mumbling)
So what is undefined behavior?
Basically, C++ said there's one restrictions
on the behavior on the program,
but when does it affect behavior of our program?
It only affects if you're actually execute
the thing or if the compiler knows that we would
actually execute it, okay.
So it's not like, so basically if we have
let's say a dead code, like a lug, let's say it fails
and we have some undefined behavior there,
then it should not affect behavior of our program.
The most important thing is that compilers treats
undefined behavior as a promise that these things
are not going to happen.
So what can really happen after hitting undefined behavior?
So you probably heard about this little creatures
that come through your nose and mess up with your code,
nasal demons,
but seriously, in theory everything could happen right?
The sender does not guarantee anything, but you know
the odds of formatting your hard drive are zero right?
It's like it never happens.
Who did format hard drive using undefined behavior?
No one right?
Of course I will get into the examples when
it actually happens.
So let's start up with some boring undefined behaviors.
By the way if you have any questions, or any comment
just like do it on the way.
It's better just to wait until the end.
So there are things like naming variables
starting with double underscore
or defining functions in namespace std
or specializing non-user defined types in namespace std.
So like translating it, it means for example
we can not specialize std hash of pair integer and integer.
If it will be like hash of pair of my type
and integer then it will be fine.
Another thing, we cannot take address of member function
from namespace std.
So this sounds like pretty weird,
like why the behavior of my program would be undefined
if I use some weird names from my variables right?
And there is a reason for that because
suppose that you define a micro and then
you would include std vector.
If you would, if the name of the micro would be the same
I don't know, as some parameter or like some part
of the name inside of the implementation of the vector,
then you might actually change the behavior
of how the vector works and in most of the cases
if you would actually do this then you would get
either linger or compiler,
but it's not guaranteed.
So this standard needs to have some sort of namespace where
you can put things and it can just guarantee that
this thing, this will work as we say
and you cannot just say okay if you'll do this,
do this, do this then it's not gonna work.
This is basically undefined behavior
and the same thing for namespace std,
like putting functions in namespace std.
So there might be some implementation
like some helper functions that implement some things
like we don't want to violate ODR and things like that
and it's also about being,
it is also about the new standards,
so if we would let's say move to C++ 20 and someone
will add a new member function like I don't know, someone
will add a new push back like it's,
like sender did in C++ 11,
then if you are sure you take address of push back
and multiple people did it using boost bind or something.
Then it won't go on a compile because now we have two
definitions right?
And this is because this is a (mumbling) because
like from the standard point of view,
I mean it was invalid to take this address.
So then sender can break things like that.
So how to mitigate these issues?
There are no good techniques, but it is not a huge problem
so no one wants to deal with this right?
It's like who uses ugly names like starting
with double underscore?
Only standard libraries.
So we could use for example clang tidy.
That would check for names but that's not a big deal.
So let's go some more interesting undefined behaviors
like calling main or integer overflow or using initialized
values or forgetting your original statement.
So what's up with calling main?
This program is totally valued in C,
but it has undefined behavior in C++
because you cannot take address of main
or call it or do anything like that
and the reason for that is that we don't to have
we don't want main to be recursive.
In other words, we don't want to,
we only want to have one call to main,
like there is only one start,
like just on the beginning of your
program main will be called
and the reason for that is let's say you have
multiple global objects and they have initializers.
So then by running those functions we can actually figure
out what will be the values of those global objects
and then forward it to the main, right?
So then we can do that across propagation,
but we couldn't do it if main could be called multiple times
because we wouldn't have this assumption.
That would be the values of the main, of these globals.
So overflows.
If we compiled this function then a smart compiler
can actually do a transformation like this
and this seems to be totally valid, right?
We have just by doing simple math
we always know that x plus one is greater than x,
but this is not the case if you have unassigned integer
because the unassigned overflow on the unassigned integer
is totally defined right?
We just get to do module and overflow with the assigned
integer is undefined.
So this means that compiler can look at it
and say okay if x plus one would overflow,
then this would be undefined behavior.
So there's no way that this could happen.
This is something that could not happen.
So I can assume it's not gonna happen,
so by doing this this means that x plus one
will be greater than x so I can just replace with true
or there are things like if we multiply and divide
we would also just forward it to x
and there are multiple bugs with this
because many people did check if the overflow occurred
just by doing a computation like this.
Let's say that you want to, someone passes you a size
and you want to allocate size plus because you want to put
I don't know, zero at the end, then some people checked if
actually if size plus one overflows and then
with abort or something and smart compiler can
just did the whole branch here right?
So there were multiple bugs like that,
but actually integer overflows is not very helpful
for that cases.
I don't think it gives a lot of performance from
leading branches like this because this is more about loops.
So if you take this kind of loop and we'll ask Java
what do you know about this loop?
Java compiler probably will not be able to tell you
that for example this loop will terminate
because the integer overflow is defined in Java.
So you cannot tell us the n could be the maximum value
of the integer so it could wrap
and go and go, right,
and it won't terminate it.
It will know that this loop will have n plus one steps
or that n is always greater than I,
but there is also something else
that is much more important.
So if you have some integer and we do some address
computations, like here we do a sub I, b sub I
and we are on this 64 bit architecture.
We have to compute it in 64 bits and basically
if this will be unassigned we'll need to have
some extra instruction code, zero extension
and it will be much slower especially if
this loop will be pretty hot.
So because we know the overflow cannot occur
then we can just wider it to 64 bits
and it will work.
So if all those cases you know vectorization
and unrolling is basically much simpler.
Probably for most of the cases it will be possible
with unassigned integers.
Note that here we use less than equals.
If it would be just less assigned,
then we also will be fine.
So how to mitigate those issues?
So UBsan can find overflow during runtime.
UBsan is undefined behavior say that there is
a runtime tool.
So basically it adds some checks to your code,
like for example for integer overflows
and when something bad occurred it just shows you
warning code.
It show you error during the runtime.
There is also flags to GCC and clang
like fwrapv which defines integer overflow.
So if not a fan of undefined behavior,
you can use this flag and compiler will not
perform optimizations based on the undefined behavior
from integer overflows and there's also ftrapv
which basically traps on every assigned integer overflow.
Sometimes warnings can also help.
So let's move to the initialized values.
So if you think that this function is a random
number generator then like seriously.
Because if you actually call this function
and optimizer will just see oh okay, I am just reading
initialized value, then it can translate it to this right.
So I don't see this as a random at all
and like many people, there are some cases in the office's
project or some big project where people used
initial values as input to random number generator right
and it works for some time, but then a new compiler came
and I see oh, that's funny thing here.
So I would just optimize the way
and it wasn't random at all.
How to mitigate those issues.
Pretty much warnings help a lot.
Static analysis can help.
UBsan I think can catch some of the cases
and there is also a tool called memory sanitizer
and this tool is designed to catch reading
of uninitialized values and for simple cases
if we just have a small integer,
it's pretty easy for a compiler to
find out that it was not set,
but if you have some very huge buffer,
then it is like you probably cannot do it
in the static like during your compilation.
You have to do it in your runtime.
So with that, let's try to answer some questions.
Like for example, I think that something is a good
candidate to be undefined behavior like in C++ sender
actually when it would be considered as a bug
and we would lose some performance
if we pulled in undefined behavior, right?
Because C++ is about performance.
Anyone agrees with that?
Okay, a few people.
I will to convince you.
So I will try to give you some real
reasons for undefined behavior.
For integer overflows, it's like back in the days
when C was there, some architectures did weird things
when there was assigned integer overflows.
It could trap.
It could behave as the CPUs that we use today
or it could saturate.
There is the different things and standard couldn't say
that this one thing will happen because some CPUs
might it will cause some CPUs to be slower
so it just didn't define and there are things like
initialized values and we're pretty much setting
up one variable is pretty fast like we just do
in Excel or something but if you have a long buffer
then it actually might be (mumbling).
So also this is why I think it's not defined
and the things like the different thing in null pointer.
If we, if you would like to define null pointed reference,
then we would actually have to check
just before the reference tic if it's a null pointer
and then raise an exception or something right?
Because someone architectures could implement it
in a better way like for example, if you read
from memory zero then there is a special page
that fails, things like that.
But basically senders couldn't just define it
because there might be platforms that wouldn't work
and for things like buffer overflow
and also we would have to check, to do a bound check
and it's also pretty much not possible with a pointers
because you might just get some pointer and you pretty much
don't know what's the bounds of
the memory that it points to, right?
So it's not that easy.
So let's move to some tasty undefined behaviors.
So here we have a nullptr dereference,
a buffer overflow, using pointer to object
of ended lifetime, a violating strict-aliasing
and const_cast in const.
So this null program contains (mumbling) here.
It contains a really simple bug.
Who sees that?
So, we have use after move pretty much, right?
We just move from p and then we dereference p
and unique pointer is totally defined.
The move constructor of unique pointer is defined
as setting a zero to the pointer
and this is actually not a thing for every container
like pseud vector.
If you move from vector, then they are only saying
that it is an invalid state, but it doesn't mean
that is as you would just create a new vector.
In order to use vector after moving,
you would have to actually call clear or something,
but this is not the case for the unique pointer.
So here we'll have the null pointer reference
and when you compiler with clang and run it,
you actually get something like this.
So let's state the fact.
My compiler is (mumbling), right?
It's just saw the undefined behavior in code
and deleted my whole code, right?
So let's see how it work.
So just after doing some inlining it's found out
that we are doing a null pointer reference.
So it put it something like code unreachable
which means that this path is not reachable, right?
And everything after unreachable is of course unreachable,
so we can remove that
and now we can just do some simple optimization
like p2 is not used, p is not used.
We don't need allocation.
Well you just delete the whole code.
And we were pretty lucky here.
It emitted a track, so our program did not suddenly close
and didn't say anything.
It just crashed.
So we were lucky at least for this one,
but there are some other things.
In that case we had just null pointer reference
and here we don't have an null pointer reference, right?
But if you compile it with GCC, you would see that
hmm okay, so we are referencing a pointer
and then we are checking if p is a null pointer.
So if p would be a null pointer that would mean
that the dereference was invalid,
like it was an undefined behavior.
So it cannot happen.
So it means it's full right?
So you can just delete the whole thing.
And there are many bugs like that.
For example, you could put that print f for something
as John showed yesterday to I don't know,
debug a value, but you forgot to put the if statement
and then because you just call print f and you loaded
the value, then all the checks for the null pointer
will be deleted after this
and you might say but you know not
many people write code like this, right?
And you are probably right.
Things like this happens mostly after inlining,
because if you have a function and we have check like that
then it might be a dead code on some branch on some call
site, but it doesn't mean it's dead code on every branch.
So this is also a reason why it's not that easy to
find things like that,
but wait, there is more because what is better?
Removing your code forward, right?
Who knows what I am talking about right now?
Any guesses?
(man mumbling)
Time travel.
So let's move the check above, right.
Well first he will check if p is a null pointer
and then we'll do some things and then we'll dereference.
So if the compiler will be smart
enough it can actually do this, okay.
So let me walk through how it could happen.
So we know that on every path we
are gonna go to the pointed reference.
So we might as well just copy it,
like hoist it to the branch
and then do else, right?
This is the same equivalent program
and then it could say that okay we have a check
if p is a null pointer and then we dereference that.
So this is unreachable, and then if this is unreachable
so it means that we cannot,
this part of the code cannot be reached which means
that this thing is also unreachable
and the whole branch is unreachable, okay.
So you know for me, it looks like that.
It seems to me that this kind of optimization that
does not play a big role here right?
It's, yeah.
But there's some more stuff.
You probably might have seen this example.
It's pretty popular a month ago or something.
So we have a function pointer
and we call it inside the main.
So this is a global thing so this is set to zero
and we also have a function set that sets
this pointer to evil function
and note that evil function
and set function is never called.
But there is one thing.
This function pointer is static.
This means that the visibility of this is only
this translation unit.
So this means that compiler can come up and say okay
I can see all the reads and writes to this pointer right?
And I know that this is set to zero in the first case
and here it will be undefined behavior
if I just read it from zero.
So there must be some way that this function pointer is set.
Like I don't know, some global initializer or something.
So it means I actually have to call this function right?
Because the function needs to be called somewhere
because it will be undefined behavior in other case.
As you can see now function set does not do anything.
It just returns and we just inlined
evil function to main right?
And it turns out for some benchmarks this is actually
pretty important optimization,
maybe not from the function pointers,
but if we see that we write to a global variable,
like the same concept all the time, yes?
- [Man] Would this still be undefined if set was static?
- Yeah, I mean pretty much the
same optimization will happen.
Yeah, I mean it doesn't matter if the set is.
I mean maybe compiler could figure out that is never called,
but it could do the same thing like anyways.
Yeah, so if there is a global variable that we said
is the same value all the time, then for some benchmark
it actually matters to find out that okay,
we are writing the same value so we can just
cross propagate it for every write.
Okay so that's some of the dereferencing in null pointer.
So first question is why the compiler
does not warn about it?
It is clearly able to find those
optimizations in performance
so why it does not tell us about this?
And one other thing is diagnostics are much
harder than optimizations because in diagnostics
you have to think about false positives.
So if you have this thing in anther function,
like just after inlining and finding out
this branch is dead,
this does not mean that this branch is dead
for every call set right?
So if we would just raise warning like we use some weird
things here or like the set function like this is that.
It will be invalid right,
because there might be some other function that
codes it with a p that is null pointer
and it will be to have died.
The other explanation is that there is compiler
architectures like a clang issues diagnostic
in the frontend and it is doing
optimization in the middle end
and in the middle end we lose
a lot of information about code
and it's like first it will be pretty hard to go back
and issue a warning about special line of code after being
a lot of optimization and also we have to do inlining
so we will have to keep track of what we did inline
and stuff like that.
So it is not really simple
and there are also some other compilers like MSVC
that actually issues diagnostics in the backend
and this is also pretty weird because depending
on what optimization level you are using,
you will get different warnings.
The compiler will figure out different things right
and other explanation is that we don't like to repeat
the computation because we might as well do
the same things in the frontend
and then do pretty much the same,
I'll use the same algorithms
to do optimization in the backend,
but it will take time.
How to mitigate dereferencing a null pointer.
Firstly, like try to not debug with optimization.
If something weird is happening to your code,
it might not make sense to debug your code with optimization
during run, right, because if the optimizer figures out
that there is some null pointer in your dereference
and it will just delete your whole code,
then we're gonna have a bad time debugging it.
So if you just don't use optimization,
you will be fine.
There are also some flags like Og
which is please optimizing it in a way that
it does not affect optimizations.
Sorry, it does not affect the debugging.
This sort of work.
I heard some complaints about,
like that it doesn't work all the cases very well,
but there is something like this.
I think clang is trying to have it as 01,
but there is not a lot of work going
on with this right now.
We can also use static analyzers.
So you know in my opinion, it would be very nice
to have some sort of flag saying that
please don't perform some weird optimization
that will be very hard to debug
or please don't perform very weird optimizations
that I don't care about.
Okay, so let's go to the return statement.
So in C++, when you forget the return statement,
then there is a path in your function
that leads to end of the function without return statement
and we have to return something
and this is undefined behavior
and the compiler can pretty much
just optimize it to this thing
because right now it just assumes that p is always true,
because if p is not true, then the path will
lead to undefined behavior which is undefined
so we can not think about it.
But pretty weird things can happen if we actually
don't have any return statement at all
because what clang will do for this one
it will emit some instructions and it forget
return statement at all.
You will not have any return in your functions,
like nothing,
and so if you actually call this function,
it will just start performing these instructions.
I didn't even know why it's ending this push.
Anyway it will start to do these instructions
and then it will just go on right?
It might find some other function or something else.
So this sounds like a fun.
So what if there will be some pre-word function
just after it by accident that is never called.
So we could get the (mumbling)
for this function just after my function
and if I would compile it on my machine,
this and I will call function foo,
this won't gonna work, it won't gonna delete my files.
Why is that like?
Does anyone knows?
Any guesses?
I'm using 64 bits.
(man mumbling)
- [Man] It's not a vine?
- Yes, so in 64 bits, like X64, the stack needs to
be aligned 16 bytes when we call the function
and we do a call to this system here.
So and then, yeah?
(man mumbling)
I mean it just crashes pretty much.
Probably the system, probably there might be things
that care about this.
There is no checks for this but
I don't know CPU may assume that I meant I don't know
and because of curse we are doing this one push,
it is only pushing eight bytes,
it is unaligned.
So we can fix that in a simple way,
because that's another function and if we compile these
two translation units, it will work on my machine.
Like if we call function foo,
it will just delete all my files.
I can then check that with the error, but.
Okay so to mitigate those issues.
It's like pretty much I don't know, read compiler warnings.
I cannot tell you more or less.
So the other thing is that it would be nice
if clang would not screw up with us like this right?
It's like it would be nice if it performs optimization like
this, it would I don't know emit a trap or something
that would stop the running of (mumbling), yes?
- [Woman] So is clang actually seeing this as an advantage?
Because I will freely admit, I've made this mistake.
I have forgotten the return statement.
It is painful to debug and it's not repeatable,
so they're thinking that's what you intended to do?
- So I also hate this bug and it's always
I and just debugging right.
Oh, there's some more.
It's like I don't care right,
and then what happens right?
So I think it makes sense for some cases where I don't know,
where you don't have this kind of,
you have some stuffs.
I don't know, you have a switch and there is some
unreachable (mumbling) or something,
then it makes sense to do some sort of optimization,
but yes I like, yeah?
- [Man] Isn't clang born by default if not all control
paths are turned (mumbling)?
- Yes, it do.
(man mumbling)
(men talking)
- I don't think so.
Maybe it changed, but at least
two versions ago or something.
(man mumbling)
Yeah, of course, of course, but not everyone is using
these kind of things right and yeah.
So let's move to some more interesting stuff
like buffer overflows.
So this simple code also has undefined behavior.
So we just have a function that should just,
it just checks if we have a value inside our table
and we have four elements and we go from I equals zero
to four and then we dereference right?
And because it has only four elements,
we actually go to fifth element
and that is undefined behavior in the last step.
Like someone should put less instead of less than equals.
So the compiler can come and see okay,
this is a constant count so I would just unroll it right
and then it says that oh wait, in the last if
there is undefined behavior, right
or I'm just going out of this code.
So this something that cannot happen.
I'm probably finished my working before it.
So the only way you could finish the work before
it is by finding the element right?
So we just return true.
So now we have a function that if carry with any integer,
pretty much our table consists
of every integer we can think of.
How to mitigate those issues.
So we have address sanitizer and we have valgrind.
Pretty much it's designed to find buffer overflows
and of course, this won't gonna help us if the compiler
will optimize the way right.
So another thing, if something pretty weird happens,
just spend a couple minutes
recompiling without optimizations
and then start the debugging.
And also static analyzer can find some sort of issues,
but of course it's, either way it's not that strong to do
the find in most of the cases.
So, let's talk about const.
So here I have some information of my vector or something
and I have function size that is const and I have
operator square bracket.
That is also const and of course const means
that I won't go and modify anything in my class right?
So it it legal to do this kind of thing right?
I'm just iterating over my things and I didn't modify
anything inside of the vector so the size shouldn't change,
so can I just hoist it and not call size every time?
- [Man] No.
No, size could return just a global variable.
- That's right and the size is like constants
does not actually say we cannot modify anything
inside of a class, right?
So this is invalid.
But it actually works with the standard vector.
So if we write something like this,
it will pretty much optimize to this kind of thing.
Any guesses why it works with standard vector?
- [Man] It sizes inline?
- Yeah, it's pretty much inline all the things,
but by magic of compilers it is able to figure out
that I just have those pointers,
the size is a subsection of this pointer from this pointer
and I do not modify those pointers so it is constant
and I can hoist it.
- [Man] I'm sorry, what was the problem with the first one?
Can you pull it up again?
The one where you said size before.
- This one?
- Yeah.
Okay, so we cannot,
okay compiler cannot transform this one to this one.
Like it could--
- [Man] Oh okay yeah.
The code is fine.
It just can't (mumbling). - Yes, yes.
And there's another thing.
If you actually write a follow up like this,
it'll pretty much do the same thing
because if the range based for loop actually
memorized the beginning and end.
So it does not call end all the time in the loop.
This also means that this transformation in your source code
may not be valid all the time right
because if you actually modify a vector,
if you push back to the vector inside of the for loop,
you cannot just use the range based for loop for the vector.
You could use it for a list or something,
but you also wouldn't get all the elements if
you were pushed back to the list.
Okay, so about const.
So it is pretty much illegal to do this kind of
transmission because the size could return to global
or even if it would return a member size or something,
we could still const_cast.
So const_cast on a const reference it is about
what the reference points to.
So if you have const reference or a const pointer
that points to some memory that does not occur as the const,
then it is legal to do a const_cast.
If we have a const reference, a variable that was
occur to the const, like let's say I have a const integer,
then this is undefined behavior to do const_cast, okay,
and the pointers and reference is about how I can
get from point a to point b.
It does not guard us from actually modifying the thing
that it points to even if it's a const reference.
So pretty much const does not help us with any
sort of optimization like this, right.
Maybe if we declare a variable as a const,
then it could help, but as I see, I think compilers
at least for a non-global variables does not perform
any major optimization there.
What's the solution to how to fix that?
Use link time optimization basically.
We don't actually need to have this guarantee in C++
if we do link time optimization, right,
because you might think about okay, what if
const would be actually stronger
and do with this kind of thing?
Right, so that is just introducing undefined behavior,
but we actually don't need to because we have the tools
to avoid this kind of issues and you might say okay,
but I do like I am someone who has a lot of RAM
and a lot of CPU power?
Link time optimization is so slow
that no one is using, like seriously.
But there are some other good things like ThinLTO
which is a new framework for scalable
and fast link time optimization in LVN.
There is also WHOPR in GCC and there is LTCG in MSVC.
So there are alternatives and it works pretty well.
Okay, so let's talk about the lifetime of the pointers.
This is slightly modified example from John Regehr blog.
So I have, I allocate memory for one integer
and then I reallocate this memory.
So this should return me the same pointer because
I didn't pass any bigger size, this is the same size.
So I can, realloc can throw me the same pointer,
but just to be sure I will check if p actually equal to q
and then I will write one to p,
I will write two to q and I will just print those two values
and if you compile with clang,
you'll get one and two.
So you know, I have those two
pointers pointing at the same value
and the value of one is one
and the value of the second is two.
It doesn't make any sense right?
This is like totally valid optimizations to do
because there is undefined behavior in our code
and undefined behavior happens when we dereference a p.
So here right, and this because if we reallocate
our memory using realloc or a placement new,
then the old pointer is pretty much dead.
You cannot use it.
It points to memory that is dead
and even if you have a pointer
that points to the same memory,
there is some magic going on there
and that says that pretty much
that pointer is invalid to use,
but this pointer is valid to use.
So we can use q but we cannot use p after doing realloc.
Is there any difference between virtual functions and
functions like virtual functions written in C?
Because you probably have some colleagues writing C code
and they're like I don't need C++.
I can write virtual functions by my own right?
But actually C++ virtual functions can be faster
because assuming we have the same semantics
of the virtual functions and we don't do some tricks
that C++ does not do, then the compiler can perform
more optimizations on C++ virtual functions.
And comes to the object's lifetime.
So if we have a virtual function
and we call the same function two times
does this really call the same function?
We'll probably expect this is the same function,
like what can happen there?
Is there any magic that can happen in the virtual code
that would change your viewpoint or something?
I'm like hmm, what about this.
So there is some magic called placement new
and this line is pretty much what it's doing is that
okay I will nuke myself, I will create an object that
I derived inside of myself and as long as derived
is the same size as base, this is valid.
Okay so this line is legal thing in C++.
You can write evil code like this and this is not fun
for me because I work under virtualization
and I have to care about cases like this.
But there is a good thing because there is object lifetime.
So we actually nuke ourselves with placement new
then the pointer that we have to
the old object is a zombie, right?
It's invalid use.
So this measures after this first call,
like if this function will be called
the second code will be undefined behavior.
Compilers does not use this fact right now.
There is implementation in clang with (mumbling)
table pointers that uses this fact
and you can do the virtualization like this,
but in general it's very hard to actually model a const
like this in a C program or in C++ programs and that's why
it's not still in the found on default
because there some weird counter
examples that's would miscompile.
How to mitigate this kind of issue?
So I mean nobody is nuking vpointer inside of the virtual
functions, but there are some other things like someone
could overwrite vpointer by a buff overflow or something
so there are things like control flow integrity
and this is a clang thing.
I think there is also another thing, MSVC.
So what CFI is doing is that for every indirect call
it checks if you actually called some function that is
some kind of valid.
At least if it's,
like if we don't just call some random memory or something.
There is allows UBsan that checks
if we're calling a function
that it just goes to the runtime tag information
and checks if the time information is the same
as my vpointer and if not, it just a runtime error.
Okay wrapping up, there are also some things
that are pretty weird in C++ because for some cases
C++ is saying this is totally defined
or it does not tell anything about some things.
So for example, there is no single mention
of a stack overflow inside of C++ standard, right?
This thing doesn't exist of for C++
and this is pretty much because it is not a language thing.
It is more about the platform
and about the way it works, right?
So it's like just standard does not care about it.
You can say that.
And there is also things like
throwing bad alloc when allocation fails.
So if you ask do you during Linux kernel please give me
64 gigs of memory having got only 60 gigs of memory.
It may give you a pointer, but that is not null
and it will not throw any exception or anything
and you're like okay, I have a pointer.
I just kept 64 gigs of memory.
So I'll just start reading or writing to it
and every time we're gonna hit a new page,
it's just gonna allocate a new page.
So we can have a lot of virtual memory,
but we might run out of it.
So if we're gonna just start writing to this memory,
at some point you're just gonna crash right?
So this does not seem like a thing that
we can catch by try except or something.
This is thing that totally is telling us we're gonna have a
bad alloc or null pointer or something,
but it actually doesn't work.
Okay, so wrapping up.
Pretty much undefined behavior is used to optimize code.
There is a bad thing because we actually don't know
what gains do we get from every undefined behavior.
We have some knowledge about some (mumbling),
there is like a couple of papers,
there's of course like knowledge in the compiler writers,
but there are many things that we don't actually know
if this makes sense to have as undefined behavior
and do we actually get anything, any performance
from this undefined behavior?
I totally think that there should
be for every undefined behavior,
there should be a tool that we could find it, right?
And I think clang is trying to do that a little bit.
So if they do some new optimization,
they are trying to make sure that there is a tool
that may actually find this sort of problem, right,
if we do optimization on an undefined behavior.
Of course it is not perfect.
We have those runtime tools like ASan, TSan, like UBSan,
things like that and some other flags,
but pretty much it is not complete, right?
We are getting there, but yeah,
it will probably take a lot of time.
So unfortunately, undefined behavior stays in C++.
So, thank you for your attention
and do you have any questions for me?
- [Man] Hi, thank you for your talk.
I want to share a particularly awesome undefined behavior
we had in our company.
We had a loop that would try to connect until
a certain amount of retries and the loop looked like this.
While p, end, end, retries minus minus, p equals connect.
That sounds fine.
Somewhere about 20 lines later or something like that
in a completely separate place, p was accessed.
Can you imagine what the undefined behavior logic did there?
- Wait, so there was like,
what is the end end means that one?
- [Man] While p, while not p.
I'm sorry, while not p meaning haven't connected yet.
End end retries minus minus meaning retries hasn't hit zero,
p equals connect.
So it would return null if it didn't connect
or it would return the thing if it did manage to connect
and you do retries five, four, three, two, one, zero
and then leave the loop.
So p was accessed 20 lines later.
- So you mean like you could a situation where it actually
didn't work, like the connection didn't occurred,
but you are showing dereference a pointer up there?
- [Man] But you still dereference a pointer.
So the compiler in its infinite wisdom,
removed the retry check.
It did not remove the p check
because that is the right thing.
That is not what you would expect.
You have a dereference of a p and the undefined behavior
deleted your retry check,
so that is I think a particularly nice.
- Yes, (mumbling).
- [Man] An awesome qualifying behavior.
- Okay, anymore questions?
I unfortunately do not have any back up slides.
(man mumbling)
(all laughing)
Okay, so thank you. - Thank you.