Skip to content
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

Merged
merged 5 commits into from
May 19, 2022
Merged

Conversation

mjp41
Copy link
Member

@mjp41 mjp41 commented May 17, 2022

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.

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.
@mjp41 mjp41 requested a review from davidchisnall May 17, 2022 10:37
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
Copy link
Member Author

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.

Copy link
Collaborator

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted

Copy link
Collaborator

@davidchisnall davidchisnall left a 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
Copy link
Collaborator

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.

@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* off. It defaults to false for both debug and release builds and
* off. It defaults to false and

Copy link
Member Author

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.

* 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.
Copy link
Collaborator

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
* A read bounds check, performed only when read checks are enabled.
*/
Read,
Copy link
Collaborator

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.

Copy link
Member Author

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.

Copy link
Collaborator

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.

Copy link
Member Author

@mjp41 mjp41 May 19, 2022

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.

Copy link
Collaborator

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.

Copy link
Member Author

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.

Copy link
Collaborator

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.

@davidchisnall
Copy link
Collaborator

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.

@mjp41 mjp41 merged commit c445de8 into microsoft:main May 19, 2022
@mjp41 mjp41 deleted the memcpy_codegen branch May 19, 2022 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants