{"id":104387,"date":"2020-10-21T07:00:00","date_gmt":"2020-10-21T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=104387"},"modified":"2020-10-21T06:58:35","modified_gmt":"2020-10-21T13:58:35","slug":"20201021-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20201021-00\/?p=104387","title":{"rendered":"How do I get from a volume to the physical disk that holds it?"},"content":{"rendered":"<p>Last time, we saw <a title=\"How do I get from a file path to the volume that holds it?\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20201020-00\/?p=104385\"> how to get from a file path to the volume that holds it<\/a>. The next step is to get from the volume to the physical disk.<\/p>\n<p>The lazy way is to ask for the device number:<\/p>\n<pre>STORAGE_DEVICE_NUMBER number;\r\nDeviceIoControl(handle,\r\n    IOCTL_STORAGE_GET_DEVICE_NUMBER,\r\n    nullptr, 0, \/\/ no input\r\n    &amp;number, sizeof(number), \/\/ output goes here\r\n    &amp;bytesWritten,\r\n    nullptr);\r\nDWORD physicalDriveNumber = number.DeviceNumber;\r\n<\/pre>\n<p>This is lazy for multiple reasons:<\/p>\n<ul>\n<li>It fails to account for the case where the volume spans multiple physical drives.<\/li>\n<li>In my experience, if the volume is a CD-ROM drive with no disk in the drive, the call reports that the physical drive number is 0, which is almost certainly incorrect.<\/li>\n<\/ul>\n<p>In practice, it seems that if the volume spans multiple physical drives, the <code>IOCTL_<\/code><code>STORAGE_<\/code><code>GET_<\/code><code>DEVICE_<\/code><code>NUMBER<\/code> fails (with <code>ERROR_<\/code><code>INVALID_<\/code><code>FUNCTION<\/code>, it seems, which is the Win32 manifestation of the NT status code <code>STATUS_<\/code><code>INVALID_<\/code><code>DEVICE_<\/code><code>REQUEST<\/code>), so at least you don&#8217;t get <i>wrong<\/i> answers. You just get <i>no<\/i> answer.<\/p>\n<p>The less lazy (and more likely to be correct) way is to ask the volume for its disk extents. This one is a bit annoying because it returns a variable-sized structure, so you need to ask twice. The first time tells you how big a structure you need, and the second time actually gets the structure.<\/p>\n<p>Since nearly all volumes have only one extent, we can optimize slightly for that case by passing an initial buffer big enough to hold a single extent. If that works, then there&#8217;s no need to try a second time.<\/p>\n<pre>VOLUME_DISK_EXTENTS* extents = nullptr;\r\n\r\n\/\/ Anticipate common case where there is only one extent.\r\nVOLUME_DISK_EXTENTS singleExtent;\r\n\r\n\/\/ But also have a place to manage allocated data.\r\nstd::unique_ptr&lt;BYTE[]&gt; lifetime;\r\n\r\nDWORD bytesWritten;\r\nif (DeviceIoControl(handle, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,\r\n        nullptr, 0,\r\n        &amp;singleExtent, sizeof(singleExtent),\r\n        &amp;bytesWritten,\r\n        nullptr)) {\r\n  \/\/ Worked on the first try. Use the preallocated buffer.\r\n  extents = &amp;singleExtent;\r\n} else {\r\n  VOLUME_DISK_EXTENTS* lastQuery = &amp;singleExtent;\r\n  while (GetLastError() == ERROR_MORE_DATA) {\r\n    assert(RTL_CONTAINS_FIELD(lastQuery, bytesWritten, NumberOfDiskExtents));\r\n    DWORD extentCount = lastQuery-&gt;NumberOfDiskExtents;\r\n    DWORD allocatedSize = FIELD_OFFSET(VOLUME_DISK_EXTENTS, Extents[extentCount]);\r\n    lifetime.reset(new BYTE[allocatedSize]);\r\n    lastQuery = (VOLUME_DISK_EXTENTS*)lifetime.get();\r\n    if (DeviceIoControl(handle, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,\r\n          nullptr, 0,\r\n          lastQuery, allocatedSize,\r\n          &amp;bytesWritten,\r\n          nullptr)) {\r\n      extents = lastQuery;\r\n      break;\r\n    }\r\n  }\r\n}\r\n\r\nif (extents) {\r\n  \/\/ process the extents\r\n}\r\n<\/pre>\n<p>The extents tell you which physical drives the volume draws its storage from, and which bytes on those physical drives are devoted to the volume. But for this exercise, we just want the physical drives.<\/p>\n<p>Once you have the physical drive numbers, you can convert them to physical drive handles by building a path of the form <code>\\\\.\\PhysicalDrive#<\/code> where the <code>#<\/code> is the decimal expansion of the drive number.<\/p>\n<pre>wchar_t physicalDrivePath[80];\r\nwsprintf_s(physicalDrivePath, L\"\\\\\\\\.\\\\PhysicalDrive%d\", physicalDriveNumber);\r\ndriveHandle = CreateFile(physicalVolumePath,\r\n        0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,\r\n        nullptr, OPEN_EXISTING, 0, nullptr);\r\n<\/pre>\n<p>Okay, great, now you have a physical drive handle.<\/p>\n<p>Next time, we&#8217;ll see that there&#8217;s a shortcut available for all this.<\/p>\n<p><b>Bonus chatter<\/b>: If you are interested only in the first physical drive of a multi-drive volume, you can do it much more simply, because the ioctl will fill in as much of the buffer as it can. Passing a buffer that can hold one physical drive will give you the first physical drive. (Mind you, the drives don&#8217;t appear to be in any particular, order, so really, you&#8217;re just grabbing one at random.)<\/p>\n<pre>wil::unique_hfile GetFirstPhysicalDiskHandleForVolume(HANDLE volume)\r\n{\r\n  VOLUME_DISK_EXTENTS extents;\r\n  if (!DeviceIoControl(volume, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,\r\n        nullptr, 0,\r\n        &amp;extents, sizeof(extents),\r\n        &amp;bytesWritten,\r\n        nullptr) &amp;&amp; GetLastError() != ERROR_MORE_DATA) {\r\n    THROW_LAST_ERROR();\r\n  }\r\n\r\n  wchar_t physicalDrivePath[80];\r\n  swprintf_s(physicalDrivePath, L\"\\\\\\\\.\\\\PhysicalDrive%u\",\r\n             extents.Extents[0].DiskNumber);\r\n\r\n  wil::unique_hfile result{ CreateFile(physicalDrivePath, 0,\r\n                      FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,\r\n                      nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr) };\r\n  THROW_LAST_ERROR_IF(!result);\r\n  return result;\r\n}\r\n<\/pre>\n<p><b>Bonus bonus chatter<\/b>: It seems that the I\/O subsystem can&#8217;t decide whether the number is a physical <i>device<\/i> number, a physical <i>disk<\/i> number, or a a physical <i>drive<\/i> number.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Another step down the hierarchy.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-104387","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Another step down the hierarchy.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/104387","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=104387"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/104387\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=104387"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=104387"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=104387"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}