How I Discovered CVE-2026-28976: A macOS Login Password Leak in the loginwindow–UserAccountUpdater Handoff
July 31, 2026 · David Ige

Publication status: Publicly disclosed. Apple fixed this issue in macOS Tahoe 26.5, released May 11, 2026.
At a Glance
| CVE | CVE-2026-28976 |
| Affected component | UserAccountUpdater / the loginwindow account-update handoff |
| Directly demonstrated impact | An ordinary same-user process recovered the real macOS login password |
| Apple's published impact | Potential root privilege gain by an app |
| Validated environment | macOS 26.4, build 25E5233c |
| Fixed release | macOS Tahoe 26.5 |
| Fix description | Additional validation for an information leak |
Introduction
In March 2026, while researching cross-process LocalAuthentication context transfer on macOS, I found a system login path that delegated much more than an authentication result.
When loginwindow launched UserAccountUpdater on the conditional post-update account-migration path after a password-based login, it created a LACSExtractablePassword object for the user's real account password. That wrapper carried an externalized reference to a live LocalAuthentication context. loginwindow serialized the wrapper and placed it in UserAccountUpdater's command-line arguments.
The command line did not visibly contain the plaintext password. It contained something nearly as sensitive: a bearer reference to a context whose credential API returned the plaintext password.
Because another process running as the same user could observe the transient UserAccountUpdater command line, it could obtain the reference, import the live context, and retrieve the original password bytes. The observing process did not need a LocalAuthentication entitlement, code injection, or control of either Apple system process.
How I Got Here
The securityd research that came first
Before I found CVE-2026-28976, I had been deep in a different macOS security problem:
securitydextractMasterKeylets public userland recover reusable keychain database secrets
That research started in the legacy Security.framework CSSM/CSPDL surface. I found that an ordinary local process could authenticate a keychain, reach securityd's extractMasterKey path, ask for the returned reference key to remain extractable, and then export the underlying 24-byte keychain master key.
I kept pushing because a returned key object alone did not establish the real impact. I used the exported key offline against a copied modern keychain DbBlob, decrypted and authenticated its protected section, recovered the database encryption and signing keys, unwrapped the matching secure-storage group key, and recovered a controlled generic-password value from the copied database. I repeated the result against both a disposable keychain and the controlled account's real login/default keychain.
The evidence also established an important limit. This was not an authentication bypass. securityd forced the database through lockDb() and makeUnlocked(false), and the no-credential follow-ups failed. The clean finding was that once a process had valid keychain unlock material, it could turn that one authentication event into durable, database-level secrets usable outside securityd.
After I reported that issue to Apple, its authentication boundary left an obvious technical question: where might malicious same-user software obtain the real password needed to drive that path?
The morning the direction changed
One morning, I was in the shower worshiping God when the Holy Spirit said, “Look deeper. There is a deeper vulnerability in there.”
I got out of the shower and did my workout, but my mind was already at the Mac. I was so excited by the direction that I could barely complete all of my bench press sets. Eventually I settled in at my Mac and started pulling on the thread.
I did not begin that morning knowing about UserAccountUpdater. The question was much broader: after the user enters a password, where does that credential go, and does any part of the login flow transfer more authority than it should?
Following the thread
I had already established a useful LocalAuthentication primitive. While a producer kept an LAContext alive, a second process could import its 16-byte external form and recover credentials stored under types -5 and -9: LACCredentialExtractablePasscode and LACCredentialExtractablePassword.
That was technically interesting, but it was not yet a vulnerability with a real victim. I needed to find an Apple system process that used externalized contexts while handling an actual password.
I searched the macOS application and daemon corpus for consumers of externalizedContext and initWithExternalizedContext:. The first shortlist included SecurityAgent, authorizationhost, findmydeviced, mdmclient, and loginwindow. Filtering those binaries for password and credential-setting behavior made loginwindow stand out. The same binary contained:
LACSExtractablePassword
setCredential:type:error:
externalizedContext
_authSuccessUsingPassword:forUser:
launchUserAccountUpdaterIfNecessaryWithPassword:
The tools did not immediately give up the answer. Direct symbol searches on loginwindow returned nothing useful. Broad disassembly searches did not recover the method labels I needed. Objective-C metadata attempts also came back empty. The binary was stripped enough that the obvious static routes kept cooling off.
The breakthrough came from the system's own history. I searched the research Mac's unified logs for a real, non-test process setting an extractable password credential. A March 10 event showed:
setCredential:type:-9
The caller was PID 477. That process was loginwindow.
A tight log slice connected the event to UAUEnvUserPassword, the UserAccountUpdater launch, and the updater's connection to the same externalized context.
The remaining work was to prove that an unrelated process could exploit the handoff as it happened. The archived password wrapper contained the raw 16-byte externalized context, so I built an independent same-user observer and drove a fresh login cycle.
The first attempt was instructive but incomplete: loginwindow created the password-bearing context, yet shouldLaunchUpdate was false and the updater did not run. That exposed the account-update condition as the missing piece. On the next controlled cycle, the updater launched and an observer already waiting in a separate SSH session captured its transient argument vector.
The imported context reported that credential type -9 was present and returned an _NSInlineData object. I then authenticated those recovered bytes against the local account directory. The authentication succeeded.
The finding also brought the research back to where it began. The securityd extractMasterKey finding showed what could happen once malicious local software possessed valid keychain authentication material. CVE-2026-28976 showed how the system login flow itself could expose that material.
In an automated chained run, I used the recovered login password to create and authenticate a new keychain, then drove the securityd path to export its 24-byte master key and recover a controlled stored secret offline. I had validated the same securityd primitive separately against the account's real login/default keychain.
The Components
loginwindow
loginwindow drives the graphical login flow and creates the user's session. It legitimately receives the account password during password-based authentication. The vulnerable producer path lived in:
-[UserAccountUpdater launchUserAccountUpdaterIfNecessaryWithPassword:]
Here, UserAccountUpdater is also the name of a helper class inside loginwindow, not only the standalone executable.
LACSExtractablePassword
LACSExtractablePassword is the password wrapper used for this handoff. In the observed path, constructing the wrapper caused LocalAuthentication to:
- create a fresh
LAContext; - externalize the context;
- store the supplied bytes as credential type
-9; and - retain the externalized context so the intended consumer could reconnect.
/System/Library/CoreServices/UserAccountUpdater
The standalone utility consumes launch metadata and loads account-migration plugins after an operating-system update. It base64-decodes and securely unarchives the LACSExtractablePassword object supplied by loginwindow.
The intended consumer is highly trusted. Its code signature includes private LocalAuthentication entitlements such as:
<key>com.apple.private.LocalAuthentication.ExtractCredential</key>
<true/>
<key>com.apple.private.LocalAuthentication.SaveExtractableCredential</key>
<true/>Those entitlements make sense for the intended system utility. The problem was that possession of the externalized context reference—not proof of being that intended utility—was enough for an unrelated process to reconnect to the live credential state.
LocalAuthentication and coreauthd
LAContext is the client-side object representing authentication state managed by macOS's LocalAuthentication services. An externalized context is meant to let another process reconnect to an existing operation.
In this case, the shared state included an extractable password. The external form therefore acted as a capability to access the password, not merely as a correlation identifier.
The Vulnerable Data Flow
User enters the macOS account password
|
v
loginwindow
receives the real password
enters the post-update account path
|
v
LACSExtractablePassword
creates a fresh LAContext
stores credential type -9
externalizes the live context
|
| serialized wrapper in launch arguments
v
UserAccountUpdater
base64-decodes the argument
unarchives the wrapper
consumes the password for migration work
^
|
unrelated same-user process
observes transient argv
imports the same live context
retrieves credential type -9
|
v
real macOS login password
Producer-Side Callsite
The following is simplified pseudocode reconstructed from the arm64e loginwindow callsite; it is not Apple source:
NSData *passwordData =
[password dataUsingEncoding:NSUTF8StringEncoding];
LACSExtractablePassword *wrapped =
[[LACSExtractablePassword alloc] initWithData:passwordData
error:&error];
if (wrapped != nil) {
launchEnvironment[UAUEnvUserPassword] = wrapped;
}This detail is important. The caller did not hand an existing authentication result to the updater. It converted the password string to bytes and created a wrapper specifically designed to preserve an extractable password across the process boundary.
The live system trace tied that callsite to LocalAuthentication activity:
loginwindow: shouldLaunchUpdate = YES
loginwindow: externalizedContext on LAContext[...]
coreauthd: setCredential:type:-9 on ContextProxy[...]
loginwindow: UAUEnvUserPassword: <LACSExtractablePassword ...>
loginwindow: UAULaunch CLI Arguments
Consumer-Side Callsite
The UserAccountUpdater entry point performed the inverse operation. The relevant arm64e calls reconstruct to the following high-level shape:
NSData *archive =
[[NSData alloc] initWithBase64EncodedString:passwordArgument
options:0];
LACSExtractablePassword *wrapped =
[NSKeyedUnarchiver
unarchivedObjectOfClass:LACSExtractablePassword.class
fromData:archive
error:&error];
[sessionParameters setPassword:wrapped];Using unarchivedObjectOfClass:fromData:error: constrained the archive's top level to the expected class. That is good deserialization hygiene, but it did not solve the confidentiality problem. The expected object itself contained a reference to live password-bearing state, and the archive traveled over a channel visible to other same-user processes.
Conceptually, the serialized object graph looked like:
LACSExtractablePassword
└── password: LACSPassword
└── context: externalized LAContext reference
The externalized reference observed in the validation run was 16 bytes.
Why an Opaque Context Was Still a Secret
It is tempting to classify the command-line value as harmless because it was base64-encoded serialized data rather than readable password text. That view confuses representation with authority.
The externalized context was a bearer capability:
- it identified live authentication state;
- the state contained
LACCredentialExtractablePassword; - a process that knew the reference could import the context; and
- the imported context's credential API returned the stored bytes.
No cryptographic break was required. The system authentication service performed the retrieval because the externalized reference was accepted as sufficient authority.
This is the same security principle that applies to session cookies, password reset links, cloud access keys, and IPC endpoint capabilities: an opaque value is still a secret when possession of it grants sensitive authority.
Why Process Arguments Were the Wrong Transport
Process arguments are launch configuration, not confidential IPC. In validation, an unentitled, unsandboxed same-user observer remained alive across the graphical login through a separate SSH session and read the live UserAccountUpdater argument vector using standard process inspection.
The vulnerable value was transient, but transience is not an access-control boundary. Software already running in that position can watch for short-lived processes, and monitoring software routinely records process creation metadata.
End-to-End Validation
I validated the issue on a real Mac running macOS 26.4 build 25E5233c with a controlled lab account.
The redacted result was:
observed_process = UserAccountUpdater
serialized_password_wrapper = <redacted>
imported_context_class = LAContext
credential_type_-9_present = yes
returned_credential_class = _NSInlineData
returned_credential_bytes = <redacted>
account_authentication_check = success
The final check authenticated the recovered bytes against the local account directory. That established that the returned value was the real account password, not an unrelated token, hash, placeholder, or test value produced by the probe.
This result was not inferred solely from disassembly. Static analysis explained the path; the real macOS login flow, process launch, LocalAuthentication service, and account authentication check established the impact.
Context Lifetime and Reliability
The externalized value was not a permanent password token. In validation:
- another process could import the context while a live holder still existed;
- an imported holder could continue using the adopted context after the original producer exited; but
- after every holder was gone, a fresh import failed with
ACMContextCreateWithExternalForm failed.
The vulnerability was therefore timing-sensitive but not race-the-scheduler fragile. An observer needed to catch the short-lived launch and adopt the context before the final live reference disappeared. Once adopted, the attacker process held the live state itself.
The account-update path was also conditional: it ran when macOS determined that post-update user-account work was required. The issue was not exposed on every login.
Security Impact
The demonstrated result was disclosure of the real macOS login password to an ordinary process already running as the same user.
For an administrator account authorized to use sudo, this creates a direct path to root: the recovered password can satisfy sudo authentication. That is the impact described in Apple's security advisory. If the affected account is a standard user, password disclosure alone does not grant root privileges.
Regardless of account type, the recovered password may also unlock protected resources or other services where the user reused the same secret. Those consequences depend on the user's configuration.
What Apple Fixed
Apple released macOS Tahoe 26.5 on May 11, 2026. The official security-content page states:
UserAccountUpdater
Impact: An app may be able to gain root privileges
Description: An information leakage was addressed with additional validation.
How This Class of Bug Should Be Prevented
Sensitive authentication state crossing a process boundary needs both a confidential channel and strong recipient authorization.
A robust design should:
- transport password-bearing state over authenticated IPC rather than process arguments;
- bind any transferable context to the intended recipient's audit token, code identity, or equivalent unforgeable principal;
- reject imports from every other process even if it knows the external form;
- prefer a narrow, operation-specific authorization result over a transferable plaintext credential;
- make transfer capabilities single-use and short-lived; and
- prevent reusable authentication capabilities from entering process metadata, logs, crash records, or analytics.
Changing the encoding or making the argument harder to recognize would not have fixed the underlying flaw. The security property that mattered was whether an unintended process could exercise the authority represented by the value.
Disclosure Timeline
- March 11, 2026: End-to-end behavior validated on macOS.
- March 2026: Vulnerability reported to Apple.
- May 11, 2026: Apple released macOS Tahoe 26.5 with the fix and published CVE-2026-28976.
- July 2026: Detailed public-disclosure article prepared.