Safari bug: can't enable extensions on Catalina

March 25 2020 by Jeff Johnson

In macOS 10.15.3, Apple introduced a bug that can prevent you from enabling or disabling Safari extensions. In order to enable or disable an extension, you must click the checkbox next to the extension in the Extensions pane of Safari Preferences. The support site for my own extension StopTheMadness has instructions and screenshots explaining how to enable a Safari extension, but under normal circumstances it's relatively easy. When the bug occurs, however, then clicking the checkbox does nothing: the checkbox doesn't get checked, and the extension doesn't get enabled either. This is a serious bug affecting many users and all Safari extensions by every extension developer. Unfortunately, the bug was not fixed in macOS 10.15.4, which was released yesterday, so we're still stuck with the bug for the immediate future. The purpose of this blog post is to raise awareness about the bug, for Safari extension users, Safari extension developers, and the Safari engineering team itself.

There is some good news. First, the bug does not affect macOS 10.14 Mojave or earlier. Second, on Catalina you can usually work around the bug, with some effort. The bug is somewhat haphazard and doesn't occur all the time. Sometimes it helps just to move the Safari Preferences window to a different area of your screen. You can also switch to other preference panes and then switch back to the Extensions pane. If that doesn't solve the problem, open System Preferences to the Security & Privacy pane, select the Privacy tab, and select the Accessibility item in the table. Apps in the Accessibility list have been known to trigger the bug, so try temporarily quitting and/or disabling any apps that can capture mouse clicks or key presses on your Mac.

I believe that this Safari bug was introduced by Apple in a bungled attempt to prevent extensions from getting enabled via "synthetic clicks." As described by Mac security researcher Patrick Wardle, a synthetic click is a mouse click generated automatically by malware rather than manually by the user. In theory it may be a good idea for Apple to prevent synthetic clicks in this case, but in practice the code that Apple shipped here was buggy and caused more harm than help. Safari incorrectly identifies real user clicks as fake synthetic clicks, preventing users from enabling their installed Safari extensions.

Appendix: Technical Details

