Apple uses two sessions to talk about security in Swift code, which is probably something that most junior and middle level developers don’t pay attention to (and probably don’t use), but it’s really important to understand, especially if you want to be a senior developer, so let’s continue to learn from Swift Deep Dive

What exactly makes code “unsafe”? Know your programming language’s security precautions — and when you need to be exposed to unsafe operations. We’ll look at what unexpected states can result from incorrect use of the API, and how to write more specific code to avoid undefined behavior. Learn how to use the C APIs that use Pointers and the steps to take to use the APIs for unsafe Pointers in Swift.

Unsafe API

Swift offers many differentclass.struct.protocol.property.functionAnd so on, a small number of which are clearly marked as unsafe, one of the obvious prefixes isUnsafe.

It is obvious that it is “unsafe” on the face of it, but it is not obvious at first glance what exactly distinguishes it from an unsafe type – in fact, the difference lies in the implementation of handling invalid input. Most operations in the library fully validate their input before they are executed, so it’s safe to assume that any serious coding errors we might make will be reliably caught and reported.

What are “safe” and “unsafe”?

For example:

let value: Int? = nil

print(value!) // Fatal error: Unexpectedly found nil while unwrapping an Optional value
Copy the code

Strong solution nil will cause the crash, this is a serious programming error (due to the unreliability artificially, the team we are forbidden to use strong solution), but it is clear and the consequences of defined, so we say strong solution is a “safe”, because we can clearly know what will be the result for various inputs.

From this we can know that the so-called “safe” does not mean that crash will not happen anyway, but refers to whether all inputs have defined results:

For “unsafe” operations, there are some inputs that cause uncertain results (not just crashes) :

So let’s be very clear about apple dad’s definition of both:

The Unsafe operation

Take unsafelyUnwrapped for example, which provides an “unsafe” strong solution operation:

let value: String? = "Hello"

print(value.unsafelyUnwrapped) // Hello
Copy the code

It has to do with! Similarly, so value must also be non-nil. But with compile optimization enabled, it doesn’t check for nil, it thinks the developer has made a guarantee. If we accidentally call this property on nil, it might cause an immediate crash, or it might return some junk value. Debugging is difficult because you can’t be sure what will happen. This is a classic “unsafe” type.

The “unsafe” types in the standard library all have this characteristic: they all have assumptions that they are unwilling or unable to fully verify.

The unsafe prefix is like a danger symbol, alerting developers to its potential dangers. In fact, certain tasks can only be done with them, so they are not forbidden, but they need to be used with great care and with full understanding of their conditions of use.

Meaning of existence

As mentioned earlier, certain tasks can only be done using unsafe:

  1. Provides interoperability with C or Objective-C
  2. Provides fine-grained control over runtime performance or other aspects of program execution

The unsafelyUnwrapped attribute falls into the second category. It eliminates the need to make redundant judgments about whether a value is nil, because performance measurements show that these unnecessary checks can have an adverse impact on performance despite the minimal cost.

It is important to note that the goal of the security API is not to prevent crashes. Quite the opposite: when they are given input outside of a given constraint, the security API ensures execution stops by raising fatal runtime errors. Used to prompt developers to fix immediately.

We say Swift is a safe programming language because its language and feature library fully validate their inputs, and anything that doesn’t is labeled unsafe. The Swift standard library provides powerful “unsafe” pointer types that are at roughly the same level of abstraction as Pointers in C.

How do Pointers work?

To understand how Pointers work, we need to talk about memory.

Swift has a flat memory model: it treats memory as a linearly addressable 8-byte address space. Each byte has its own unique address, usually printed as a hexadecimal integer value.

At run time, the address space is filled with a small amount of data that reflects the execution state of the application at any given moment. These include:

  1. Executable binary;
  2. All imported libraries and frameworks;
  3. The stack provides storage for local and temporary variables and some function parameters;
  4. Dynamic memory areas, including class instance stores and memory manually allocated by developers;
  5. Some areas may even map to read-only resources, such as image files.

