1// Copyright 2024 The Chromium Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "base/files/drive_info.h" 6 7#include <IOKit/IOTypes.h> 8 9#include "base/files/file.h" 10#include "base/files/file_path.h" 11#include "build/build_config.h" 12 13#if BUILDFLAG(IS_MAC) 14#include <CoreFoundation/CoreFoundation.h> 15#include <DiskArbitration/DiskArbitration.h> 16#import <Foundation/Foundation.h> 17#include <IOKit/IOBSD.h> 18#include <IOKit/IOKitLib.h> 19#include <IOKit/storage/IOMedia.h> 20#include <IOKit/storage/IOStorageDeviceCharacteristics.h> 21#include <sys/stat.h> 22 23#include "base/apple/bridging.h" 24#include "base/apple/foundation_util.h" 25#include "base/apple/mach_logging.h" 26#include "base/apple/scoped_cftyperef.h" 27#include "base/files/file_path.h" 28#include "base/mac/mac_util.h" 29#include "base/mac/scoped_ioobject.h" 30#include "base/strings/sys_string_conversions.h" 31#endif 32 33#if BUILDFLAG(IS_MAC) 34namespace { 35template <typename T> 36T QueryParentsForProperty(io_object_t io_object, const char* key) { 37 return static_cast<T>(IORegistryEntrySearchCFProperty( 38 io_object, kIOServicePlane, 39 CFStringCreateWithCString(kCFAllocatorDefault, key, 40 kCFStringEncodingUTF8), 41 kCFAllocatorDefault, 42 kIORegistryIterateRecursively | kIORegistryIterateParents)); 43} 44} // namespace 45#endif 46 47namespace base { 48 49std::optional<DriveInfo> GetIOObjectDriveInfo(io_object_t io_object) { 50#if BUILDFLAG(IS_IOS) 51 DriveInfo info; 52 info.has_seek_penalty = false; 53 return info; 54#else 55 static_assert(BUILDFLAG(IS_MAC)); 56 57 if (!IOObjectConformsTo(io_object, kIOMediaClass)) { 58 return std::nullopt; 59 } 60 DriveInfo drive_info; 61 62 // Query parents for the drive medium, which is a device characteristic, and 63 // determines whether the drive is rotational (has seek penalty). 64 apple::ScopedCFTypeRef<CFDictionaryRef> device_characteristics( 65 QueryParentsForProperty<CFDictionaryRef>( 66 io_object, kIOPropertyDeviceCharacteristicsKey)); 67 if (device_characteristics) { 68 CFStringRef type_ref = apple::GetValueFromDictionary<CFStringRef>( 69 device_characteristics.get(), CFSTR(kIOPropertyMediumTypeKey)); 70 if (type_ref) { 71 NSString* type = apple::CFToNSPtrCast(type_ref); 72 if ([type isEqualToString:@kIOPropertyMediumTypeRotationalKey]) { 73 drive_info.has_seek_penalty = true; 74 } else if ([type isEqualToString:@kIOPropertyMediumTypeSolidStateKey]) { 75 drive_info.has_seek_penalty = false; 76 } 77 } 78 } 79 80 // Query parents for the physical interconnect (to determine whether a drive 81 // is connected over USB), which is a protocol characteristic. 82 apple::ScopedCFTypeRef<CFDictionaryRef> protocol_characteristics( 83 QueryParentsForProperty<CFDictionaryRef>( 84 io_object, kIOPropertyProtocolCharacteristicsKey)); 85 if (protocol_characteristics) { 86 CFStringRef phy_type_ref = apple::GetValueFromDictionary<CFStringRef>( 87 protocol_characteristics.get(), 88 CFSTR(kIOPropertyPhysicalInterconnectTypeKey)); 89 if (phy_type_ref) { 90 drive_info.is_usb = [apple::CFToNSPtrCast(phy_type_ref) 91 isEqualToString:@kIOPropertyPhysicalInterconnectTypeUSB]; 92 } 93 } 94 95 // Query for the "CoreStorage" property, which is present on CoreStorage 96 // volumes. 97 apple::ScopedCFTypeRef<CFBooleanRef> cf_corestorage( 98 QueryParentsForProperty<CFBooleanRef>(io_object, "CoreStorage")); 99 // If the property doesn't exist, it's safe to say that this isn't 100 // CoreStorage. In any case, starting with Big Sur, CoreStorage 101 // functionality has mostly been stripped from the OS. 102 drive_info.is_core_storage = 103 cf_corestorage && CFBooleanGetValue(cf_corestorage.get()); 104 105 drive_info.is_apfs = false; 106 // Don't delete the IOObject of interest as we release each object in the 107 // heirarchy. 108 IOObjectRetain(io_object); 109 { 110 io_object_t current_obj = 0; 111 io_object_t next_obj = io_object; 112 // The GUID for the normal type of APFS physical store. Code that uses 113 // GetBSDNameDriveInfo() with a `/dev/diskX` device name may see a physical 114 // store. 115 constexpr NSString* kApfsPhysicalStoreGUID = 116 @"7C3457EF-0000-11AA-AA11-00306543ECAC"; 117 // APFS Container GUID, which resides on a physical store. Manually querying 118 // for objects in IOKit with a matching dictionary can obtain these objects. 119 constexpr NSString* kApfsContainerGUID = 120 @"EF57347C-0000-11AA-AA11-00306543ECAC"; 121 // APFS Volume or Snapshot GUID. A volume resides in a container, while a 122 // snapshot is associated with a volume. Code that uses GetFileDriveInfo() 123 // will likely obtain a Volume. 124 constexpr NSString* kApfsVolumeOrSnapshotGUID = 125 @"41504653-0000-11AA-AA11-00306543ECAC"; 126 // Used for iBoot. 127 constexpr NSString* kApfsIBootGUID = 128 @"69646961-6700-11AA-AA11-00306543ECAC"; 129 // Used for the recovery system. 130 constexpr NSString* kApfsRecoverySystemGUID = 131 @"69646961-6700-11AA-AA11-00306543ECAC"; 132 // Keep scoped object outside the loop so the object lives to the next 133 // GetParentEntry. 134 mac::ScopedIOObject<io_object_t> current_obj_ref(current_obj); 135 do { 136 current_obj = next_obj; 137 current_obj_ref.reset(current_obj); 138 apple::ScopedCFTypeRef<CFStringRef> cf_content( 139 static_cast<CFStringRef>(IORegistryEntryCreateCFProperty( 140 current_obj, CFSTR(kIOMediaContentKey), kCFAllocatorDefault, 0))); 141 if (cf_content) { 142 auto* ns_content = apple::CFToNSPtrCast(cf_content.get()); 143 if ([ns_content isEqualToString:kApfsPhysicalStoreGUID] || 144 [ns_content isEqualToString:kApfsContainerGUID] || 145 [ns_content isEqualToString:kApfsVolumeOrSnapshotGUID] || 146 [ns_content isEqualToString:kApfsIBootGUID] || 147 [ns_content isEqualToString:kApfsRecoverySystemGUID]) { 148 drive_info.is_apfs = true; 149 break; 150 } 151 } 152 } while ((IORegistryEntryGetParentEntry(current_obj, kIOServicePlane, 153 &next_obj)) == KERN_SUCCESS); 154 } 155 156 // There is a key for determining whether media is removable, though this does 157 // not tell the whole story. Disk Utility uses a combination of determining 158 // whether a disk is ejectable and whether it is stored internally to the 159 // device to determine whether it is removable, this behavior is emulated 160 // here. It also only respects the value of `kIOMediaRemovableKey` and 161 // `kIOMediaEjectableKey` if they are true. 162 // 163 // Interesting to note is that removable hard drives (hard drive enclosures) 164 // are not marked as removable according to IOKit despite definitely being 165 // removable in reality (can eject in finder, etc), and this is due to them 166 // being marked as External). 167 apple::ScopedCFTypeRef<CFBooleanRef> cf_removable( 168 QueryParentsForProperty<CFBooleanRef>(io_object, kIOMediaRemovableKey)); 169 if (cf_removable && CFBooleanGetValue(cf_removable.get())) { 170 drive_info.is_removable = true; 171 } else { 172 apple::ScopedCFTypeRef<CFBooleanRef> cf_ejectable( 173 QueryParentsForProperty<CFBooleanRef>(io_object, kIOMediaEjectableKey)); 174 if (cf_ejectable && CFBooleanGetValue(cf_ejectable.get())) { 175 drive_info.is_removable = true; 176 } else { 177 apple::ScopedCFTypeRef<CFStringRef> cf_phy_location( 178 QueryParentsForProperty<CFStringRef>( 179 io_object, kIOPropertyPhysicalInterconnectLocationKey)); 180 if (cf_phy_location) { 181 drive_info.is_removable = [apple::CFToNSPtrCast(cf_phy_location.get()) 182 isEqualToString:@kIOPropertyExternalKey]; 183 } 184 } 185 } 186 187 apple::ScopedCFTypeRef<CFNumberRef> cf_volume_size( 188 QueryParentsForProperty<CFNumberRef>(io_object, kIOMediaSizeKey)); 189 if (cf_volume_size) { 190 int64_t size; 191 Boolean success = 192 CFNumberGetValue(cf_volume_size.get(), kCFNumberSInt64Type, &size); 193 if (success) { 194 drive_info.size_bytes = size; 195 } 196 } 197 198 apple::ScopedCFTypeRef<CFBooleanRef> cf_writable( 199 QueryParentsForProperty<CFBooleanRef>(io_object, kIOMediaWritableKey)); 200 if (cf_writable) { 201 drive_info.is_writable = CFBooleanGetValue(cf_writable.get()); 202 } 203 204 apple::ScopedCFTypeRef<CFStringRef> cf_bsd_name( 205 QueryParentsForProperty<CFStringRef>(io_object, kIOBSDNameKey)); 206 if (cf_bsd_name) { 207 drive_info.bsd_name = SysCFStringRefToUTF8(cf_bsd_name.get()); 208 } 209 210 return drive_info; 211#endif // BUILDFLAG(IS_MAC) 212} 213 214std::optional<DriveInfo> GetBSDNameDriveInfo(const std::string_view bsd_name) { 215#if BUILDFLAG(IS_IOS) 216 DriveInfo info; 217 info.has_seek_penalty = false; 218 return info; 219#else // BUILDFLAG(IS_MAC) 220 CHECK_NE(bsd_name.find("/dev/"), 0UL); 221 std::string full_name("/dev/"); 222 full_name.append(bsd_name); 223 apple::ScopedCFTypeRef<DASessionRef> session( 224 DASessionCreate(kCFAllocatorDefault)); 225 if (!session) { 226 return std::nullopt; 227 } 228 apple::ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName( 229 kCFAllocatorDefault, session.get(), full_name.c_str())); 230 if (!disk) { 231 return std::nullopt; 232 } 233 mac::ScopedIOObject<io_object_t> io_media(DADiskCopyIOMedia(disk.get())); 234 return GetIOObjectDriveInfo(io_media.get()); 235#endif 236} 237 238std::optional<DriveInfo> GetFileDriveInfo(const FilePath& file_path) { 239#if BUILDFLAG(IS_IOS) 240 DriveInfo info; 241 info.has_seek_penalty = false; 242 return info; 243#else // BUILDFLAG(IS_MAC) 244 DriveInfo drive_info; 245 struct stat path_stat; 246 if (stat(file_path.value().c_str(), &path_stat) < 0) { 247 return std::nullopt; 248 } 249 char devname_buf[256]; 250 const char* dev_name = 251 devname_r(path_stat.st_dev, S_IFBLK, devname_buf, sizeof(devname_buf)); 252 if (!dev_name) { 253 return std::nullopt; 254 } 255 return GetBSDNameDriveInfo(dev_name); 256#endif 257} 258 259} // namespace base 260