The appendix (which is longer than the main section of this blog post LOL) will describe the technical details underlying the bug, for those who may be interested. In order to track down the cause of the bug, I started by watching the Console log while reproducing the bug in Safari. This was rather difficult, because nowadays macOS logs voluminously to the Console. So it was like trying to find a needle in a haystack. (I remember the old days of Mac OS X when I could leave Console running with the preferences set to bounce the app in the Dock when a new message was logged, and that wasn't too annoying. Today it would be impossible, though, because Console would never stop bouncing!) Luckily, I did find the needle in the haystack. Every time I clicked the checkbox, and the checkbox failed to check, I saw a message from Safari in Console: "Not enabling extension due to security measures". Bingo! I searched the contents of the files on my system for that string, and I found it in System/Library/PrivateFrameworks/Safari.framework/Safari. Here's the disassembly, courtesy of my command-line tool riptool, which displays RIP-relative addresses in the output of /usr/bin/otool.

-[ExtensionWrapper enable].cold.1:
0000000000564972	pushq	%rbp
0000000000564973	movq	%rsp, %rbp
0000000000564976	subq	$0x10, %rsp
000000000056497a	movq	%rdi, %rsi
000000000056497d	leaq	-0x10(%rbp), %r8
0000000000564981	andw	$0x0, (%r8)
0000000000564986	leaq	-0x56498d(%rip) [0x0], %rdi
000000000056498d	leaq	0x11781c(%rip) [0x67c1b0], %rcx ## literal pool for: "Not enabling extension due to security measures"
0000000000564994	pushq	$0x10
0000000000564996	popq	%rdx
0000000000564997	pushq	$0x2
0000000000564999	popq	%r9
000000000056499b	callq	0x56b168 ## symbol stub for: __os_log_error_impl
00000000005649a0	addq	$0x10, %rsp
00000000005649a4	popq	%rbp
00000000005649a5	retq

The cold.1 above is a hot/cold splitting optimization, which is described in a talk from the 2019 LLVM Developers Meeting. Ironically, this was supposed to be a cold, little used path, but it turns out to be a hot, overly used path, which is the bug. I won't show the full method -[ExtensionWrapper enable], due to length, but -[ExtensionsPreferencesOld canEnableExtensions] returning NO is ultimately what leads to the "cold" path and the log message I see in Console:

-[ExtensionWrapper enable]:
00000000003150bb	pushq	%rbp
00000000003150bc	movq	%rsp, %rbp
00000000003150bf	pushq	%r15
00000000003150c1	pushq	%r14
00000000003150c3	pushq	%r12
00000000003150c5	pushq	%rbx
00000000003150c6	movq	%rdi, %r14
00000000003150c9	movq	0x5eb048(%rip) [0x900118], %rdi ## Objc class ref: ExtensionsPreferencesOld
00000000003150d0	movq	0x5ce761(%rip) [0x8e3838], %rsi ## Objc selector ref: sharedInstance
00000000003150d7	movq	0x46c27a(%rip) [0x781358], %r12 ## Objc message: +[ExtensionsPreferencesOld sharedInstance]
00000000003150de	callq	*%r12
00000000003150e1	movq	%rax, %rdi
00000000003150e4	callq	0x56b462 ## symbol stub for: _objc_retainAutoreleasedReturnValue
00000000003150e9	movq	%rax, %r15
00000000003150ec	movq	0x5ddd25(%rip) [0x8f2e18], %rsi ## Objc selector ref: canEnableExtensions
00000000003150f3	movq	%rax, %rdi
00000000003150f6	callq	*%r12
00000000003150f9	movl	%eax, %ebx
00000000003150fb	movq	%r15, %rdi
00000000003150fe	callq	*0x46c25c(%rip) [0x781360] ## literal pool symbol address: _objc_release
0000000000315104	testb	%bl, %bl
0000000000315106	je	0x315143
0000000000315108	cmpq	$0x0, 0x8(%r14)
000000000031510d	je	0x315165
000000000031510f	movq	0x5ea60a(%rip) [0x8ff720], %rdi ## Objc class ref: SafariAppExtensionsController
0000000000315116	movq	0x5cd3ab(%rip) [0x8e24c8], %rsi ## Objc selector ref: sharedController
000000000031511d	callq	*%r12
0000000000315120	movq	%rax, %rdi
0000000000315123	callq	0x56b462 ## symbol stub for: _objc_retainAutoreleasedReturnValue
0000000000315128	movq	%rax, %r15
000000000031512b	movq	0x8(%r14), %rdx
000000000031512f	movq	0x5d56d2(%rip) [0x8ea808], %rsi ## Objc selector ref: setExtension:isEnabled:
0000000000315136	movq	%rax, %rdi
0000000000315139	movl	$WBSLocalizableStringFlagPluralRules, %ecx
000000000031513e	callq	*%r12

Compare with the non-buggy implementation in Safari.framework on macOS 10.14.6 Mojave, which doesn't call -[ExtensionsPreferencesOld canEnableExtensions] at all:

-[ExtensionWrapper enable]:
0000000000358745	pushq	%rbp
0000000000358746	movq	%rsp, %rbp
0000000000358749	pushq	%r15
000000000035874b	pushq	%r14
000000000035874d	pushq	%r12
000000000035874f	pushq	%rbx
0000000000358750	movq	%rdi, %rbx
0000000000358753	movq	_OBJC_IVAR_$_ExtensionWrapper._appExtension(%rip), %r15
000000000035875a	cmpq	$0x0, (%rdi,%r15)
000000000035875f	je	0x35879c
0000000000358761	movq	0x65b8e8(%rip) [0x9b4050], %rdi ## Objc class ref: SafariAppExtensionsController
0000000000358768	movq	0x63e879(%rip) [0x996fe8], %rsi ## Objc selector ref: sharedController
000000000035876f	movq	0x4d1b8a(%rip) [0x82a300], %r12 ## Objc message: +[SafariAppExtensionsController sharedController]
0000000000358776	callq	*%r12
0000000000358779	movq	%rax, %rdi
000000000035877c	callq	0x5d7836 ## symbol stub for: _objc_retainAutoreleasedReturnValue
0000000000358781	movq	%rax, %r14
0000000000358784	movq	(%rbx,%r15), %rdx
0000000000358788	movq	0x64f859(%rip) [0x9a7fe8], %rsi ## Objc selector ref: setExtension:isEnabled:
000000000035878f	movl	$WBSLocalizableStringFlagPluralRules, %ecx
0000000000358794	movq	%rax, %rdi
0000000000358797	callq	*%r12

The method canEnableExtensions checks for some kind of occlusion of the window. This is the code that causes the bug:

-[ExtensionsPreferencesOld canEnableExtensions]:
000000000030ce6c	pushq	%rbp
000000000030ce6d	movq	%rsp, %rbp
000000000030ce70	pushq	%rbx
000000000030ce71	pushq	%rax
000000000030ce72	movq	_OBJC_IVAR_$_ExtensionsPreferencesOld._windowIsOccluded(%rip), %rax
000000000030ce79	cmpb	$0x0, (%rdi,%rax)
000000000030ce7d	je	0x30ce88
000000000030ce7f	xorl	%eax, %eax
000000000030ce81	addq	$0x8, %rsp
000000000030ce85	popq	%rbx
000000000030ce86	popq	%rbp
000000000030ce87	retq
000000000030ce88	movq	%rdi, %rbx
000000000030ce8b	movq	_OBJC_IVAR_$_ExtensionsPreferencesOld._occlusionDetectionView(%rip), %rax
000000000030ce92	movq	(%rdi,%rax), %rdi
000000000030ce96	movq	_OBJC_IVAR_$_ExtensionsPreferencesOld._occlusionValidationToken(%rip), %rax
000000000030ce9d	movq	(%rbx,%rax), %rdx
000000000030cea1	movq	0x5e5cd8(%rip) [0x8f2b80], %rsi ## Objc selector ref: validateNoOcclusionSinceToken:
000000000030cea8	callq	*0x4744aa(%rip) [0x781358] ## Objc message: -[%rdi validateNoOcclusionSinceToken:]
000000000030ceae	testb	%al, %al
000000000030ceb0	je	0x30ce7f
000000000030ceb2	movq	_OBJC_IVAR_$_ExtensionsPreferencesOld._cursorAssertion(%rip), %rax
000000000030ceb9	movq	(%rbx,%rax), %rdi
000000000030cebd	movq	0x5d5f7c(%rip) [0x8e2e40], %rsi ## Objc selector ref: isValid
000000000030cec4	addq	$0x8, %rsp
000000000030cec8	popq	%rbx
000000000030cec9	popq	%rbp
000000000030ceca	jmpq	*0x474488(%rip) [0x781358] ## Objc message: -[%rdi isValid]

I don't know exactly what's going wrong with the synthetic click test; that's for Apple engineers to figure out. The easiest way for Apple to fix the Safari extensions bug is simply to remove this new method and go back to the old, working code from Mojave, at least until they've figured out how to reliably differentiate between real and synthetic clicks.

The other issue here is the lack of a visible error message for the user. I found a message buried among thousands of other unrelated messages in Console log, but no normal Safari user will ever see that. There ought to be a warning in the Safari Preferences window. Silent failure is a security failure. If an unauthorized process were in fact trying to secretly enable Safari extensions, shouldn't the user be made aware of that immediately? Users can't stop malware if they don't even know the malware exists.

Jeff Johnson (My apps, PayPal.Me)