• Swift 5 Exclusivity Enforcement
  • Swift.org
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: LoneyIsError
  • Proofread by: Bruce-Pac, Danny1451

In understanding the concept, I refer to the Ownership Manifesto of cat god – annotated version of translated version of Swift’s official article Ownership Manifesto

Swift 5 further enhances Swift as a secure language by allowing runtime checks on “exclusive access to memory” to be enabled by default during the Release build. In Swift 4, this runtime checking is only allowed to be enabled during a Debug build. In this article, I’ll first explain what this change means for Swift developers, then delve into why it’s critical to Swift’s security and performance strategy.

background

To achieve memory security, Swift requires exclusive access to a variable before it can be modified. Essentially, when a variable is modified as an inout parameter or self in a mutating method, it cannot be accessed by a different name.

In the following example, the modifyTwice function modifies it by passing count as the inout argument. An exclusive violation occurs because the modifier closure for count is called at the same time that the variable is read in the scope in which the count variable is modified. In modifyTwice, the count variable can only be securely accessed with the value parameter modified by inout, and in the Modifier closure, it can only be securely accessed with $0.

func modifyTwice(_ value: inout Int, by modifier: (inout Int) -> ()) {
  modifier(&value)
  modifier(&value)
}

func testCount(a) {
  var count = 1
  modifyTwice(&count) {$0+ =count }
  print(count)}Copy the code

As is often the case with exclusivity violations, the programmer’s intentions are a little vague. Do they want count to print “3” or “4”? Either way, the compiler cannot guarantee the outcome. Worse, compiler optimizations can produce subtle unpredictable behavior when such errors occur. To prevent violations of exclusivity and to allow the introduction of language features that rely on security guarantees, forced exclusivity was originally introduced in Swift 4.0: SE-0176: enforces exclusive access to memory.

Compile-time (static) detection can catch many common exclusive violations, but runtime (dynamic) detection is also needed to catch violations involving escaped closures, properties of class types, static properties, and global variables. Swift 4.0 provides mandatory detection at both compile time and run time, but mandatory detection at run time is only enabled during Debug builds.

In Swift 4.1 and 4.2, compiler checks have been progressively enhanced to catch more and more programmers circumventing exclusivity rules — most notably by capturing variables in or converting non-escape closures to escape closures. Swift 4.2 claims to have upgraded the exclusive access memory warning to an error in Swift 4.2, and explains some common cases affected by the new mandatory exclusivity detection.

Swift 5 fixes the remaining holes in the language model and fully implements the model. Because mandatory runtime checking for memory exclusivity is enabled by default during the Release build, some Swift programs that have previously performed well but have not been adequately tested in Debug mode may be affected.

Rare cases involving illegal code that cannot be detected by the compiler (SR-8546, SR-9043).

Impact on Swift project

Mandatory exclusivity checking in Swift 5 can have two effects on existing projects:

  1. If the project source code violates Swift’s exclusivity rules (see SE-0176: Enforce exclusive access to memory), Debug Debug tests fail to execute invalid code, and then runtime traps may be triggered when building the Release binary. Produces a crash and throws a diagnostic message containing a string:

    Simultaneous accesses to…, but modification requires exclusive access

    Source-level fixes are usually simple. Common examples of violations and fixes are shown in the following sections.

  2. The overhead of memory access checking can affect the performance of the Release binary package. In most cases, the effect should be small; If you see a significant performance degradation, please submit a bug so we can see what needs to be improved. As a general rule, class attribute access should be avoided in most performance-critical loops, especially on different objects in each iteration of the loop. If you must, you can modify the class property to be private or internal to help tell the compiler that no other code is accessing the same property within the loop.

You can disable these runtime checks with Xcode’s “Exclusive Access to Memory” build setting, This setting also has two options: “run-time Checks in Debug Builds Only” and “compil-time Enforcement Only” :

The corresponding Swiftc compiler flags are -invincie-exclusivity = unchecked and -invincie-Exclusivity = None.

While disabling run-time checking may address the performance degradation, it does not mean that exclusivity violations are safe. If coercion is not enabled, the programmer must assume responsibility for following the exclusivity rule. It is strongly recommended not to disable runtime checking when building a Release package, because if your program violates the exclusivity principle, unpredictable results can occur, including crashes or memory corruption. Even if the program appears to be working right now, future versions of Swift could lead to other unpredictable situations and potentially expose security vulnerabilities.

The sample

The “testCount” example in the background section violates the principle of exclusivity by passing a local variable as an inout parameter while capturing it in a closure. When the compiler detects this at build time, it will look like the following screen capture:

Inout parameter violations can often be fixed simply by adding lets:

let incrementBy = count
modifyTwice(&count) {$0 += incrementBy }
Copy the code

The next example might also modify self in the mutating method, resulting in an exception. The append(removingFrom:) method adds an array element by removing all elements from another array:

extension Array {
    mutating func append(removingFrom other: inout Array<Element>) {
        while! other.isEmpty {self.append(other.removeLast())
        }
    }
}
Copy the code

However, using this method to add all elements from its own array to itself causes an unexpected case — an infinite loop. Here, the compiler throws an exception again at build time because “Inout arguments are not allowed to alias each other” :

To avoid these simultaneous modifications, copy the local variable to another var and pass it to the mutating method as an inout argument:

var toAppend = elements
elements.append(removingFrom: &toAppend)
Copy the code

Now, the two modification methods make changes to different variables, so there are no conflicts.

Examples of some common conditions that lead to build errors can be found in upgrading exclusive access memory warnings to errors in Swift 4.2.

By changing the first example to use global variables instead of local variables, you can prevent the compiler from throwing errors at build time. However, running the program will hit “Simultaneous Access” checks:

