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.

QuickLook in Mojave
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-.

<hacker-voice>We're in</hacker-voice> 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
<QLPreviewPanel: 0x119c64c90>

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=(-) => <CALayer: 0x600003332a80>
                 [ A     W ] h=--- v=--- NSRemoteView 0x119c3a040 f=(0,0,360,180) b=(-) => <NSVBCALayerHost: 0x600003225a80>
     [ 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=(-) => <NSTextLayer: 0x600003f785a0>
        [ AF     w ] h=--- v=--- NSTextField 0x11e04cda0 "Screenshot 2023-08-28 at 16.21.26" f=(42,8,66,16) b=(-) => <NSTextLayer: 0x600003f72610>
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.

NSRemote­View

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

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]
<NSRemoteView: 0x1e9543bc0>:
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<NSRemoteViewDelegate>* 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 QuickLookUI­Service

(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'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:

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];
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]
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]
<QLImageDisplayBundleViewController: 0x13053d770>:
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('<I', 0xd65f03c0),
    e
)
assert e.Success()

Check it worked:

(lldb) dis -n updateCornerRadius -c 4
ImageKit`-[IKImageContentView updateCornerRadius]:
-> 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:

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('<I', jump_insn),
    e
)
assert e.Success()

Check it worked:

(lldb) dis -n '-[QLDisplayBundleViewController enableBorder]' -c 4
QuickLookUI`-[QLDisplayBundleViewController enableBorder]:
    0x20b23ed60 <+0>: 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.