-
Notifications
You must be signed in to change notification settings - Fork 112
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
Alter FailFast behaviour of memcpy #526
Conversation
This commit changes the codegen for error messages for failed memcpys. This no longer generates a stack frame and correctly tail calls the error messages generator. It also turns the error messages on in Release builds. This will lead to better adoption experience.
CMakeLists.txt
Outdated
@@ -211,16 +211,13 @@ endif() | |||
|
|||
target_compile_definitions(snmalloc INTERFACE $<$<BOOL:CONST_QUALIFIED_MALLOC_USABLE_SIZE>:MALLOC_USABLE_SIZE_QUALIFIER=const>) | |||
|
|||
# In debug and CI builds, link the backtrace library so that we can get stack | |||
# Link the backtrace library so that we can get stack |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidchisnall I am not sure if I should make this part of the change. It is required to get it to print stack traces, but systems could use core dumps or debuggers to get those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On non-glibc systems, the backtrace thing is in a separate library, so it's probably not a good idea to include it. In a non-debug build, you won't have the unwind tables and so it may be able to fall back to trying to follow chains of frame pointers and return addresses, but outside of AArch64 those aren't mandated to exist by the ABI, so there's no guarantee that the backtrace will actually contain anything useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we probably need to iterate on this a bit more. The original code was a set of tools that could be used to add bounds checks to multiple routines, with an example in memcpy
. The new version is specific to memcpy
and is not useable elsewhere, yet maintains the separation into generic and memcpy-specific files.
CMakeLists.txt
Outdated
@@ -211,16 +211,13 @@ endif() | |||
|
|||
target_compile_definitions(snmalloc INTERFACE $<$<BOOL:CONST_QUALIFIED_MALLOC_USABLE_SIZE>:MALLOC_USABLE_SIZE_QUALIFIER=const>) | |||
|
|||
# In debug and CI builds, link the backtrace library so that we can get stack | |||
# Link the backtrace library so that we can get stack |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On non-glibc systems, the backtrace thing is in a separate library, so it's probably not a good idea to include it. In a non-debug build, you won't have the unwind tables and so it may be able to fall back to trying to follow chains of frame pointers and return addresses, but outside of AArch64 those aren't mandated to exist by the ABI, so there's no guarantee that the backtrace will actually contain anything useful.
src/snmalloc/global/bounds_checks.h
Outdated
@@ -22,15 +22,17 @@ namespace snmalloc | |||
* error. With it set to false we print a helpful error message and then crash | |||
* the process. The process may be in an undefined state by the time the | |||
* check fails, so there are potentially security implications to turning this | |||
* off. It defaults to true for debug builds, false for release builds and | |||
* off. It defaults to false for both debug and release builds and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* off. It defaults to false for both debug and release builds and | |
* off. It defaults to false and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a slightly different rewrite, but thanks for pointing out the overcomplicated nature of what I had put.
src/snmalloc/global/bounds_checks.h
Outdated
* Note that this function never returns. We do not mark it [[NoReturn]] | ||
* so as to generate better code. The function claims to return a void*, | ||
* this is so it can be tail called in memcpy. Note [[NoReturn]] prevents | ||
* tailcails in GCC and Clang. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you file an LLVM bug about this? It's the exact opposite of what I'd have expected: you should always be able to tail call a noreturn function because nothing cares about the state of the stack above it and so you can completely corrupt any prior argument frame without affecting the program. This may only be possible with noexcept
functions, because I think noreturn functions can still throw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a known feature from what I understand:
https://stackoverflow.com/questions/55656938/why-noreturn-builtin-unreachable-prevents-tail-call-optimization
/** | ||
* A read bounds check, performed only when read checks are enabled. | ||
*/ | ||
Read, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like that this has gone away because I was using it in other functions. The goal for this header was to provide a set of tools that could be used for wrappers around other libc functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have moved the if constexpr
back inside, so you can now do
check_bounds<CheckReads>(...)
Is this acceptable? Could you elaborate on where you feel the CheckDirection
approach would be clearer?
It seems you have to pass two parameters
check_bounds<CheckDirection::Read, CheckReads>(...)
I have updated the comments and error message changes to not be memcpy
specific, and have added a FakeReturn
, so that different things could be tail called in case the function needs a different return type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The check needs two pieces of information:
- Is this a read or a write?
- Are we checking reads or writes?
I'm not sure how you encode that in a single template parameter.
I am using ifunc resolvers to instantiate the outer functions multiple times so that I get two versions:
- No checks
- Checks on writes
- Checks on reads and writes
If it's a wrapper, then I use the underlying function for the no-checks version. I can then switch between them based on an environment variable at process-launch time. As long as I can still do that concisely, I'm happy.
Making check_bounds
take two arguments meant that I didn't need any if constexpr
in the outer function, which simplifies my wrappers considerably because they just unconditionally forward their arguments. My ifunc resolver is, itself, generated from a templated function that takes a template template
parameter and instantiates it with the wrapper version to generate (read or write checks).
Most of the libc function wrappers can be a simple template that calls check_bounds<>
on any read/write pointer arguments and then forwards to the underlying implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So with a single template parameter I can do the following, which has no if constexpr
in the caller.
// Check the bounds of the arguments.
if (SNMALLOC_UNLIKELY(!check_bounds<Checked && ReadsChecked>(src, len)))
return report_fatal_bounds_error(
src, len, "memcpy with source out of bounds of heap allocation");
if (SNMALLOC_UNLIKELY(!check_bounds<Checked>(dst, len)))
return report_fatal_bounds_error(
dst, len, "memcpy with destination out of bounds of heap allocation");
I can push the SNMALLOC_UNLIKELY
inside the call, but it doesn't seem to bias the branches correctly.
It has to be two calls to get the good codegen, when we have error messages as we need to conditionally tail call the error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. I can always wrap the two calls in a macro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So a macro would work, but you will probably need two versions one for a void
returning function, and another that takes a type parameter for a not void function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of my favourite small parts of C++ is that you can return
an expression that evaluates to a void
type from a function with a void
return, so this should be possible to make generic.
When we're just doing this for memcpy, the increase in (source) code size in the checked function is fine. My intent with this code was that we'd be able to gradually instrument any libc (or external) function that takes pointer arguments with bounds checks. There's going to be a lot more code duplication for this in the new version than the old. |
This commit changes the codegen for error messages for failed memcpys.
This no longer generates a stack frame and correctly tail calls the
error messages generator.
It also turns the error messages on in Release builds. This will lead
to better adoption experience.