Each individual project is assigned a contiguous storage area where a specific type of data is stored at a specific location. As the application executes, its memory state changes constantly. Stack space is constantly changing, new objects are created to allocate memory, and old objects are destroyed. Fortunately, we generally don’t need to manage memory manually in Swift.

But when we do need to do so, the “unsafe” pointer gives us all the low-level operations we need to manage memory efficiently. Of course, there are risks associated with using them; these Pointers simply represent the address of a location in memory. They provide powerful operations, but rely on developers to use them correctly and are fundamentally “unsafe.”

If not careful, pointer operations can scribble across the entire address space, destroying the application’s carefully maintained state.

Take this example:

We dynamically allocate storage for integer values, create a memory bit, and provide a direct pointer to that location. When the underlying memory is freed, the pointer is invalidated, but it doesn’t know it’s invalidated, and trying to access it will crash.

In addition, subsequent allocations may use the memory bit to store other values, in which case unreferencing the suspended pointer can cause more serious problems.

Xcode provides a Runtime debugging tool called Address Sanitizer to help catch such memory problems. For details, see Finding Bugs Using Xcode Runtime Tools

] (developer.apple.com/videos/play…

For a more detailed discussion on how to avoid pointer type safety issues, see Safely Manage Pointers in Swift.

Interoperate with C or Objective-C

One of the big reasons why we use Pointers that are so dangerous is to interoperate with C or Objective-C.

Direct mapping

In C or Objective-C, functions usually take pointer arguments, and in order to be able to call them from Swift, developers need to know how to generate Pointers to Swift values. In fact, there is a direct mapping between the C pointer types and their corresponding Swift insecure pointer types.

The underlying implementation

Take this example:

// C:
void process_integers(const int *start, size_t count);

// Swift:
func process_integers(_ start: UnsafePointer<CInt>! ._ count: Int)
Copy the code

When this C function is introduced to Swift, the const int pointer argument is converted to an optional unsafe pointer type of implicitly strong solution.

The underlying implementation of Swift is as follows:

// 1. Use the static allocate method on UnsafeMutablePointer to create a dynamic buffer suitable for storing integer values
let start = UnsafeMutablePointer<CInt>.allocate(capacity: 4)

// 2. Use pointer algorithms and special initialization methods to set the elements of the buffer to specific values
start.initialize(to: 0)
(start + 1).initialize(to: 2)
(start + 2).initialize(to: 4)
(start + 3).initialize(to: 6)

// 3. Call C and pass it a pointer to the initialization buffer
process_integers(start, 4)

// 4. When the function returns, we can uninitialize and free the buffer, allowing Swift to reuse its memory location for other purposes at a later date
start.deinitialize(count: 4)
start.deallocate()
Copy the code

But virtually every step of the way is unsafe:

  1. The lifetime of an allocated buffer is not managed by a return pointer. We must remember to release it manually at the appropriate time, otherwise it will be there forever, causing a memory leak.
  2. Initialization does not automatically verify that the address location is in our allocated buffer. If we get it wrong, we get undefined behavior.
  3. In order to call the function correctly, we must know whether it will gain ownership of the underlying buffer. In this case, we assume that we only access the function during the call, neither holding the pointer nor attempting to release it. It’s not language enforced; You can only query for this in the function’s documentation.
  4. Uninitialization makes sense only if the underlying memory has previously been initialized with values of the correct type. At the same time, only previously allocated, uninitialized memory must be freed.

At every step, there are unchecked assumptions, and any mistake can lead to unknown behavior.

Swift optimization support

The library provides four “unsafe” buffer pointer types to easily obtain buffer boundaries when we need to deal with memory regions rather than Pointers to individual values.

For example, Swift’s standard contiguous collection uses these buffer Pointers to provide temporary direct access to its underlying storage buffer through these convenient but “insecure” methods:

We can also get a temporary pointer to a single Swift value, which we can then pass to the expected C function:

The lifetime of these temporary Pointers only exists in the current closure.

For example, we simplify the code by isolating unsafe operations into as small a part of the code as possible:

// C:
void process_integers(const int *start, size_t count);

// Swift:
// 1. To eliminate the need for manual memory management, we can store the input data in array values
let values: [CInt] = [0.2.4.6]

// 2. We can then use the withUnsafeBufferPointer method to temporarily access the underlying storage of the array directly
values.withUnsafeBufferPointer { buffer in
  // 3. In the closure, we can get the starting address and length and pass them directly to the C function we want to call
  print_integers(buffer.baseAddress!, buffer.count)
}
Copy the code

In fact, the need to pass Pointers to C functions is so frequent that Swift provides special syntax for it.

We can simply pass the array value to a function that needs an unsafe pointer, and the compiler will automatically generate the equivalent withUnsafeBufferPointer for us without the developer having to do this manually. But remember: Pointers are only valid for the duration of a function call.

If a pointer tries to escape from a function and tries to access the underlying memory, whatever syntax we use to get the pointer to escape will have unknowable consequences.

Here is a list of such implicit value-to-pointer conversions supported by Swift:

It can be seen from the figure in turn:

  1. To pass the contents of the Swift array to the C function, we simply pass in the array value itself.
  2. If the function wants to change the element, we can pass an arrayinoutReference to get a mutable pointer.
  3. A function that accepts a C string can be called by passing in the Swift string value directly: the string will generate a temporary C string, including the most important terminationsNULLCharacters.
  4. If the C function only needs a pointer to a single value, we can use a pointer to the corresponding Swift valueinoutReference to get an appropriate temporary pointer to it.

Fine-grained control of runtime performance or other aspects of program execution

With careful use of the above features, we can invoke even the most complex C interfaces.

For example,DarwinThe C functions provided by the module can be used to query or update low-level information about the current system:

This C function takes six arguments, which looks scary. However, calling this function from Swift is not necessarily as complex as it would be in C. Implicit pointer conversions can be used here to great effect and thus appear to be indistinguishable from other native syntax.

For example, here we create a function to query the size of a cache block in the CPU architecture we are running:

For a more detailed explanation of this example, check out Unsafe Swift at 16:30.

Of course, because we could also choose to extend this code to call based on explicit closures:

The two pieces of code are functionally equivalent, and choosing one is a matter of personal preference. Regardless of which version you choose, as we learned earlier, these Pointers are generated temporarily and will be invalidated when the function returns. In pure Swift code, this latter closing-based approach makes it easier for developers to notice this.

Other improvements

Temporary pointer warning

As we can see from the above, closure based design can more clearly grasp the pointer life cycle, thus avoiding related problems such as this invalid pointer conversion:

The life cycle of p only exists in the current closure, but the developer may not know about it and try to access it after the closure ends, at which point the underlying memory location may no longer exist, or other values may have been re-stored. To help catch these types of errors, the Swift5.3 compiler can now generate a useful warning when it detects such cases.

New construction method

The Swift standard library now provides new constructors that allow us to create data directly by copying data directly into the underlying uninitialized storeArrayorString. This eliminates the need to allocate temporary buffers only to initialize such data.

For example, in the following example, we need to find the kernel version of the operating system we are running, which is identified by version in the kernel section.

Again, a full explanation of this example can be found in Unsafe Swift at 20:00. (Ok, I admit it’s laziness 😑)

conclusion

From this chapter we can know:

  1. We can use the standard library’s “insecure” API to elegantly solve even the most difficult problems of getting along.
  2. These “unsafe” apis can be used effectively with knowledge of their flaws, otherwise the code will have undefined behavior.
  3. Control the use of “insecure” apis as much as possible and choose secure alternatives as much as possible.
  4. When dealing with an area of memory that contains multiple elements, it is best to use “unsafe” buffer Pointers (not just pointer values) to track its boundaries.
  5. Xcode provides a great set of tools to help debug how we use insecure apis, including Address Sanitizer, using them to identify errors in code before going live.

Related to the Session

  • Unsafe Swift
  • Safely manage pointers in Swift

It’s not easy to create the article, any mistake, welcome to feng comment (Diao), please click on 👍, thank you very much!