diff --git a/writeup/sort_safety/text.md b/writeup/sort_safety/text.md index d27a342..6e145be 100644 --- a/writeup/sort_safety/text.md +++ b/writeup/sort_safety/text.md @@ -80,7 +80,7 @@ If the sort operation is understood as a series of swaps, C, D, E and F can all ### Exception safety -Exception safety encompasses the various guarantees of correctness that can be provided in the presence of exception. In the concrete case of sort implementations that accept user-provided comparison functions, exception safety expresses the possible behavior when the comparison function can throw an exception. +Exception safety encompasses the various guarantees one can make about the program state in the presence of exceptions. In languages with stack unwinding based exceptions like C++ and Rust, the concern for sort implementations is the state in which the input is left in when the user-provided comparison function raises an exception. C++: @@ -106,18 +106,18 @@ data.sort_by(|a, b| { }); ``` -The weakest guarantee is that an exception does not directly lead to undefined behavior. The strongest guarantee is "transactional exception safety", that guarantees that in the case an operation fails, then the state of the program is reverted to the one before the failed operation was attempted. This strongest guarantee is typically both too strong and too performance intensive, so very rarely observed in practice. +The weakest guarantee is that an exception does not directly lead to undefined behavior, which is often referred to as "basic exception safety". The strongest kind of exception safety, known as "strong exception safety", models transactional semantics. With strong exception safety, the input must be returned to the state it was in before the operation was started. -For the purpose of this benchmark, the guarantee we are interested in will be denoted "intuitive exception safety". It encompasses the natural behavior a user that is not especially aware of the internal of sort algorithms may expect when a sort routine is interrupted. As the observable side-effect of a sort routine is to reorder elements of the input, intuitive exception safety expresses the guarantee that in the face of an exception, elements from the input may only be partially reordered, as if the sort process had been interrupted. In particular, this excludes modifying elements of the input, for instance duplicating some of them, removing some of them, or, in the case of C++, leaving some elements in the "moved out" state. +For the purpose of this analysis, the guarantee being analyzed will be denoted as "intuitive exception safety". It encompasses the behavior assumed to be the intuitive result of interrupting a routine that rearranges elements. Once stack unwinding reaches user code, the input may be partially reordered, as if the sorting procedure had been interrupted. An implementation does not provide intuitive exception safety if the input after stack unwinding contains new duplicates or values that were not previously seen as part of the input. In the case of C++, this includes leaving some elements in the "moved out" state. -We choose intuitive exception safety as the desired property for sort algorithms, because failure to uphold this property can indirectly cause UB in user code by violating invariants enforced in the rest of the code. +Failure to uphold intuitive exception safety can indirectly lead to undefined behavior in user code by violating the invariants enforced in the rest of the code. Examples include: -As examples, consider the following two situations: +- The user is sorting a `std::vector` of move-only types like `std::unique_ptr`. If the user's code guarantees that, by construction, the pointers in that vector are not null, then it is permissible in user code to rely on this invariant and omit null checks. However, this is fraught in the presence of a sort implementation that doesn't verify the intuitive exception safety property. The input may unexpectedly contain null pointers after the comparison function throws. +- The user is sorting a container of numbers that serve as indices in a graph-like structure. If the user's code guarantees that, by construction, the indices in the container are never repeated, then it is permissible in user code to rely on this invariant and delete the node associated with each index without checking if it was previously deleted. However, this is fraught in the presence of a sort implementation that doesn't verify the intuitive exception safety property. The input may unexpectedly contain duplicated indices after the comparison function throws. This can also affect Rust implementations, even ones implemented with zero lines of `unsafe`, as shown [here](https://github.com/google/crumsort-rs/issues/1). -1. The user is sorting a vector of move-only types like `std::unique_ptr`. If the user's code guarantees that, by construction, the pointers in that vector are not null, then it is permissible in user code to rely on this invariant and omit null checks. However, this is fraught in the presence of a sort implementation that doesn't verify the intuitive exception safety property: the input may unexpectedly contain null pointers after the comparison function throws. -2. The user is sorting a vector of natural numbers that serve as indices in a graph-like structures. If the user's code guarantees that, by construction, the indices in that vector are never repeated, then it is permissible in user code torely on this invariant and delete the node associated with each index without checking if it was previously deleted. However, this is fraught in the presence of a sort implementation that doesn't verify the intuitive exception safety property: for trivially-copyable types, the input may unexpectedly contain duplicated indices after the comparison function throws. +Failure to uphold intuitive exception safety might also directly lead to UB in C++, for types that by mistake don't follow C++ best practices for modeling types. This includes types from third-party libraries, the user has no control over, as well as interaction with owning C library types without appropriate RAII wrappers. -These issues are not theoretical: even assuming a world filled exclusively with C++ types following best practices, where duplicating integers will not directly lead to UB, it can still easily break adjacent assumptions made about a sort operation only re-arranging elements and not duplicating them, as shown [here](https://github.com/google/crumsort-rs/issues/1). +Intuitive exception safety differs from strong exception safety in a couple key ways. Firstly, it is impossible to rollback arbitrary side-effects caused by the user-provided comparison function. In Rust doing so would even lead to UB in purely safe code in combination with interior-mutability as shown [here](https://github.com/emilk/drop-merge-sort/issues/23). In addition, restoring the original state would require auxiliary memory to either track the original order of elements or to store a copy of them, which is impossible to acquire in no_std/freestanding environments. ### Observation safety