AppKit State Restoration on macOS 14 Sonoma
AppKit State Restoration on macOS 14 Sonoma
AppKit state restoration behaviour changed on macOS 14 Sonoma in a subtle way that can lead to apps not restoring their state correctly. The change can lead to silent breakages which can be hard to debug.
Behavioural Changes
The AppKit release notes for macOS 14 state:
Secure coding is automatically enabled for restorable state for applications linked on the macOS 14.0 SDK. Applications that target prior versions of macOS should implement
NSApplicationDelegate.applicationSupportsSecureRestorableState()
to return true so it’s enabled on all supported OS versions.
As usual, the behavioural changes only apply to apps that have been linked against the latest SDK to preserve backwards compatibility for existing apps.
Consequences
The relase notes don’t make it immediately clear what the consequences of
automatically enabling secure coding might be. In practice, it means that secure
coding violations can now occur. The response to such violations depends on the
value of NSCoder
’s decodingFailurePolicy
property which can be one of:
NSDecodingFailurePolicyRaiseException
: A failure policy that directs the coder to raise an exception.NSDecodingFailurePolicySetErrorAndReturn
: A failure policy that directs the coder to capture the failure as an error object.
The decodingFailurePolicy
property is readonly, thus outside of our control as
the NSCoder
object is created by the framework. AppKit uses
NSDecodingFailurePolicySetErrorAndReturn
as the default policy for state restoration.
The documentation states:
On decode failure, the
NSCoder
will capture the failure as anNSError
, and prevent further decodes (by returning0
/nil
equivalent as appropriate).
Thus, after a secure coding violation, subsequent decoding operations would silently fail.
Secure Coding Violations
NSSecureCoding
docs demonstrate the canonical secure coding violation:
Historically, many classes decoded instances of themselves like this:
id obj = [decoder decodeObjectForKey:@"myKey"];
if (![obj isKindOfClass:[MyClass class]]) { /* ...fail... */ }
This technique is potentially unsafe because by the time you can verify the class type, the object has already been constructed, and if this is part of a collection class, potentially inserted into an object graph.
So any usages of -[NSCoder decodeObjectForKey:]
would trigger a secure coding
violation. The docs for NSCoder.decodingFailurePolicy
provide further examples:
A decode call can fail for the following reasons:
…snip…
A secure coding violation occurs. This happens when you attempt to decode an object that doesn’t conform to
NSSecureCoding
. This also happens when the encoded type doesn’t match any of the types passed todecodeObject(of:forKey:)
.
How to Debug
Violations can now arise in any -restoreStateWithCoder:
implementations,
so they need to be audited.
- Check for any usages of
-[NSCoder decodeObjectForKey:]
.- Replace with the appropriate secure variants.
- At the end of
-restoreStateWithCoder:
, check the value ofNSCoder.error
property.- If it’s non-
nil
, an error must have occurred earlier.
- If it’s non-