Determine Volume Groups in macOS Catalina, Big Sur and later

1k views Asked by At

Since macOS 10.15 (Catalina), a volume, as the user sees it, may actually be comprised of multiple volumes, such as the System and the Data volume.

I'm writing a tool that needs to identify these volumes separately because when using specific file operations, such as searchfs and fts_read, which do not cross such volume boundaries, I need to understand which volumes belong together, so that, when the user wants to search a system volume, I know to include both the "System" and its "Data" volumes in the file operations.

How do I safely determine which volumes belong together?

Using [NSFileManager mountedVolumeURLsIncludingResourceValuesForKeys:options:] is not much help alone because it won't include the root system's Data volume at /System/Volumes/Data (but may include the hidden /System/Volumes/Data/home volume). Same goes for using command line tools such as df.

I need to consider other system volumes that are not currently booted. E.g, if I have both a BigSur and a Catalina system, and have started up from the former, I want to be able to identify these four volumes:

/                              BigSur System volume
/System/Volumes/Data           BigSur Data volume
/Volumes/Catalina              Catalina System volume
/Volumes/Catalina - Daten      Catalina Data volume (created on a German system)

How would I tell that the two volumes containing "Catalina" actually belong to the same group? I don't like to match them up by partial names as this seems rather random and unreliable to me. And the fact that the data volume is not even using "Data" in the name if it's not created on an English system makes this already much too difficult to get right.

Is there perhaps some other volume property that would help identify these volume groups?

3

There are 3 answers

1
elulcao On

Use diskutil list to check the Volumes and their names. system_profiler display much information but you must parse the ouput for the Volume information, using system_profiler -xml > output.xml will create a file for understanding the available values for specific queries.

0
Thomas Tempelmann On

The following shell command lists volume groups:

diskutil apfs listVolumeGroups

And for parsing by code one could append the option to output as a plist, which one can then import with CFPropertyListCreateWithData with format:kCFPropertyListXMLFormat_v1_0:

diskutil apfs listVolumeGroups -plist

(Answer provided in a tweet byHoward Oakley)

0
Thomas Tempelmann On

Mike Bombich provided me with this solution:

You can get the volume UUID and the volume group UUID from IOKit. Two volumes that are in the same group will have the same group UUID. Note that the group UUID is always the same as the Data volume's UUID (at least in practice).

Here's the code for getting the list of mounted volumes, including the hidden ones that are part of a volume group:

- (void)listVolumes
{
    NSArray<NSURL*> *vols = [NSFileManager.defaultManager mountedVolumeURLsIncludingResourceValuesForKeys:nil options: 0 ];
    vols = [vols arrayByAddingObject:[NSURL fileURLWithPath:@"/System/Volumes/Data"]]; // the root's Data vol isn't added by default
    NSMutableArray<NSString*> *lines = [NSMutableArray new];
    for (NSURL *vol in vols) {
        NSDictionary *d = [vol resourceValuesForKeys:@[
            NSURLVolumeIsBrowsableKey,
            NSURLVolumeIsRootFileSystemKey,
            NSURLVolumeIdentifierKey,
            NSURLVolumeNameKey
        ] error:nil];

        struct statfs fsinfo;
        statfs(vol.path.UTF8String, &fsinfo);
        NSString *bsdName = [NSString stringWithUTF8String:fsinfo.f_mntfromname];
        bsdName = [bsdName lastPathComponent];

        [lines addObject:[NSString stringWithFormat:@"%@, %@, %@, %@", bsdName, vol.path, d[NSURLVolumeIsBrowsableKey], d[NSURLVolumeNameKey]]];
    }
    NSLog(@"\n%@", [lines componentsJoinedByString:@"\n"]);
}

And the code for listing the volume group IDs, and their roles:

- (void)listGroupIDs
{
    io_iterator_t iterator; io_object_t obj;
    IOServiceGetMatchingServices (kIOMasterPortDefault, IOServiceMatching("IOMediaBSDClient"), &iterator);
    while ((obj = IOIteratorNext (iterator)) != 0) {
        io_object_t obj2;
        IORegistryEntryGetParentEntry (obj, kIOServicePlane, &obj2);
        NSString *bsdName = CFBridgingRelease(IORegistryEntryCreateCFProperty(obj2, CFSTR("BSD Name"), kCFAllocatorDefault, 0));
        //NSString *volID = CFBridgingRelease(IORegistryEntryCreateCFProperty(obj2, CFSTR("UUID"), kCFAllocatorDefault, 0));
        NSString *groupID = CFBridgingRelease(IORegistryEntryCreateCFProperty(obj2, CFSTR("VolGroupUUID"), kCFAllocatorDefault, 0));
        NSArray *roles = CFBridgingRelease(IORegistryEntryCreateCFProperty(obj2, CFSTR("Role"), kCFAllocatorDefault, 0));
        if (groupID != nil && ![groupID isEqualToString:@"00000000-0000-0000-0000-000000000000"]) {
            NSLog(@"%@: %@, %@", bsdName, groupID, roles);
        }
    }
}

With both this information, the volumes from IOKit can be matched to the NSURLs through their BSD Names.

However, there's one more special case: On macOS Big Sur the root system's device is not the regular "diskXsY" but a snapshot device such as "diskXsYsZ". And while that gets listed as well by the IOKit code, its entry is missing the role information.

Here's an example output from the Mac with both a Big Sur and a Catalina system as shown in the question (slightly edited for readability):

disk3s1s1, /, 1, BigSur
disk3s5,   /System/Volumes/VM, 0, VM
disk3s3,   /System/Volumes/Preboot, 0, Preboot
disk3s6,   /System/Volumes/Update, 0, Update
disk4s1,   /Volumes/Catalina - Daten, 0, Catalina - Daten
disk4s2,   /Volumes/Catalina, 1, Catalina
disk3s2,   /System/Volumes/Data, 1, BigSur

disk4s1:   18464FE4-8321-4D36-B87A-53AC38EF6AEF, 18464FE4-8321-4D36-B87A-53AC38EF6AEF, ("Data")
disk3s1:   86812DBD-9252-4A2E-8887-752418DECE13, 058517A6-48DD-46AB-8A78-C1F115AE6E13, ("System")
disk4s2:   51DEC6AC-2D68-4B60-AE23-74BCA2C3A484, 18464FE4-8321-4D36-B87A-53AC38EF6AEF, ("System")
disk3s2:   058517A6-48DD-46AB-8A78-C1F115AE6E13, 058517A6-48DD-46AB-8A78-C1F115AE6E13, ("Data")
disk3s1s1: C26440B0-0207-4227-A4B1-EBDD62C90D24, 058517A6-48DD-46AB-8A78-C1F115AE6E13, (null)

I have published a working code sample that determines all mounted volumes, and their group relationships. The entire compilable code (which you can replace in a new Obj-C App project's AppDelegate.m file can be found here: https://gist.github.com/tempelmann/80efc2eb84f0171a96822290dee7d8d9