https://foon.uk/fixing-quicklook/
[foon]
[title]
After upgrading from Mojave to Ventura, I noticed that Apple had
changed QuickLook.
QuickLook is one of my favourite pieces of software. You just select
a file in the Finder, press space, and the file pops up on your
screen. Then you press space again and it goes away.
Focus stays in the Finder, so you can use the arrow keys to select
different files, and the QuickLook window updates to show them to
you.
It's simple, well-designed, fast, tasteful and just all around
excellent.
Until now.
For whatever reason, QuickLook will now remove the corners of your
images before showing them to you.
[mojave-light] QuickLook in Mojave [big-sur-light] QuickLook in
Ventura
It doesn't matter if they're photos, game assets, or UI elements
you're designing. Everything will be rounded off before you see it.
Naturally, I searched around for the secret switch to make it stop.
Apple is usually pretty good about letting us opt out of their latest
improvements, so there has to be something, right? An accessibility
setting? A cheeky defaults.write? As far as I can tell, there isn't.
So, I guess we're out of luck?
No. Don't think like that. It might be a Mac but it's still your
computer. Let's fix this.
QuickLook headers
If we want to mess with how QuickLook displays things, we need to get
at its window. Then we can hopefully poke around in the views and
change how they draw.
The QuickLook API is public and documented, and if you look through
the docs you'll see that this window is called QLPreviewPanel,
there's one instance of it per app, and you retrieve it with
[QLPreviewPanel sharedPreviewPanel].
Debugging Finder
I'd only ever used QuickLook from Finder, so that was where I thought
to start. Let's bust out the debugger and see what Finder's up to:
$ lldb -n Finder
error: process exited with status -1 (attach failed (Not allowed to
attach to process.))
Actually, first, let's deal with SIP. SIP is a new Apple technology
that helps you out by stopping you from reading and writing memory on
your own computer. In order to fix problems in system processes like
this, we have to turn it off and reboot.
Alright, let's try that again:
$ lldb -n Finder
Process 4040 stopped
thread #1, queue = 'com.apple.main-thread', stop reason = signal
SIGSTOP
frame #0: 0x000000018df1bf54 libsystem_kernel.dylib`
mach_msg2_trap + 8
libsystem_kernel.dylib`mach_msg2_trap:
-> 0x18df1bf54 <+8>: ret
libsystem_kernel.dylib`macx_swapon:
0x18df1bf58 <+0>: mov x16, #-0x30
0x18df1bf5c <+4>: svc #0x80
0x18df1bf60 <+8>: ret
Target 0: (Finder) stopped.
Executable module set to "/System/Library/CoreServices/Finder.app/
Contents/MacOS/Finder".
Architecture set to: arm64e-apple-macosx-.
We're in to our own machine.
Let's try setting a breakpoint on sharedPreviewPanel.
(lldb) break set -n sharedPreviewPanel
Breakpoint 5: where = QuickLookUI`+[QLPreviewPanel
sharedPreviewPanel], address = 0x00000001f81c025c
(lldb) c
Navigate to an image file, press space, and:
Process 4040 stopped
thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint
5.1
frame #0: 0x00000001f81c025c QuickLookUI`+[QLPreviewPanel
sharedPreviewPanel]
QuickLookUI`+[QLPreviewPanel sharedPreviewPanel]:
-> 0x1f81c025c <+0>: pacibsp
0x1f81c0260 <+4>: sub sp, sp, #0x40
0x1f81c0264 <+8>: stp x22, x21, [sp, #0x10]
0x1f81c0268 <+12>: stp x20, x19, [sp, #0x20]
LLDB has stopped the Finder process in the sharedPreviewPanel
function. Let's get the return value, which should be the preview
panel instance:
(lldb) finish
(lldb) po $x0
Alright! We're getting somewhere. Let's see what's in this panel:
(lldb) po [$x0 recursiveDescription]
[ A w ] h=--- v=--- NSNextStepFrame 0x12cea29b0 f=(0,0,370,223) b
=(-) => <_NSViewBackingLayer: 0x600003f7fcf0>
[ A w ] h=-&- v=-&- QLPreviewBackgroundView 0x11e00df90 f=
(0,0,370,223) b=(-) => <_NSViewBackingLayer: 0x600003f7fc60>
[ A w ] h=--- v=--- NSView 0x11e040ec0 f=(0,0,370,223) b=(-)
=> <_NSViewBackingLayer: 0x600003f7fc00>
[ A w ] h=--- v=--- NSView 0x11e047f50 f=(5,5,360,180) b=(-)
=> <_NSViewBackingLayer: 0x600003f7f9f0>
[ A w ] h=-&- v=-&- NSView 0x11cf35780 f=(0,0,360,180) b=
(-) => <_NSViewBackingLayer: 0x600003f7f960>
[ A w ] h=-&- v=-&- NSView 0x11cf31c50 f=(0,0,360,180) b
=(-) => <_NSViewBackingLayer: 0x600003f7f8d0>
[ A w ] h=-&- v=-&- NSView 0x11cf31fb0 f=
(0,0,360,180) b=(-) => <_NSViewBackingLayer: 0x600003f7f840>
[ A w ] h=-&- v=-&- QLPanelPreviewView 0x11cf35d10 f
=(0,0,360,180) b=(-) => <_NSViewBackingLayer: 0x600003f7f4e0>
[ A W ] h=-&- v=-&- QLPreviewContainerView
0x11cf360d0 f=(0,0,360,180) b=(-) =>
[ A W ] h=--- v=--- NSRemoteView 0x119c3a040 f=
(0,0,360,180) b=(-) =>
[ A W ] h=--- v=--- QLPreviewTitleBarView 0x11e0482b0 f=
(8,189,354,30) b=(-) => <_NSViewBackingLayer: 0x600003fbaf70>
[ AF w ] h=--- v=--- QLPreviewWindowButton 0x11cf35300
"Button" f=(4,9,12,12) b=(-) => <_NSViewBackingLayer: 0x600003f7fa80>
[ A V W ] h=--- v=--- NSButtonImageView 0x11cfaeca0 f=
(0,0,12,12) b=(-) => <_NSViewBackingLayer: 0x600003f7f2a0>
[ AF w ] h=--- v=--- QLPreviewWindowButton 0x11cf31310
"Button" f=(24,9,12,12) b=(-) => <_NSViewBackingLayer:
0x600003f7fb40>
[ A V W ] h=--- v=--- NSButtonImageView 0x11cfaef00 f=
(0,0,12,12) b=(-) => <_NSViewBackingLayer: 0x600003f7f390>
[ A w ] h=--- v=--- NSView 0x11e048d80 f=(114,4,240,22) b
=(-) => <_NSViewBackingLayer: 0x600003f7a310>
[ A w ] h=--- v=--- QLControlsCenteringView 0x11cfa9960
f=(0,0,240,22) b=(-) => <_NSViewBackingLayer: 0x600003f7a280>
[ A w ] h=--- v=--- QLControlsContainerView
0x11cfa9ed0 f=(0,0,240,22) b=(-) => <_NSViewBackingLayer:
0x600003f7a220>
[ AF w ] h=--- v=--- QLControlSegmentedControl
0x11cf630a0 f=(38,-2,33,25) b=(-) => <_NSViewBackingLayer:
0x600003ced470>
[ AF V W ] h=--- v=--- NSSegmentItemView 0x11cf63560
f=(0,0,33,25) b=(-) => <_NSViewBackingLayer: 0x600003ced500>
[ A V W ] h=--- v=--- NSSegmentItemBezelView
0x11cf63840 f=(0,0,33,25) b=(-) => <_NSViewBackingLayer:
0x600003f80d80>
[ A w ] h=--- v=--- NSSegmentItemImageView
0x11cf63bb0 f=(10,1.5,15,17) b=(-) => <_NSViewBackingLayer:
0x600003ced410>
[ AF w ] h=--- v=--- QLControlSegmentedControl
0x11cf5b880 f=(-1,-2,33,25) b=(-) => <_NSViewBackingLayer:
0x600003cec690>
[ AF V W ] h=--- v=--- NSSegmentItemView 0x11cf5bd40
f=(0,0,33,25) b=(-) => <_NSViewBackingLayer: 0x600003ced530>
[ A V W ] h=--- v=--- NSSegmentItemBezelView
0x11cf5c020 f=(0,0,33,25) b=(-) => <_NSViewBackingLayer:
0x600003f80060>
[ A w ] h=--- v=--- NSSegmentItemImageView
0x11cf5c390 f=(9,4,15,15) b=(-) => <_NSViewBackingLayer:
0x600003ceccc0>
[ AF w ] h=--- v=--- QLControlSegmentedControl
0x11cf53ee0 f=(77,-2,33,25) b=(-) => <_NSViewBackingLayer:
0x600003cee370>
[ AF V W ] h=--- v=--- NSSegmentItemView 0x11cf543a0
f=(0,0,33,25) b=(-) => <_NSViewBackingLayer: 0x600003ced110>
[ A V W ] h=--- v=--- NSSegmentItemBezelView
0x11cf54680 f=(0,0,33,25) b=(-) => <_NSViewBackingLayer:
0x600003f80270>
[ A w ] h=--- v=--- NSSegmentItemImageView
0x11cf549f0 f=(9.5,2,15,17) b=(-) => <_NSViewBackingLayer:
0x600003cec9c0>
[ AF w ] h=--- v=--- QLControlSegmentedControl
0x11cf4c8c0 f=(116,-2,125,25) b=(-) => <_NSViewBackingLayer:
0x600003f784e0>
[ AF V W ] h=--- v=--- NSSegmentItemView 0x11cf4cc70
f=(0,0,125,25) b=(-) => <_NSViewBackingLayer: 0x600003f78720>
[ A V W ] h=--- v=--- NSSegmentItemBezelView
0x11cf4cf50 f=(0,0,125,25) b=(-) => <_NSViewBackingLayer:
0x600003f83000>
[ AF w ] h=--- v=--- NSSegmentItemLabelView
0x11cf4d650 "Open with Preview" f=(2,3,121,16) b=(-) =>
[ AF w ] h=--- v=--- NSTextField 0x11e04cda0 "Screenshot
2023-08-28 at 16.21.26" f=(42,8,66,16) b=(-) =>
A=autoresizesSubviews, C=canDrawConcurrently, D=needsDisplay, F=
flipped, G=gstate, H=hidden (h=by ancestor), L=needsLayout (l=child
needsLayout), U=needsUpdateConstraints (u=child
needsUpdateConstraints), O=opaque, P=
preservesContentDuringLiveResize, S=scaled/rotated, W=wantsLayer (w=
ancestor wantsLayer), V=needsVibrancy (v=allowsVibrancy), #=has
surface
This is pretty juicy. We can probably ignore QPreviewTitleBarView and
its children, so let's focus on the other branch of the tree. There's
a QLPreviewBackgroundView, then a bunch of NSViews, and then a
QLPanelPreviewView, a QLPreviewContainerView, and right at the very
top, an NSRemoteView.
NSRemoteView
I haven't come across NSRemoteView before and I'm not really sure
what it is. There doesn't seem to be a public header for it. I wonder
how it works?
Well, we might be getting ahead of ourselves here. We still don't
know if the NSRemoteView has anything to do with our image. Let's
hide it and see if our image disappears.
(lldb) po [(NSRemoteView *)0x119c3a040 setHidden:YES]
(lldb) c
[blank-light]
Indeed it does. So it's this NSRemoteView that's actually displaying
our image. The "remote" makes me think it's communicating with
another process which is doing the actual rendering, so we'll
probably have to look inside the remote process to see what it's
doing. But which process is that? Perhaps the NSRemoteView can tell
us? Let's see what it can do:
(lldb) po [[NSRemoteView class] fp_shortMethodDescription]
:
in NSRemoteView:
Class Methods:
+ (void) initialize; (0xd465800195a3a894)
+ (void) observeValueForKeyPath:(id)arg1 ofObject:(id)arg2 change:
(id)arg3 context:(void*)arg4; (0xea63800195a944bc)
+ (id) _findFirstKeyViewInDirection:(unsigned long)arg1
forKeyLoopGroupingView:(id)arg2; (0x3679000195a95140)
+ (BOOL) automaticallyNotifiesObserversOfTouchBar;
(0xc10a800195ad0774)
+ (const __CFString*) privateRunLoopMode; (0x7101800195a95148)
+ (CGColor*) _warningColorCG; (0x2119800195aadb38)
+ (void) initAll; (0x932a000195a3ad40)
+ (id) _warningColorNS; (0x7d7d000195aadbbc)
+ (BOOL) _appModalSessionWouldBeSafe:(BOOL)arg1; (0x9c39800195a9b638)
+ (id) _remoteViewForIdentifier:(id)arg1; (0xda2a800195a94dc4)
+ (BOOL) allowSetObjectForKey:(id)arg1 bridge:(id)arg2 bridgePhase:
(unsigned char)arg3 withReply:(^block)arg4; (0x6272000195aa1b44)
+ (BOOL) anyRemoteViewAttachedToWindow:(id)arg1; (0x285f800195a94c14)
+ (void) deferBlockOntoMainThread:(^block)arg1; (0xe1c800195a9514c)
+ (BOOL) isFakeEvent:(id)arg1; (0x4b56800195ab2860)
+ (id) remoteViewsAttachedToWindow:(id)arg1; (0xe551800195a94a14)
+ (void) rendezvousWindow:(unsigned char)arg1 kind:(unsigned char)
arg2 spawnedBy:(id)arg3 styleMask:(unsigned long)arg4 contentRect:
(CGRect)arg5 identifier:(id)arg6 listenerEndpoint:(id)arg7
completion:(^block)arg8; (0x8501800195aa4da0)
+ (Class) rendezvousWindowClass:(Class)arg1; (0x6316800195a99d48)
Properties:
@property (retain, nonatomic) NSAccessibilityRemoteUIElement*
accessoryViewAccessibilityParent;
@property (readonly, nonatomic) NSRemoteView* spawnedBy;
@property (readonly) CGPoint requestedOrigin;
@property (readonly) BOOL _isAssociated;
@property NSObject* delegate;
@property (retain) NSView* accessoryView;
@property (readonly) BOOL isValid;
@property (copy, nonatomic) NSString* serviceName;
@property (retain, nonatomic) NSUUID* serviceInstanceIdentifier;
(@synthesize serviceInstanceIdentifier = _serviceInstanceIdentifier;)
@property (copy, nonatomic) NSString* serviceSubclassName;
@property (readonly) unsigned long hash;
@property (readonly) Class superclass;
@property (readonly, copy) NSString* description;
@property (readonly, copy) NSString* debugDescription;
@property (readonly, nonatomic) NSXPCInterface*
auxiliaryInterfaceOutgoing;
@property (readonly, nonatomic) NSXPCInterface*
auxiliaryInterfaceIncoming;
@property (weak) NSXPCConnection* auxiliaryServiceConnection;
(@synthesize auxiliaryServiceConnection =
auxiliaryServiceConnection;)
@property (readonly) BOOL wantsAlertStylePadding;
Instance Methods:
- (oneway void) release; (0xc97d800195a40a60)
- (void) dealloc; (0x715b800195a94fa0)
- (id) description; (0x1b2e000195a94710)
- (id) retain; (0x3811000195a3ff68)
- (BOOL) isValid; (0xc844800195a413e0)
- (void) .cxx_destruct; (0x7560800195ab293c)
- (void) _setDelegate:(id)arg1; (0xc37a800195a3feac)
- (id) delegate; (0x1a7c800195a3fb64)
[...hundreds and hundreds of methods...]
A lot!
Searching for "process" in that list of methods yields
_serviceProcessIdentifier so let's call it.
(lldb) po [$v _serviceProcessIdentifier]
5959
What's that process?
$ ps -c 5959
PID TTY TIME CMD
5959 ?? 0:14.34 QuickLookUIService
This has to be it. Debugging Finder isn't what we want. We need to be
debugging QuickLookUIService.
Debugging QuickLookUIService
(lldb) detach
Process 4040 detached
(lldb) attach 5959
Process 5959 stopped
thread #1, queue = 'com.apple.main-thread', stop reason = signal
SIGSTOP
frame #0: 0x000000018df1bf54 libsystem_kernel.dylib`
mach_msg2_trap + 8
libsystem_kernel.dylib`mach_msg2_trap:
-> 0x18df1bf54 <+8>: ret
libsystem_kernel.dylib`macx_swapon:
0x18df1bf58 <+0>: mov x16, #-0x30
0x18df1bf5c <+4>: svc #0x80
0x18df1bf60 <+8>: ret
Target 0: (QuickLookUIService) stopped.
So, the other side of that NSRemoteView is... somewhere... in this
process.
If we could iterate through all the views in the process, we could
see if any of them looked like the other end of an
NSRemoteViewDelegate.
I searched the internet for a function that would get all views, but
I couldn't find anything except for the odd person saying to just use
Xcode.^1
Well, why don't we just use Xcode?
After opening Xcode, creating a project, choosing where to save the
project, entering a team name, choosing a template, entering a bundle
identifier, creating a scheme, and setting the scheme target so it'll
let me use the debugger, I attached it to QuickLookUIService, pressed
"Show View Hierarchy", and:
[xcode-ligh] Xcode's view debugger attached to QuickLookUIService.
Well, that was easy! We can click through these views and get info
about them, including their addresses so we can mess with them in the
debugger. And we can see straight away that the frontmost view, which
Xcode tells us is a QLBorderView, is a border with rounded corners!
Wait, there's a border?
The Border
There is. I hadn't noticed it at first, but if you zoom in, you can
see that as well as rounding the corners, QuickLook also superimposes
a slightly-too-small border:
[big-light]
The QLBorderView turns out to be very simple; it's just a view whose
layer has a borderWidth and a borderColor. Let's get its address from
Xcode and tell it to cut it out:
(lldb) po [0x600000850180 setBorderWidth:0];
[big-border] Border successfully removed!
Unclipping
So, the border is gone, but the corners are still rounded. We can see
in the view debugger that the image view's corners are not rounded,
so something must be clipping it.
Let's have a look through all of the parent views' layers and see if
any of them have a nonzero cornerRadius.
You could probably write some code to do this but I just clicked
through each one and copied its layer's address into the debugger to
check:
(lldb) p [(CALayer *)0x60000283f5a0 cornerRadius]
(double) $78 = 0
(lldb) p [(CALayer *)0x60000283f660 cornerRadius]
(double) $79 = 0.43438914027149322
Well, that didn't take long. Let's try...
(lldb) po [0x60000283f660 setMasksToBounds:0]
(lldb) po [0x60000283f660 setNeedsDisplay]
[fixed-ligh] Corners restored!
Keeping it that way
So, we've now fixed our view. We can see all of our pixels.
But we're not done yet, because when you click on a different file,
you get a new view, and the new view is rounded. It looks like QL
creates one view per previewed file, and reuses the old view only for
the same file.
So, we need to intercept it when it makes new views and stop it from
screwing them up.
Let's start with that border view. When is it created? We'll break on
its init method, which its method list tells us is initWithFrame:.
(lldb) break set -n '[QLOverlayBorderView initWithFrame:]'
(lldb) c
This does indeed trigger when you select a new image:
Process 5959 stopped
thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint
1.1
frame #0: 0x00000001f827c7fc QuickLookUI`-[QLOverlayBorderView
initWithFrame:]
QuickLookUI`-[QLOverlayBorderView initWithFrame:]:
-> 0x1f827c7fc <+0>: pacibsp
0x1f827c800 <+4>: sub sp, sp, #0x30
0x1f827c804 <+8>: stp x20, x19, [sp, #0x10]
0x1f827c808 <+12>: stp x29, x30, [sp, #0x20]
Target 0: (QuickLookUIService) stopped.
Let's see who's calling us:
(lldb) bt 5
thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint
1.1
* frame #0: 0x00000001f827c7fc QuickLookUI`-[QLOverlayBorderView
initWithFrame:]
frame #1: 0x00000001f8203128 QuickLookUI`-
[QLDisplayBundleViewController enableBorder] + 968
frame #2: 0x0000000106a02f80 Image`___lldb_unnamed_symbol317 + 84
frame #3: 0x00000001f8202cb8 QuickLookUI`-
[QLDisplayBundleViewController _updateOverlayBorder] + 88
frame #4: 0x00000001f8203264 QuickLookUI`-
[QLDisplayBundleViewController
observeValueForKeyPath:ofObject:change:context:] + 108
Interesting. The border view is being called from something called
enableBorder. What if we disabled enableBorder?
There are no docs for the QLDisplayBundleViewController, of course,
but there's always a method list:
(lldb) po [$x0 fp_shortMethodDescription]
:
in QLImageDisplayBundleViewController:
Properties:
@property (readonly) IKImageView2* imageView;
Instance Methods:
- (id) imageView; (0xf978000106a02cf8)
- (BOOL) _epsilonEqualRect:(CGRect)arg1 toRect:(CGRect)arg2;
(0x8f22800106a02d50)
- (BOOL) useLayerMaskForCorners; (0xe439000106a02dbc)
- (void) updateContentCornerRadius; (0x582e800106a02e8c)
- (void) didSave:(BOOL)arg1 toURL:(id)arg2 forClosing:(BOOL)arg3;
(0xf67c000106a02e90)
- (void) enableBorder; (0x7b35800106a02f2c)
- (void) disableBorder; (0x9d21800106a02f90)
- (void) teardownMarkup:(long)arg1 needsSave:(BOOL*)arg2;
(0xe96b000106a02ff4)
- (BOOL) mustHandleDragAtLocation:(CGPoint)arg1; (0xdc49800106a0305c)
- (BOOL) mustHandleDoubleClickAtLocation:(CGPoint)arg1;
(0xb042800106a030b0)
- (void) copy:(id)arg1; (0x325c800106a03140)
- (void) selectAll:(id)arg1; (0xb532800106a031a4)
(QLDisplayBundleViewController ...)
As well as enableBorder, there's a disableBorder. If we're lucky it
might disable the border.
(lldb) po [$x0 disableBorder]
It does! So, to fix our QuickLook process, we'll have to arrange for
disableBorder to be called instead of enableBorder.^2
Now, what about the rounded corners? We can set a breakpoint to see
where the corners get rounded:
(lldb) break set -n 'setCornerRadius:' -c '$x0 == 0x600002cbd830 &&
$d0 != 0.0'
When this breakpoint is hit, we can see that it's being called from a
method called updateCornerRadius.
So, our strategy for making our fix persist will be to:
1. Patch updateCornerRadius to return immediately without doing
anything.
2. Patch enableBorder to just call disableBorder and return.
Aside: Method Swizzling
In Objective-C, classes have methods and methods have
implementations. As a holdover from the bicycle-for-the-mind days,
the Objective-C runtime is hacker-friendly and provides functions to
help us monkey around with things:
Method class_getInstanceMethod(Class, SEL);
IMP class_getMethodImplementation(Class, SEL);
IMP method_getImplementation(Method);
IMP method_setImplementation(Method, IMP);
void method_exchangeImplementations(Method, Method);
In theory, you should be able to use these functions to switch out
the implementations of enableBorder and updateCornerRadius.
But, try as I might, I couldn't get it to work reliably. Maybe I was
holding it wrong, but instead of spending time figuring out why, I
just went straight to no-nonsense instruction patching.
Patching the corner radius
Let's patch updateCornerRadius to return immediately without doing
anything. We'll write a ret instruction over the first instruction in
the function.
As documented in this 76MB PDF, the encoding of ret in ARM64 is
0xd65f03c0.
We can write a simple Python script that will find the function in
memory and patch it:
(lldb) script
x, = lldb.target.FindSymbols('-[IKImageContentView
updateCornerRadius]')
e = lldb.SBError()
lldb.process.WriteMemory(
x.symbol.addr.load_addr,
struct.pack(' 0x1c3ec9a60 <+0>: ret
0x1c3ec9a64 <+4>: stp d9, d8, [sp, #-0x30]!
0x1c3ec9a68 <+8>: stp x20, x19, [sp, #0x10]
0x1c3ec9a6c <+12>: stp x29, x30, [sp, #0x20]
Success!
Patching enableBorder
Patching enableBorder is a little tricker, since we don't just want
to have it ret, we want to have it jump to disableBorder.
The ARM64 instruction for performing an unconditional jump is b, and
is encoded as follows:
* The instruction is 4 bytes long, like all ARM64 instructions.
* The high 6 bits are 00101.
* The low 26 bits are the number of instructions to jump, relative
to the current instruction.
Each instruction is 4 bytes, so we just need to subtract the
destination address from the source address, sign-extend to 26 bits,
and set the high 6 bits to 00101.
Here's the Python code to do that:
(lldb) script
def get_symbol_address(name):
symbol_context, = lldb.target.FindSymbols(name)
return symbol_context.symbol.addr.load_addr
enableBorder = get_symbol_address('-[QLDisplayBundleViewController
enableBorder]')
disableBorder = get_symbol_address('-[QLDisplayBundleViewController
disableBorder]')
jump_offset_bytes = disableBorder - enableBorder
jump_offset_insns = jump_offset_bytes // 4
def sign_extend(val, nbits):
return (val + (1 << nbits)) % (1 << nbits)
jump_insn = (5 << 26) | sign_extend(jump_offset_insns, 26)
e = lldb.SBError()
lldb.process.WriteMemory(
enableBorder,
struct.pack(': b 0x20b23f164 ; -
[QLDisplayBundleViewController disableBorder]
0x20b23ed64 <+4>: stp d15, d14, [sp, #-0x90]!
0x20b23ed68 <+8>: stp d13, d12, [sp, #0x10]
0x20b23ed6c <+12>: stp d11, d10, [sp, #0x20]
Nice! ^3
Putting it all together
We now have all the building blocks we need to make a nice script
that attaches to all running QuickLook processes and patches them.
This article is long enough already, so instead of reproducing it
here I'll just link to the repo. That way you'll get any fixes that
have been made since I wrote this article, too.
You can find the latest version of the script here.
Conclusion
I am reminded of the words of the late, great Douglas Adams:
The major difference between a thing that might go wrong and a
thing that cannot possibly go wrong is that when a thing that
cannot possibly go wrong goes wrong it usually turns out to be
impossible to get at and repair.
Well, perhaps not impossible. But that was rather involved!
The bit where I try and sell you something
Thanks for reading. If you'd like to read more from me, I've got this
article from last year about how I ported a Flash game to C++. My
website has a bunch of free games I've made over the years, and I've
also got two games out on Steam:
Firstly, Blackshift, an epic puzzle game which you can think of as
Chip's Challenge in space.
And more recently, Hapland Trilogy, a smaller point-and-click game.
If you have comments about the article, or just want to say hi,
please feel free to drop me an email at r@foon.uk.
Oh, and if you want to keep up to date with anything else I write,
there's an RSS feed. RSS is coming back!
See you next time!
Robin Allen
2023
1. I'm not sure why I didn't think of [NSApp windows]. But I'm glad I
ended up in Xcode because its view debugger is great.
2. Why not just have enableBorder do nothing? I tried, but it's not
enough. You really do need to call disableBorder.
3. No, I did not get all this stuff working on the first try.
(c) 2023 Robin Allen * foon.uk