In many cases, conflicting access occurs in different statements, as shown in the example.

struct Point {
    var x: Int = 0
    var y: Int = 0

    mutating func modifyX(_ body:(inout Int) -> ()) {
        body(&x)
    }
}

var point = Point(a)let getY = { return point.y  }

// Copy `y`'s value into `x`.
point.modifyX {
    $0 = getY()
}
Copy the code

Runtime detection captures the access information when modifyX is started, and the access information that conflicts within the getY closure, and displays the stack information that caused the conflict:

Simultaneousaccesses to ... , but modification requires exclusive access.Previous access (a modification) started at Example`main + ....
Current access (a read) started at:
0    swift_beginAccess
1    closure #1
2    closure #2
3    Point.modifyX(_:)
Fatal access conflict detected.
Copy the code

Xcode first identifies internal access conflicts:

Select “Last Access” from the view of the current thread in the sidebar to determine external modifications:

An exclusive violation can be avoided by copying any values needed in the closure:

let y = point.y
point.modifyX {
    $0 = y
}
Copy the code

If this is written without getters and setters:

point.x = point.y
Copy the code

… Then there is no exclusivity violation, because in a simple assignment (with no inout parameter), the change is instantaneous.

At this point, the reader may wonder why the original example is considered a violation of the exclusivity rule when reading or writing two separate attributes; Point. X and point. Y. Because Point is declared as a struct, it is considered a value type, which means that all of its properties are part of the entire value, and accessing any one property accesses the entire value. The compiler makes exceptions to this rule when security can be proved through simple static analysis. In particular, the compiler avoids throwing reports of exclusivity violations when the same statement initiates access to two disjoint stores. In the next example, the modifyX method is called to access point so that its attribute X is immediately passed as inout. The point is then accessed again with the same statement to capture it in the closure. Because the compiler can immediately see that the captured value is only used to access the property Y, there is no error.

func modifyX(x: inout Int, updater: (Int)->Int) {
  x = updater(x)
}

func testDisjointStructProperties(point: inout Point) {
  modifyX(x: &point.x) { // First `point` access
    let oldy = point.y   // Second `point` access
    point.y = $0;        / /... allowed as an exception to the rule.
    return oldy
  }
}
Copy the code

Attributes can be divided into three categories:

  1. The instance property of the value type

  2. Instance properties of a reference type

  3. Static and class attributes of any type

Only changes to attributes of the first class (instance attributes) require exclusive access to the overall store of aggregate values, as shown in the struct Point example above. The other two categories can be executed separately as separate stores. If this example were converted to a class object, the exclusivity principle would not be violated:

class SharedPoint {
    var x: Int = 0
    var y: Int = 0

    func modifyX(_ body:(inout Int) -> ()) {
        body(&x)
    }
}

var point = SharedPoint(a)let getY = { return point.y  } // no longer a violation when called within modifyX

// Copy `y`'s value into `x`.
point.modifyX {
    $0 = getY()
}
Copy the code

purpose

The combination of compile time and run time exclusivity checks described above is necessary to enforce Swift’s memory security. Fully implementing these rules, rather than placing the programmer with the burden of following exclusive rules, helps in at least five ways:

  1. Performing an exclusivity check eliminates dangerous interactions between programs involving mutable state and distant actions.

    As programs grow in size, they become more likely to interact in unexpected ways. The following example, similar to the array.append (removedFrom:) example above, requires an exclusivity check to prevent programmers from passing the same variable as both source and target data. Note, however, that when it comes to class objects, because the two variables refer to the same object, the program inadvertently makes it easier to pass the same Names instance in the SRC and dest positions. Of course, this leads to an endless loop:

func moveElements(from src: inout Set<String>, to dest: inout Set<String>) {
    while let e = src.popFirst() {
        dest.insert(e)
    }
}
 
class Names {
    var nameSet: Set<String> = []}func moveNames(from src: Names, to dest: Names) {
    moveElements(from: &src.nameSet, to: &dest.nameSet)
}
 
var oldNames = Names(a)var newNames = oldNames // Aliasing naturally happens with reference types.
 
moveNames(from: oldNames, to: newNames)
Copy the code

Se-0176: Implementing exclusive access to memory describes this problem in more depth.

  1. Performing an exclusivity check eliminates unspecified behavior rules in the language.

    Prior to Swift 4, exclusivity was necessary for clearly defined program behavior, but the rules were not limited. In practice, it is easy to violate these rules in subtle ways, leaving programs vulnerable to unpredictable behavior, especially in releases of compilers.

  2. Performing an exclusive check is necessary to stabilize the ABI.

    Failure to fully perform exclusivity checks can have unpredictable effects on the stability of the ABI. An existing binary built without a full check might work in one version, but not in future versions of the compiler, the standard library, and the runtime.

  3. Perform exclusivity checks to make performance optimizations legal while protecting memory security.

    Exclusivity checks on inout parameters and mutating methods provide the compiler with important information that can be used to optimize memory access and reference counting operations. Since Swift is a memory-safe language, as mentioned in point 2 above, it is not enough for the compiler to simply declare an unspecified behavior rule. Fully enforcing an exclusivity check allows the compiler to optimize based on memory exclusivity without sacrificing memory security.

  4. Exclusivity rules give the programmer ownership and control over moving-only types.

    Added the principle of exclusivity to Swift’s ownership declaration and explained how it provides the basis for adding ownership and moving-only types to the language.

conclusion

By forcing full exclusivity checks to start during the Release build, Swift 5 helps eliminate errors and security issues, ensures binary compatibility, and supports future optimizations and language features.

Other questions?

Please feel free to post questions on relevant topics in the Swift forums.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.