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.
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.