2026-03-06 • 48:19
Learn how Memory Integrity Enforcement and Pointer Authentication work together to defend your app against memory corruption. Find out how the type-aware secure allocator leverages Memory Tagging Extensions to prevent exploitation, and how to adapt allocator wrappers and object pools to preserve hardware protections. Plus, explore how to enable Pointer Authentication in Xcode with arm64e to maintain control flow integrity.
This session was originally presented as part of the Meet with Apple event “Fortify your app: Essential strategies to strengthen security.” Watch the full video for more insights and related sessions.
Speakers: Enrico Perla, Filippo Bigarella, Oliver Hunt
Part of: Fortify your app: Essential strategies to strengthen security
Downloads from Apple
Transcript
Welcome to the first deep dive of the day. My name is Enrico Perla. I’m a security engineer and I’ll be later joined on stage by my colleagues, Filippo Bigarella, and Oliver Hunt. We’re going to look at two of our most exciting security features. Memory integrity, enforcement, and pointer authentication.
This is going to be an advanced talk. We are going to talk about memory allocators, pointers and security models. I will start by giving you an understanding of the length by which memory integrity enforcement goes to protect your application. Then Filippo will show you how to address specific custom memory management implementation that can hamper the security application or just flat out don’t work. Remember integrity enforcement. We will then switch topic and Oliver will take the stage to cover another angle of our security strategy. How we use pointer authentication to prevent attackers from achieving arbitrary code execution.
But enough with introductions. Let’s start with memory integrity enforcement. We mentioned earlier how the majority of our security issues are memory corruption bugs with memory integrity enforcement. Our mission is to make a large swath of DDS and exploitable. That’s huge. They no longer are a security problem for your application.
We are releasing memory integrity enforcement the very same way we use it for ourselves. It does not hold him back here. And that’s because we strongly believe that first party app and third party app are equally critical to protect our users safety. That’s why we put together some great resources to start with memory integrity enforcement. We’ll release a blog that describes all the security axes upon which memory integrity enforcement operates. We have a tech talk that gives you step by step on how to integrate memory integrity enforcement in your application.
And we also put together some great end to end documentation much around the same topic. There is no need to take notes. We’ll follow up with links with all of these via email. And if you’re watching later, they also attach to the online session. So just be sure to check them out. They’re great. I am not going to give today a full introduction to memory integrity enforcement. And instead I will just laser focus on our application, interact with its security centerpiece, the type aware secure memory allocator that leverages memory tagging extensions.
MTV is a hardware technology and one of the biggest investments behind memory integrity enforcement, and it’s also arguably the one with the most visible impact over application. So let’s take a quick look at it. It’s a lock and key system. Locks are assigned to memory at a 16 byte granularity. That’s a much lower granularity than a page, and it’s kind of ideal for dynamic allocations.
Keys are instead stored in the pointer. High bits, lock and keys are generally referred to as tags, hence the name. Memory tagging software controls the assignment of tags. That’s what we call the tagging policy. In the picture, it is a software decision that the yellow buffer tag seven, and so do the two pointers on the left hardware instead implements what we call the checking policy, and only allows accesses when the lock and key match.
Now, even if pointers now have tags, this does not affect software that much. In fact, enabling memory integrity enforcement is extremely straightforward. Just a few clicks of Xcode. Contrary to other technologies that require non-trivial adoption effort, MIT works largely over unmodified software. Unless, I mean, there has to be a catch or we wouldn’t be here today.
Unless your application implements custom memory management. I’ve danced around this topic a few times, so let’s go over what that means. There are three patterns that can be found in application which lead to custom memory management. I’m going to use them here in order of likelihood. The first one is allocated wrappers. Allocated wrappers are interfaces that wrap the system allocated API and do that usually for either portability reasons, or you have to implement some additional logic around memory operations.
They are by far the most common found construct. And so opposite to the other two cases that I’ll talk about in a moment, I’m going to give you here a quick example to enforce the idea. Here we define a function named allocate memory, which is one that actually calls malloc. The rest of the code that you see calls malloc directory, but instead always goes to this interface. Let’s put a pin on this example. Filippo will come to the stage in a short and expand on it.
The second direct memory managed pattern are pooling and caching strategies. The goal of these strategies is to improve performance by recycling frequently used objects, so that you avoid a round trip to the system allocator. They are also somewhat common, but not nearly as much as allocator wrappers. And lastly, something that is extremely rare in application, thankfully, but is much more a little more prevalent in frameworks which are fully custom memory allocators.
In this case, we have a drop in replacement which entirely bypasses the system default one. Intuitively, all these three patterns are detrimental to the security of memory tagging integrity enforcement as they interfere or entirely bypass the system allocator. And so, unsurprisingly, here comes the key recommendation of the day.
Use the default system allocator. I’m going to stay a little while on this slide. Use the default system allocator with no wrappers, no caches, or even custom implementations. We spend a lot of time making it scalable and fast for the vast majority of users, and it was implemented from scratch over the last three years. So if you took performance numbers more than three years ago, please reevaluate them. You might be pleasantly surprised. And we rewrote it so that it shines for integrity enforcement.
Now we do understand The. Some obstructions have a valid reason to exist. Maybe you just have some legacy code that limps around and you have to carry it forward. That’s fine. That’s why in the rest of this talk, we’re going to see how you can maintain them. But to do that, you have to meet the extremely high security bar that our locator meets.
And to do that, you need to understand why we built it that way. Which means that we should have an understanding of the security science behind it. So let me start with what attackers do, which is which is exploiting systems. And with something that hopefully will resonate and actually demystify this topic a little bit for you all.
Writing exploits is the inverse of debugging. Whenever you debug a memory corruption issue, you start from a crime scene. There’s some memory that was incorrectly modified or incorrectly used and let the program to misbehave. You try to piece together the parts to figure out what was the cause, what was the bug? What was the piece of memory where the corruption originated from?
In exploiting. It’s exactly the other way around. You start from some memory and that is incorrectly handled, and you try to find some memory that is worth modifying. And to put it in a more scientific way. We take an approach that is a little unique in the industry, and it’s actually attacker centric.
We have one piece of memory that we call the aggressor type. This is the one that is generating the corruption. Now the term type here does a lot of work. It refers to the intrinsic characteristics of the memory location. It could be a data structure. It could be a string. It could be a collection of records. Then we have the victim type. That’s some memory that is valuable to the attacker to modify. That’s the target of the memory corruption. The memory might contain a password, a function pointer, or something that later will give the attacker the ability to perform arbitrary code execution.
These two types are somewhere in memory. So there is a specific distance between them. There could be a patient, in which case the distance is one. Or they could even overlap, in which case the distance is zero. We assume that an attacker can observe the system and perform any arbitrary number of operations. They can talk to the kernel, look around the file system, anything that privileges allow them to do. We don’t rely on putting a cap on attacker actions.
The attacker wins if they control and predict the distance from the aggressor to the victim. That’s it. That’s the whole essence of writing memory, corruption exploits. And this might seem simple and, well, good science usually he is, but it’s actually very profound. It took a long time for us to get there, and it is important because it highlights what are our options when it comes to defend against memory corruption.
And what it shows is that we really have only two levers that we can pull types and distance. From a defender perspective, the whole game is to make type selection and distance impossible or extremely hard for an attacker to predict and control. And this is why our secure memory allocators all implement four key security properties.
First of all, they are type aware. Traditionally, allocations done via malloc are just a bag of bytes. Allocator doesn’t know much more than the size that was requested. It doesn’t know about the intent of the allocation. Our secure allocators instead understand types and these huge leverage is achieved with both manual typing mostly used in the kernel and compile time automated typing, which is what we use in userspace.
Once we have type information, we can start playing. Our allocators can start making hard for an attacker to control them. We can separate type into different regions in memory, and because we have a lot of types, we can’t really have one region per type, which would be ideal. So what we do is we collect together those that have similar characteristics.
We randomize those collection at boot time so that the groups will be different among different devices, which forces attackers to find a way to fine tune their exploit for each different combination. They may run on a device. Much on the same vein, we randomize the placement in memory of these type regions. Once again, we do that at boot. So then we introduce further variance across devices. This once again frustrates attempts by the attacker to predict the distance among different type classes.
And last but not least, we leverage memory tagging extension to stop attacks within the various type classes. We assign different tags at each location and free cycle, and ensure an even distribution of tags across the whole region. We also enforce the no. Two radiation objects have or have the same tag, making any distance between one and small sizes impossible to exploit.
This is how our security allocator operates. To stop memory corruption bugs, I will now hand the stage to Filippo, who will cover what you can do to prevent the three common custom memory managed patterns that I discussed earlier. So they don’t hamper the security of your application. Over to you.
My name is Filippo and I’m a security engineer in Caere. Now, as Enrico said, in most cases, adopting memory integrity enforcement does not require any code change on your side. However, there are some scenarios where we do need some extra care. And these are exactly what I’ll be working through right now. We will go over how each one of them impacts memory, integrity, enforcement, and its security model and explain what the best resolution approaches are. So let’s start by looking at what is perhaps the most commonly found abstraction across all kinds of code bases, allocator wrappers.
And let’s do that by picking up the example that Enrico has shown a moment ago. The allocate memory function, as it gives us the chance to define a set of features that you should look for to identify allocator wrappers in your code base. First of all, wrappers are functions that add logic around the system Allocator API.
They are generic in the sense that they allow requesting memory that is used to store different types, and they are used throughout the code base as an abstraction layer to interact with the allocator. So all of this seems fairly innocuous, right? Well, let’s go over how type isolation actually works in practice. To understand the security implications of allocator wrappers.
Here we have a bunch of code that’s implementing some packet processing logic. Don’t worry, we don’t need to read through all of this. And in fact, let’s focus on the core sides to the system allocator interfaces. This is where the foundations of type isolation are. And it’s actually the compiler, the one who’s doing all the work for you.
When you enable support for the type allocator in Xcode, we turn on a compiler feature called type Memory operations. With this feature under the hood, the compiler will automatically infer a type for each of your allocation code sites and rewrite each call into a type aware one, passing the type information as an extra argument. You can picture the compiler assigning different shapes to each allocation, and these shapes represent exactly the information that is then passed down to the system allocator, which uses it to isolate allocations based on their types by implementing a type bucketing policies.
When you implement a wrapper, this callsites become opaque to the compiler, which will only be able to see a single shape. And now all your allocations will be bucketed together by the system allocator, as it will only see the type information produced at the call site within the wrapper.
So let’s see what we can do to address this problem. Of course, if the logic implemented by the wrappers is not critical to the functionality of your code, the simplest solution is to just remove the wrapper altogether and call the system allocator interfaces directly. If you do need to keep the wrappers, you can implement a type aware variants that will properly forward the type information down to the system allocator by adopting type memory operations. The same compiler technology that is used throughout the operating system. So let’s walk through that process step by step.
You start by declaring a type aware variant of your upper, which takes an additional type ID argument right after the size argument. Then you want to annotate your untyped variant using the underscore malloc type macro by specifying the corresponding type variant and the position of the size argument.
We are instructing the compiler to transform all calls to the untyped variant into calls to the type aware one, and also to synthesize a type descriptor at each call site. Finally, you need to implement the type aware variant. But this is very easy. You can keep all your additional logic intact. The only change required is calling the appropriate type aware malloc interface by forwarding the type descriptor value that you get as an argument.
With this approach, you do not need to make any change to the code that’s actually using the wrapper. The compiler will automatically provide the type information at each call site for you. So this is just a brief overview and for a lot more in depth information, I encourage you to take a look at the documentation that covers this approach.
All right. Now we understand how to deal with allocator wrappers. Let’s now move to pools and caches. When we talk about pools, we refer to all those approaches that implement some form of recycling of objects of the same type in order to avoid a round trip to the system allocator, every time one of such objects is disposed and then brought back into use. Caching strategies also fit into this category.
In this context, the key notion to understand is that these abstractions change the lifecycle of the objects that are being recycled. And this has serious security implications with regards to the protections offered by memory tagging. So let me go over what those implications are by walking through an example.
Here we have a pool of objects, each of which is backed by an allocation that is tagged using memory tagging. When we extract an object from the pool, we normally get a pointer to it, which will have the same tag as the memory associated with the allocation. When we are done with the object, we put it back into the pool. Now consider the scenario where there is a bug in our code, and we keep around a dangling pointer to that object. This is exactly what could lead to a use after free vulnerability that can be exploited by an attacker.
If we simply recycle the object, this means that both the dangling pointer and the new light one will still have a valid tag. An attacker that has control over the dangling pointer will be able to modify our object after it has been recycled, leading to memory corruption that can change the logic of your app.
In order for the allocation to be protected from such vulnerabilities, the tag should be updated before the object is recycled. After retagging, any access performed using the pointers that have the old stale tag will fail safely, terminating your app and preventing exploitation. So let me cover how we can achieve that in practice.
We have implemented our recycling policy in our packet processing code. When we need to allocate a packet, we first try to extract it from a threadlocal queue that we maintain and that we refill in the release packet function when we’re disposing of objects. As you can tell, this approach is subject to the exact problem that we have just seen.
So what can we do to maintain the protections that MTA provides? Of course, you might have guessed it by now. The best solution is once again to use the system allocator directly. This ensures that each and every allocation is properly retagged, providing you the protections that you expect from memory integrity enforcement.
We spent years designing and implementing a system allocator that is both secure and that outperforms the old allocator in all scenarios. And indeed, with a threadlocal caching, our system allocator is also optimized for scenarios such as this one. In the rare cases where this approach might not be suitable for you, we encourage you to profile your code and explore different strategies that do not change the life cycle of the objects. For example, by allocating the objects that you will use in batches.
All right, so this approach makes it extremely easy to adapt your pooling strategies to preserve the security properties that MI has to offer. And now I’d like to spend a moment talking about custom allocators. This might not be necessarily implemented in your application code, but they might be part of any of the external dependencies that you’re using. Libraries have historically implemented them either for performance reasons or for portability across platforms.
In the presence of a custom allocator, all bets are off. It is very important to understand that the code that’s using a custom allocator is not getting the benefits of Ma, and in particular, your app cannot benefit from the protections of type isolation and memory tagging. In these scenarios, you should really evaluate the security implications of keeping such implementations around.
So I’m guessing you know what our suggestion is going to be. Transition that code to using the system allocator directly. We truly believe it is a foundational block for the security of your app. However, we are aware that there are cases where you cannot just move away from a custom allocator.
And this is why, as of 26.1, across all our platforms, we have included in the SDK all the building blocks that you need to to implement support for MT in your custom allocator. We will now go through them, but given that each allocator implementation has its own peculiarities, it is up to you to understand how to use these building blocks in your use case.
The first thing you need to do is to check whether MT is enabled at runtime, and you can do so using the OS security config API as shown here. After enabling hardware memory tagging, your app will run with empty enabled on the devices that support it, but the same code will still need to run on devices that do not support. Empty all the operations that rely on the empty instruction set. Architecture should only be performed after checking that empty is actually enabled in the process.
Next, when you allocate pages of memory that are used by your allocator, you should ask the VM to provide you pages that support memory tagging, bypassing the VM flags. Empty flag in your call to VM allocate. At this stage, the kernel will provide you with a page whose associated tags will all be zero.
And that’s why. Finally, and most importantly, you want to tag your allocations. There are several different possibilities when it comes to choosing a tagging scheme, and there will be a lot of nuances to consider when implementing each one of these. To give you a brief overview of the available API, Let’s just consider a simple example where we retag each location when it is freed. Back to our allocator.
There are two parts when it comes to tagging and allocation. Choosing a target, including it in the pointer and then storing the tag to the tag storage memory. To choose a tag, the first step is to generate an exclusion mask that accounts for the target that is currently associated with the allocation. This then allows you to ask the hardware to generate a new random tag by excluding the previously used tag. Empty generate random tag will give back to us a pointer pointing to the same memory, but with a different random tag in the high bits.
Last but not least, you want to use empty store tag to ask the hardware to store the newly generated tag to the tag storage memory by passing in the pointer with the new tag and the size of the underlying memory block that needs to be tagged. And this is really all you need to tag memory in your allocator.
And these were all the basic tools that you need to implement support for memory tagging in your allocator. Which actually brings us to the end of our journey in Milan. But before we conclude, let me reiterate the key takeaways from this session and what you can do to make the best of memory integrity enforcement.
You should enable hardware memory tagging and support for the Typekit allocator in order to provide a very concrete protection to your users using best in class technologies. When it comes to mitigating memory corruption vulnerabilities, it’s really easy to adopt, especially because in the large majority of scenarios, you can get all the security benefits that these technologies can offer without making any change to your code.
However, as we have seen in this talk, there are certain patterns that reduce the efficacy of the protections provided by Memory Integrity enforcement. We encourage you to audit your code base and to identify them, and so you can better evaluate your exposure. Whenever you identify one of these. The prefer a solution approach should be to transition to using the system allocator directly. This really gets you the best of all possible worlds.
However, if at times this is not feasible, we have provided you with the tools and the understanding that you need to adapt such abstractions to support memory, integrity, enforcement and preserve the security properties that are deeply rooted into the operating system. And now I’ll invite Oliver to the stage to talk about control flow integrity with pointer authentication.
Thanks, Filippo. Hi, everyone. I’m Oliver Hunt. I’m an engineer working on security and tooling, security tooling and compilers here at Apple. At this point in the session, you’ve learned about how you can protect your code against memory safety errors with memory integrity enforcement. MI makes exploiting memory safety bugs incredibly difficult, but it cannot protect against every possible attack.
I’m going to be showing you how you can adopt hardware based pointer authentication that provides your application with control flow integrity. That means even if an attacker can bypass me and corrupt arbitrary memory, they still don’t have the ability to control the code that your application will execute with pointer authentication. The hardware, the compiler, and the operating system all work together to ensure the validity of pointers that attackers target when trying to hijack your application.
Here’s how this works. Under the hood, with pointer authentication, the CPU and operating system create cryptographic signatures of pointers, and then they embed those signatures into the pointers themselves. These signatures allow the hardware to check the validity of a pointer before it is used, which gives your code a chain of trust linking the value of a pointer at the time it is used all the way back to its original value, and pointer authentication continues to protect these pointers in applications that are also using mice.
Memory tagging extensions by transparently adjusting the signing and authentication operations to accommodate the existing tag. The additional tags with all of this in place, when an attacker attempts to corrupt a pointer, the signature is no longer valid and the chain of trust is broken. Now, when your code later attempts to use that pointer, the hardware detects this and safely halts your application. The attacker has now been stopped before they can execute any malicious code.
At Apple, we’ve been developing the pointer authentication API for almost a decade, and we’ve been using it as a tool to protect our platforms for all of that time. And the design is now stable and robust enough for you to be able to adopt it and employ the exact same protections in your code that we use to protect our own software. While pointer authentication does result in significant changes to code generation, we’ve made sure that there are only a few places that your application is likely to see any differences. So let’s have a look at the big ones.
Return addresses have long been targeted by attackers, and a wide array of mitigations have been deployed over the years to protect them with pointer authentication. This is taken to the next level. Whenever you make a call, you have a return address. With pointer authentication, we combine both the return address itself, as well as information about the current call frame into the signature that’s embedded into that return address. This information is then also used when authenticating the return address before it can be followed.
This preserves the chain of trust from the moment the return address is first recorded, to the point where it’s used, and even prevents an attacker from reusing a validly signed pointer from a prior call. And all of this happens as part of the basic calling convention, and is only rarely visible to functions that are written in assembly. Now, if your code does interact with the call stack beyond the basics of just calling functions, we’ve also made sure that all of the APIs and the compiler features that you’re using to do this continue to work seamlessly.
Now let’s go through the features that you’re explicitly using for the dynamic control flow of your application. We’re going to start with function pointers because these are the most basic form of dynamic control flow in your application. And there are all sorts of most unrestricted form of indirect code execution in languages like C and C++. So we make sure that they are always signed. This is important as a common idiom in C and C++ is to cast function pointers into integers or opaque pointers. And we know that you cannot always avoid this.
That’s why we’ve made sure that the pointer authentication model maintains the embedded signatures throughout all of these operations without you needing to change your code. This means that once again, the chain of trust is maintained, and there is no point at which an attacker can modify these pointers, even though the compiler may no longer even know that they are a function pointer.
But we know in your code you don’t want to do everything using function pointers. And so you’re making extensive use of language supported dynamic dispatch. And that’s when you’re calling non-final methods in Swift virtual methods in C++ or sending messages in Objective-C in all languages. Dynamic dispatch is built on multiple layers of indirection.
In its simplest form, each object instance has a pointer to some kind of type information or a method table. Then that data structure has another pointer that goes to the actual implementation of each method. This interaction is. This interaction is attractive to attackers because each layer of these can be attacked independently.
And that’s why in Swift, C++, and Objective-C, we have protected each step in this chain. And when we create the signature for each pointer in this chain, we include information about the type of the object, the identity, or the location of the object, and even the type of the target method. And then when you make a dynamic call, each step is authenticated using the same information.
By doing all of this work, we’ve made sure that your application is protected not just from memory corruptions, but even lifetime and type confusion attacks. But this level of protection is one of the few reasons that pointer authentication may require you to change your code. To see why, let’s focus on that first authentication step.
By incorporating the object location into each signature, we’ve made sure that the signature is only ever valid at this location in memory. That prevents an attacker from using a memory safety error to copy one object over another. But when you use low level functions like Memcpy to copy these objects, it’s fundamentally no different than what an attacker is doing, and the result will be the same. You’ll get an authentication failure when you later try to use the object. This is one of the cases where pointer authentication changes existing code from being undefined, behavior that appears to work to undefined behavior that fails at runtime.
So we’ve covered just the highest levels of protection that you get with pointer authentication. There’s a much deeper array, but that’s not something that you’ll ever see. We’ve designed this implementation so that it will work with your existing code. So I’m sure that you’re excited to adopt it in your own applications. So let me take you through the steps that you need to do to do so.
Well, before you start adoption in your own application. You’ll need to ensure that all of the libraries that you embed or link to support pointer authentication. If you’re the author of these libraries, you’ll be doing that work yourself. But if you are using externally developed libraries, you will need to reach out to your vendors to have them adopt pointer authentication and provide you with universal binaries.
Once that’s done, you can start the adoption effort in your own application, and you do that by selecting the Enable Enhanced Security option in your Xcode project’s build settings. This will enable memory integrity, enforcement, and pointer authentication. When you do this, Xcode will automatically configure your project to build a universal binary that contains the familiar Arm64 slice and an additional Arm64 e slice that is used by the hardware that supports pointer authentication. If you want to focus on just adopting one thing at a time, you can focus on pointer authentication alone by just using the Enable Pointer authentication option instead.
We’ve designed all of the pointer again. We’ve designed all of the pointer authentication based protections to work with your existing code, and most of your applications will build and run correctly with all of these protections in place. But of course, that’s not a guarantee. So the next step is for you to extensively test your application.
Now, some of the first bugs that you encounter may be the result of existing bugs in your code that are now caught by authentication failures. This is an expected result. And now that you know of these bugs, you’ll be able to fix them. But it is possible that you do have code that is written in ways that are not compatible with pointer authentication. And for those, you’ll need to make some changes.
The underlying cause of these incompatibilities is when you are unintentionally invoking undefined behavior. As these operations overlap, often overlap the bugs that are exploited by attackers, they get stopped by those same protections. In practice, most code does not hit these problems. But let me quickly cover a couple of the most frequent sources of compatibility bugs that we’ve encountered.
The most common pattern that we’ve seen comes from the unsafe use of functions like memcpy to copy polymorphic objects. We already talked about why it’s not safe to use memcpy or similar functions when you’re doing this. But while you may not make these calls directly, there can be code in your container types that do perform these operations. And that is where we have seen these failures.
The fix for these errors is to either adopt higher level language features and data structures, or to adopt the library functions for object copy copies and initialization, because these are built into your language. They know about the types of your objects, and they’ll do all of the work required to ensure the correct semantics as efficiently as possible.
Even less common is storing extra data in the high bits of function pointers. If your code does this, that storage will corrupt the signature. To fix this, you need to either move the starter to the bottom bits of the pointers, or simply move that data outside of the pointers entirely.
These are the most common patterns that we have seen in code adopting pointer authentication, but they are still very rare, even in extremely large code bases. But if you do know of these patterns in your application, you’ll need to address them as part of your own adoption effort. Now, after making any of the changes you needed and your testing has shown that your application is working as expected, you’re going to deploy it to your users.
And as with any release, you might get reports of new crashes. And you want to know if these are authentication failures. To help you diagnose this, when your program is terminated, the generated crash log will include a diagnostic message If the fault may be due to an authentication failure.
If you’re performing your own crash logging, though, you can examine the high bits of the faulting address to provide a similar diagnostic. Now, simply knowing that a crash is due to an authentication failure isn’t enough on its own. So let’s take a look at an example of an authentication failure so that you can see how they will appear and how you might fix them.
So here we have a very simple program that is hit an authentication failure. When you’ve caught a failure in Xcode’s debugger. It will initially look like any other crash, but traps resulting from authentication failures will always set the high bits in the faulting address. And that’s what you see here. Now, don’t focus on the exact bits of being that are being set, as that can vary across different hardware generations.
And in our little test program, the failure is occurring immediately after this call to the copy object function. And we’ve learned earlier that it is possible that my existing code may be copying data in ways that can result in invalid objects. So let’s take a look at that function.
Perhaps unsurprisingly, as this is a demo of something going wrong, the function is using Memcpy to copy a polymorphic object because this used to work. While this example makes it very obvious if this does happen in your code, it is again only likely to occur inside custom or special case containers and data structures. Happily, the compiler already helps you find these problems early by issuing warnings for these unsafe operations, even when pointer authentication is not enabled. If your code base does permit it. You should configure these warnings to be errors.
Now you’ve found the problem. You need to decide on how you want to fix it. And there are a number of options available to you. So let’s take a look at a few of them. The most simple fix is to move from untyped memory access functions. That’s your mem copies your mem moves to the standard library. Functions provided for moving, copying and initializing objects.
These functions will ensure that any required work to ensure your objects are correctly initialized will be done, and they will use functions like Memcpy if it’s safe for them to do so. But given you’re already moving away from these low level functions, you should consider adopting higher level language features directly. For example, a safe equivalent to these copy operations would just be to use something like placement in you.
The most difficult case that we’ve seen that is very hard for you to fix is if you’re using Memcpy, because you’re trying to maintain the dynamic type of an object. Fixing this may require you to restructure your code to use something like language level polymorphism. For instance, in this example, we’ve replaced the copy with a virtual clone method, or to adopt manual type aware logic to perform these copies. So that would be you check a type.
Obviously, again, this is a very basic demonstration program just to show you what an authentication failure will look like and ways that you may approach fixing it. But almost all authentication failures in all languages are fixed in the same way, you need to migrate away from low level operations and instead use the support provided by the language you’re using its runtime and its libraries.
I’ve talked a lot about how pointer authentication protects your code. And we’ve even looked at an example written in C++. But you may not think that you need to adopt pointer authentication because you’ve already adopted, or are in the process of adopting a safe language like Swift. But that is not sufficient to protect your code.
To understand why, we need to have a look at how attackers are going to target your code. Fundamentally, attackers need two things to gain control of your application. First, they need a bug that they can use to corrupt your application state. In this example, the use of the unsafe scanf function allows an attacker to overflow the result buffer.
Second, they need code that is operating on the result of that corrupted state. Here they can use that buffer overflow to overwrite the contents of the error handling function in the calling the error handler in the calling function. When that error handler is then called, it executes the instructions that have been chosen by the attacker. They have now taken over the control flow of your application and are able to execute any code that they want.
So when you think about software exploitation, you need to think about more than just the original bug. You need to think about what the attacker is trying to do with that bug. So let’s look at that caller right now. It’s some unsafe C or C++ code like you’ve seen so many times before.
But what does it look like in a safe language? Well, I’ve rewritten this function in Swift and it is nearly identical to the C version. In fact, the exact same exploit that was used to overwrite the error handler in the C function can replace it in the Swift version as well.
Again, we’re using very simple examples so you can follow it, but the outcome is the same no matter what language you’re using, no matter what the original bug was, and no matter how complex it was to exploit. And that’s because when a user is running your app, they’re not just running your own code, they’re running code from all of the libraries that are running alongside your code. So no matter what language you’re using, you need to protect your code from any bugs that might exist in any of the unsafe code that is also running in your process.
And by adopting pointer authentication, you’ll have done that. You’ve made it much harder for attackers to compromise your application. Even those that do manage to circumvent other protections like memory integrity, enforcement. And you got this protection with few, if any, changes to your existing code. And so that’s how you can use pointer authentication to prevent attackers from hijacking your app.
And with that, I’d like to thank you for joining Enrico, Filippo and myself for this overview of how memory, integrity, enforcement, and pointer authentication can protect your users by having the hardware, the compiler, the operating system, and your apps all working together. I’m sure that you’ll discover that adopting and working with these tools is practical, and it requires little, if any, code changes, and it provides a significant benefit to the integrity and overall safety of your app. So thank you. And now back to Curt.