Created
July 1, 2026 17:39
-
-
Save dnicolson/a11918eaa5d793b050bb0e9912a387d0 to your computer and use it in GitHub Desktop.
SpringBoardServices bug: Calling setIconState with the state returned by getIconState can reorder apps above a 2×2 large widget
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| set -eu | |
| SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" | |
| BUILD_DIR="$SCRIPT_DIR/build" | |
| mkdir -p "$BUILD_DIR" | |
| clang \ | |
| -fobjc-arc \ | |
| -framework Foundation \ | |
| -o "$BUILD_DIR/sbservices-bug" \ | |
| "$SCRIPT_DIR/main.m" | |
| echo "Built $BUILD_DIR/sbservices-bug" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #import <Foundation/Foundation.h> | |
| #import <dlfcn.h> | |
| #import <objc/runtime.h> | |
| @interface MDKMobileDevice : NSObject | |
| + (NSArray *)connectedDevices; | |
| - (NSString *)UDID; | |
| - (BOOL)connectAndReturnError:(NSError **)error; | |
| - (BOOL)startSessionAndReturnError:(NSError **)error; | |
| - (BOOL)stopSessionAndReturnError:(NSError **)error; | |
| - (BOOL)disconnectAndReturnError:(NSError **)error; | |
| - (id)connectToSpringBoardServiceWithOptions:(NSDictionary *)options error:(NSError **)error; | |
| @end | |
| @interface MDKSpringBoardService : NSObject | |
| - (id)iconStateWithError:(NSError **)error; | |
| - (BOOL)setIconState:(id)state error:(NSError **)error; | |
| - (void)invalidate; | |
| @end | |
| @interface MDKServiceConnection : NSObject | |
| - (BOOL)sendMessage:(id)message error:(NSError **)error; | |
| - (NSData *)receiveDataOfLength:(NSUInteger)length error:(NSError **)error; | |
| @end | |
| static BOOL DlopenBinary(NSString *path, NSError **error) { | |
| void *handle = dlopen(path.fileSystemRepresentation, RTLD_NOW | RTLD_GLOBAL); | |
| if (handle != NULL) { | |
| return YES; | |
| } | |
| if (error != NULL) { | |
| NSString *message = [NSString stringWithUTF8String:dlerror() ?: "unknown dlopen error"]; | |
| *error = [NSError errorWithDomain:@"sbservices-bug" | |
| code:1 | |
| userInfo:@{ | |
| NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to load %@: %@", path, message] | |
| }]; | |
| } | |
| return NO; | |
| } | |
| static BOOL LoadMobileDeviceKit(NSError **error) { | |
| NSString *binaryPath = @"/Applications/Apple Configurator.app/Contents/Frameworks/ConfigurationUtilityKit.framework/Versions/A/Frameworks/MobileDeviceKit.framework/Versions/A/MobileDeviceKit"; | |
| if (![[NSFileManager defaultManager] fileExistsAtPath:binaryPath]) { | |
| if (error != NULL) { | |
| *error = [NSError errorWithDomain:@"sbservices-bug" | |
| code:1 | |
| userInfo:@{ | |
| NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Missing framework binary at %@", binaryPath] | |
| }]; | |
| } | |
| return NO; | |
| } | |
| if (!DlopenBinary(binaryPath, error)) { | |
| return NO; | |
| } | |
| return NSClassFromString(@"MDKMobileDevice") != nil && NSClassFromString(@"MDKSpringBoardService") != nil; | |
| } | |
| static id ResolveDevice(NSString *requestedUDID) { | |
| Class deviceClass = NSClassFromString(@"MDKMobileDevice"); | |
| NSArray *devices = [deviceClass connectedDevices]; | |
| if (requestedUDID.length == 0) { | |
| return devices.firstObject; | |
| } | |
| for (id device in devices) { | |
| if ([[device UDID] isEqualToString:requestedUDID]) { | |
| return device; | |
| } | |
| } | |
| return nil; | |
| } | |
| static NSString *ReadbackDumpPath(void) { | |
| NSString *currentDirectory = [[NSFileManager defaultManager] currentDirectoryPath]; | |
| return [currentDirectory stringByAppendingPathComponent:@"sbservices-bug-probe-before.plist"]; | |
| } | |
| static NSString *AfterDumpPath(void) { | |
| NSString *currentDirectory = [[NSFileManager defaultManager] currentDirectoryPath]; | |
| return [currentDirectory stringByAppendingPathComponent:@"sbservices-bug-probe-after.plist"]; | |
| } | |
| static BOOL WritePlistDump(id object, NSString *path, NSError **error) { | |
| if (object == nil) { | |
| if (error != NULL) { | |
| *error = [NSError errorWithDomain:@"sbservices-bug" | |
| code:10 | |
| userInfo:@{ | |
| NSLocalizedDescriptionKey: @"Refusing to write a nil icon state payload." | |
| }]; | |
| } | |
| return NO; | |
| } | |
| NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:object | |
| format:NSPropertyListXMLFormat_v1_0 | |
| options:0 | |
| error:error]; | |
| if (plistData == nil) { | |
| return NO; | |
| } | |
| return [plistData writeToFile:path options:NSDataWritingAtomic error:error]; | |
| } | |
| static MDKServiceConnection *ServiceConnectionForSpringBoardService(MDKSpringBoardService *springBoardService) { | |
| Ivar ivar = class_getInstanceVariable([springBoardService class], "mServiceConnection"); | |
| if (ivar == NULL) { | |
| return nil; | |
| } | |
| id value = object_getIvar(springBoardService, ivar); | |
| if (![value isKindOfClass:NSClassFromString(@"MDKServiceConnection")]) { | |
| return nil; | |
| } | |
| return (MDKServiceConnection *)value; | |
| } | |
| static BOOL RawSetIconState(MDKSpringBoardService *springBoardService, id state, NSError **error) { | |
| MDKServiceConnection *serviceConnection = ServiceConnectionForSpringBoardService(springBoardService); | |
| if (serviceConnection == nil) { | |
| if (error != NULL) { | |
| *error = [NSError errorWithDomain:@"sbservices-bug" | |
| code:11 | |
| userInfo:@{ | |
| NSLocalizedDescriptionKey: @"Unable to access the underlying MDKServiceConnection for SpringBoard." | |
| }]; | |
| } | |
| return NO; | |
| } | |
| NSDictionary *message = @{ | |
| @"command": @"setIconState", | |
| @"iconState": state, | |
| }; | |
| if (![serviceConnection sendMessage:message error:error]) { | |
| return NO; | |
| } | |
| NSData *response = [serviceConnection receiveDataOfLength:4 error:error]; | |
| if (response == nil) { | |
| return NO; | |
| } | |
| NSLog(@"read raw setIconState response bytes=%lu", (unsigned long)response.length); | |
| return YES; | |
| } | |
| int main(int argc, const char *argv[]) { | |
| @autoreleasepool { | |
| NSError *error = nil; | |
| if (!LoadMobileDeviceKit(&error)) { | |
| NSLog(@"framework load failed: %@", error.localizedDescription); | |
| return 1; | |
| } | |
| NSString *requestedUDID = argc > 1 ? [NSString stringWithUTF8String:argv[1]] : nil; | |
| id device = ResolveDevice(requestedUDID); | |
| if (device == nil) { | |
| NSLog(@"device not found%@", requestedUDID.length ? [NSString stringWithFormat:@": %@", requestedUDID] : @""); | |
| return 1; | |
| } | |
| if (![device connectAndReturnError:&error]) { | |
| NSLog(@"connect failed: %@", error.localizedDescription); | |
| return 1; | |
| } | |
| if (![device startSessionAndReturnError:&error]) { | |
| NSLog(@"startSession failed: %@", error.localizedDescription); | |
| [device disconnectAndReturnError:nil]; | |
| return 1; | |
| } | |
| MDKSpringBoardService *springBoardService = [device connectToSpringBoardServiceWithOptions:nil error:&error]; | |
| if (springBoardService == nil) { | |
| NSLog(@"connectToSpringBoardService failed: %@", error.localizedDescription); | |
| [device stopSessionAndReturnError:nil]; | |
| [device disconnectAndReturnError:nil]; | |
| return 1; | |
| } | |
| id rawState = [springBoardService iconStateWithError:&error]; | |
| if (![rawState isKindOfClass:[NSArray class]]) { | |
| NSLog(@"iconState failed: %@", error.localizedDescription ?: @"unexpected response"); | |
| [springBoardService invalidate]; | |
| [device stopSessionAndReturnError:nil]; | |
| [device disconnectAndReturnError:nil]; | |
| return 1; | |
| } | |
| if (!WritePlistDump(rawState, ReadbackDumpPath(), &error)) { | |
| NSLog(@"readback dump failed: %@", error.localizedDescription); | |
| [springBoardService invalidate]; | |
| [device stopSessionAndReturnError:nil]; | |
| [device disconnectAndReturnError:nil]; | |
| return 1; | |
| } | |
| NSLog(@"wrote raw readback payload to %@", ReadbackDumpPath()); | |
| if (!RawSetIconState(springBoardService, rawState, &error)) { | |
| NSLog(@"raw setIconState failed: %@", error.localizedDescription); | |
| [springBoardService invalidate]; | |
| [device stopSessionAndReturnError:nil]; | |
| [device disconnectAndReturnError:nil]; | |
| return 1; | |
| } | |
| NSLog(@"wrote icon state back unchanged via raw service connection"); | |
| id afterState = [springBoardService iconStateWithError:&error]; | |
| if (![afterState isKindOfClass:[NSArray class]]) { | |
| NSLog(@"post-write iconState failed: %@", error.localizedDescription ?: @"unexpected response"); | |
| [springBoardService invalidate]; | |
| [device stopSessionAndReturnError:nil]; | |
| [device disconnectAndReturnError:nil]; | |
| return 1; | |
| } | |
| if (!WritePlistDump(afterState, AfterDumpPath(), &error)) { | |
| NSLog(@"after dump failed: %@", error.localizedDescription); | |
| [springBoardService invalidate]; | |
| [device stopSessionAndReturnError:nil]; | |
| [device disconnectAndReturnError:nil]; | |
| return 1; | |
| } | |
| NSLog(@"wrote post-write payload to %@", AfterDumpPath()); | |
| [springBoardService invalidate]; | |
| [device stopSessionAndReturnError:nil]; | |
| [device disconnectAndReturnError:nil]; | |
| } | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment