| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | WSLCVhdVolume.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | Internal implementation for VHD-backed named volumes. |
| 12 | |
| 13 | --*/ |
| 14 | |
| 15 | #include "precomp.h" |
| 16 | #include "DockerHTTPClient.h" |
| 17 | #include "OptionParser.h" |
| 18 | #include "WSLCVhdVolume.h" |
| 19 | #include "WSLCVirtualMachine.h" |
| 20 | #include "WSLCVolumeMetadata.h" |
| 21 | #include "WslCoreFilesystem.h" |
| 22 | #include "wslc_schema.h" |
| 23 | |
| 24 | using namespace wsl::windows::common; |
| 25 | using wsl::shared::Localization; |
| 26 | |
| 27 | namespace wsl::windows::service::wslc { |
| 28 | |
| 29 | namespace { |
| 30 | constexpr auto c_sizeBytesOpt = "SizeBytes"; |
| 31 | constexpr auto c_fixedOpt = "Fixed"; |
| 32 | constexpr auto c_uidOpt = "Uid"; |
| 33 | constexpr auto c_gidOpt = "Gid"; |
| 34 | |
| 35 | struct VhdVolumeOptions |
| 36 | { |
| 37 | ULONGLONG SizeBytes{}; |
| 38 | bool Fixed{false}; |
| 39 | std::optional<uint32_t> Uid; |
| 40 | std::optional<uint32_t> Gid; |
| 41 | |
| 42 | static VhdVolumeOptions Parse(const std::map<std::string, std::string>& DriverOpts) |
| 43 | { |
| 44 | OptionParser parser(DriverOpts); |
| 45 | VhdVolumeOptions opts{}; |
| 46 | |
| 47 | opts.SizeBytes = parser.Required<ULONGLONG>(c_sizeBytesOpt); |
| 48 | THROW_HR_WITH_USER_ERROR_IF( |
| 49 | E_INVALIDARG, Localization::MessageWslcInvalidVolumeOption(c_sizeBytesOpt, DriverOpts.at(c_sizeBytesOpt)), opts.SizeBytes == 0); |
| 50 | |
| 51 | opts.Fixed = parser.OptionalBool(c_fixedOpt).value_or(false); |
| 52 | |
| 53 | // Uid and Gid must be supplied together — leaving one as the |
| 54 | // mkfs default (root) is a confusing footgun. |
| 55 | opts.Uid = parser.Optional<uint32_t>(c_uidOpt); |
| 56 | opts.Gid = parser.Optional<uint32_t>(c_gidOpt); |
| 57 | if (opts.Uid.has_value() != opts.Gid.has_value()) |
| 58 | { |
| 59 | const auto* missing = opts.Uid.has_value() ? c_gidOpt : c_uidOpt; |
| 60 | THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcMissingVolumeOption(missing)); |
| 61 | } |
| 62 | |
| 63 | parser.RejectUnknown(); |
| 64 | |
| 65 | return opts; |
| 66 | } |
| 67 | }; |
| 68 | |
| 69 | std::string GenerateName() |
| 70 | { |
| 71 | std::random_device rd; |
| 72 | std::independent_bits_engine<std::default_random_engine, CHAR_BIT, unsigned short> random(rd()); |
| 73 | |
| 74 | std::array<unsigned short, 32> randomBytes; |
| 75 | std::generate(randomBytes.begin(), randomBytes.end(), random); |
| 76 | |
| 77 | std::string name; |
| 78 | name.reserve(randomBytes.size() * 2); |
| 79 | for (auto b : randomBytes) |
| 80 | { |
| 81 | std::format_to(std::back_inserter(name), "{:02x}", static_cast<BYTE>(b)); |
| 82 | } |
| 83 | |
| 84 | return name; |
| 85 | } |
| 86 | |
| 87 | void RemoveLostFoundDirectory(WSLCVirtualMachine& VirtualMachine, const std::string& VolumeName, const std::string& MountPath) |
| 88 | try |
| 89 | { |
| 90 | constexpr auto c_lostFoundDir = "lost+found"; |
| 91 | const auto entries = VirtualMachine.ListDirectory(MountPath); |
| 92 | |
| 93 | // Only remove lost+found if the disk is empty besides that directory. |
| 94 | if (entries.size() != 1 || entries.front() != c_lostFoundDir) |
| 95 | { |
| 96 | return; |
| 97 | } |
| 98 | |
| 99 | try |
| 100 | { |
| 101 | VirtualMachine.RemoveDirectory(std::format("{}/{}", MountPath, c_lostFoundDir)); |
| 102 | } |
| 103 | catch (...) |
| 104 | { |
| 105 | // rmdir only removes an empty directory, so reaching here means the |
| 106 | // lone lost+found captured recovered data. Leave it and warn. |
| 107 | LOG_CAUGHT_EXCEPTION(); |
| 108 | EMIT_USER_WARNING(Localization::MessageWslcVolumeLostFoundNotEmpty(VolumeName)); |
| 109 | } |
| 110 | } |
| 111 | CATCH_LOG(); |
| 112 | } // namespace |
| 113 | |
| 114 | WSLCVhdVolumeImpl::WSLCVhdVolumeImpl( |
| 115 | std::string&& Name, |
| 116 | std::filesystem::path&& HostPath, |
| 117 | ULONGLONG SizeBytes, |
| 118 | ULONG Lun, |
| 119 | std::string&& VirtualMachinePath, |
| 120 | std::string&& CreatedAt, |
| 121 | std::string&& Mountpoint, |
| 122 | std::map<std::string, std::string>&& DriverOpts, |
| 123 | std::map<std::string, std::string>&& Labels, |
| 124 | WSLCVirtualMachine& VirtualMachine, |
| 125 | DockerHTTPClient& DockerClient, |
| 126 | bool Attached, |
| 127 | std::pair<HRESULT, std::string> Status) : |
| 128 | m_name(std::move(Name)), |
| 129 | m_hostPath(std::move(HostPath)), |
| 130 | m_virtualMachinePath(std::move(VirtualMachinePath)), |
| 131 | m_createdAt(std::move(CreatedAt)), |
| 132 | m_mountpoint(std::move(Mountpoint)), |
| 133 | m_driverOpts(std::move(DriverOpts)), |
| 134 | m_labels(std::move(Labels)), |
| 135 | m_sizeBytes(SizeBytes), |
| 136 | m_lun(Lun), |
| 137 | m_virtualMachine(VirtualMachine), |
| 138 | m_dockerClient(DockerClient), |
| 139 | m_attached(Attached), |
| 140 | m_status(std::move(Status)) |
| 141 | { |
| 142 | } |
| 143 | |
| 144 | WSLCVhdVolumeImpl::~WSLCVhdVolumeImpl() |
| 145 | { |
| 146 | Detach(); |
| 147 | } |
| 148 | |
| 149 | std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Create( |
| 150 | LPCSTR Name, |
| 151 | std::map<std::string, std::string>&& DriverOpts, |
| 152 | std::map<std::string, std::string>&& Labels, |
| 153 | const std::filesystem::path& StoragePath, |
| 154 | WSLCVirtualMachine& VirtualMachine, |
| 155 | DockerHTTPClient& DockerClient) |
| 156 | { |
| 157 | std::string name = (Name != nullptr && Name[0] != '\0') ? std::string(Name) : GenerateName(); |
| 158 | const auto opts = VhdVolumeOptions::Parse(DriverOpts); |
| 159 | auto hostPath = StoragePath / "volumes" / (name + ".vhdx"); |
| 160 | |
| 161 | auto createVhdCleanup = |
| 162 | wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(hostPath.c_str())); }); |
| 163 | |
| 164 | std::filesystem::create_directories(hostPath.parent_path()); |
| 165 | |
| 166 | const auto tokenInfo = wil::get_token_information<TOKEN_USER>(GetCurrentProcessToken()); |
| 167 | wsl::core::filesystem::CreateVhd(hostPath.c_str(), opts.SizeBytes, tokenInfo->User.Sid, false, opts.Fixed); |
| 168 | |
| 169 | auto [lun, device] = VirtualMachine.AttachDisk(hostPath.c_str(), false); |
| 170 | auto attachCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.DetachDisk(lun); }); |
| 171 | |
| 172 | // Ownership is baked into the ext4 root inode at format time so the |
| 173 | // container user can write without a post-mount chown. |
| 174 | VirtualMachine.Ext4Format(device, opts.Uid, opts.Gid); |
| 175 | |
| 176 | auto virtualMachinePath = std::format("/mnt/wslc-volumes/{}", name); |
| 177 | |
| 178 | // These should match the mount options used in Open |
| 179 | VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "discard", 0); |
| 180 | |
| 181 | auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); }); |
| 182 | |
| 183 | // mkfs.ext4 always creates a lost+found directory at the filesystem root, |
| 184 | // which makes a freshly formatted volume look non-empty to Docker and |
| 185 | // suppresses the copy-up that seeds image data on first use. Drop it so |
| 186 | // Docker seeds the volume with the image's contents. No-op when the volume |
| 187 | // already contains data. |
| 188 | RemoveLostFoundDirectory(VirtualMachine, name, virtualMachinePath); |
| 189 | |
| 190 | WSLCVolumeMetadata metadata; |
| 191 | metadata.Driver = WSLCVhdVolumeDriver; |
| 192 | metadata.DriverOpts = DriverOpts; |
| 193 | metadata.Properties = { |
| 194 | {"HostPath", hostPath.string()}, |
| 195 | }; |
| 196 | |
| 197 | docker_schema::CreateVolume request{}; |
| 198 | request.Name = name; |
| 199 | request.Driver = "local"; |
| 200 | request.DriverOpts = { |
| 201 | {"type", "none"}, |
| 202 | {"o", "bind"}, |
| 203 | {"device", virtualMachinePath}, |
| 204 | }; |
| 205 | request.Labels = {{WSLCVolumeMetadataLabel, wsl::shared::ToJson(metadata)}}; |
| 206 | |
| 207 | // Merge user labels into the Docker volume labels. |
| 208 | for (const auto& [key, value] : Labels) |
| 209 | { |
| 210 | request.Labels[key] = value; |
| 211 | } |
| 212 | |
| 213 | try |
| 214 | { |
| 215 | auto createdVolume = DockerClient.CreateVolume(request); |
| 216 | |
| 217 | auto volume = std::make_unique<WSLCVhdVolumeImpl>( |
| 218 | std::move(name), |
| 219 | std::move(hostPath), |
| 220 | opts.SizeBytes, |
| 221 | lun, |
| 222 | std::move(virtualMachinePath), |
| 223 | std::move(createdVolume.CreatedAt), |
| 224 | std::move(createdVolume.Mountpoint), |
| 225 | std::move(DriverOpts), |
| 226 | std::move(Labels), |
| 227 | VirtualMachine, |
| 228 | DockerClient); |
| 229 | |
| 230 | mountCleanup.release(); |
| 231 | attachCleanup.release(); |
| 232 | createVhdCleanup.release(); |
| 233 | |
| 234 | return volume; |
| 235 | } |
| 236 | CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to create volume '%hs'", name.c_str()); |
| 237 | } |
| 238 | |
| 239 | std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Open( |
| 240 | const wsl::windows::common::docker_schema::Volume& Volume, WSLCVirtualMachine& VirtualMachine, DockerHTTPClient& DockerClient) |
| 241 | { |
| 242 | THROW_HR_IF(E_INVALIDARG, !Volume.Labels.has_value()); |
| 243 | |
| 244 | auto metadataIt = Volume.Labels->find(WSLCVolumeMetadataLabel); |
| 245 | THROW_HR_IF(E_INVALIDARG, metadataIt == Volume.Labels->end()); |
| 246 | |
| 247 | auto metadata = wsl::shared::FromJson<WSLCVolumeMetadata>(metadataIt->second.c_str()); |
| 248 | THROW_HR_IF(E_INVALIDARG, metadata.Driver != WSLCVhdVolumeDriver); |
| 249 | |
| 250 | auto hostPathIt = metadata.Properties.find("HostPath"); |
| 251 | THROW_HR_IF(E_INVALIDARG, hostPathIt == metadata.Properties.end()); |
| 252 | THROW_HR_IF(E_INVALIDARG, hostPathIt->second.empty()); |
| 253 | |
| 254 | auto hostPath = std::filesystem::path(hostPathIt->second); |
| 255 | auto driverOpts = metadata.DriverOpts; |
| 256 | const auto opts = VhdVolumeOptions::Parse(driverOpts); |
| 257 | |
| 258 | THROW_HR_IF(E_INVALIDARG, !Volume.Options.has_value()); |
| 259 | auto deviceIt = Volume.Options->find("device"); |
| 260 | THROW_HR_IF(E_INVALIDARG, deviceIt == Volume.Options->end()); |
| 261 | THROW_HR_IF(E_INVALIDARG, deviceIt->second.empty()); |
| 262 | std::string virtualMachinePath = deviceIt->second; |
| 263 | |
| 264 | // Extract user labels (all labels except our internal metadata label). |
| 265 | std::map<std::string, std::string> userLabels; |
| 266 | for (const auto& [key, value] : *Volume.Labels) |
| 267 | { |
| 268 | if (key != WSLCVolumeMetadataLabel) |
| 269 | { |
| 270 | userLabels[key] = value; |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | ULONG lun = 0; |
| 275 | bool attached = false; |
| 276 | std::pair<HRESULT, std::string> status{S_OK, {}}; |
| 277 | |
| 278 | try |
| 279 | { |
| 280 | auto [attachedLun, device] = VirtualMachine.AttachDisk(hostPath.c_str(), false); |
| 281 | auto attachCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.DetachDisk(attachedLun); }); |
| 282 | |
| 283 | // These should match the mount options used in Create |
| 284 | VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "discard", 0); |
| 285 | auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); }); |
| 286 | |
| 287 | RemoveLostFoundDirectory(VirtualMachine, Volume.Name, virtualMachinePath); |
| 288 | |
| 289 | lun = attachedLun; |
| 290 | attached = true; |
| 291 | |
| 292 | mountCleanup.release(); |
| 293 | attachCleanup.release(); |
| 294 | } |
| 295 | catch (...) |
| 296 | { |
| 297 | // The backing VHD could not be attached or mounted. Track the volume in an errored state so the user can still inspect |
| 298 | // and delete it; containers that reference it should refuse to start. The reason is surfaced via Inspect(), not the warning. |
| 299 | const auto hr = wil::ResultFromCaughtException(); |
| 300 | const auto message = wslutil::GetErrorString(hr); |
| 301 | EMIT_USER_WARNING(Localization::MessageWslcFailedToRecoverVolume(Volume.Name)); |
| 302 | status = {hr, wsl::shared::string::WideToMultiByte(message)}; |
| 303 | } |
| 304 | |
| 305 | return std::make_unique<WSLCVhdVolumeImpl>( |
| 306 | std::string{Volume.Name}, |
| 307 | std::move(hostPath), |
| 308 | opts.SizeBytes, |
| 309 | lun, |
| 310 | std::move(virtualMachinePath), |
| 311 | std::string{Volume.CreatedAt}, |
| 312 | std::string{Volume.Mountpoint}, |
| 313 | std::move(driverOpts), |
| 314 | std::move(userLabels), |
| 315 | VirtualMachine, |
| 316 | DockerClient, |
| 317 | attached, |
| 318 | std::move(status)); |
| 319 | } |
| 320 | |
| 321 | void WSLCVhdVolumeImpl::Delete() |
| 322 | { |
| 323 | try |
| 324 | { |
| 325 | m_dockerClient.RemoveVolume(m_name); |
| 326 | } |
| 327 | catch (const DockerHTTPException& e) |
| 328 | { |
| 329 | THROW_HR_WITH_USER_ERROR_IF( |
| 330 | HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), Localization::MessageWslcVolumeInUse(m_name.c_str()), e.StatusCode() == 409); |
| 331 | THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(m_name.c_str()), e.StatusCode() == 404); |
| 332 | THROW_DOCKER_USER_ERROR_MSG(e, "Failed to delete volume '%hs'", m_name.c_str()); |
| 333 | } |
| 334 | |
| 335 | OnDeleted(); |
| 336 | } |
| 337 | |
| 338 | std::string WSLCVhdVolumeImpl::Inspect() const |
| 339 | { |
| 340 | wslc_schema::InspectVolume inspect{}; |
| 341 | inspect.Name = m_name; |
| 342 | inspect.Driver = WSLCVhdVolumeDriver; |
| 343 | inspect.CreatedAt = m_createdAt; |
| 344 | inspect.Mountpoint = m_mountpoint; |
| 345 | inspect.Scope = WSLCVolumeScope; |
| 346 | inspect.Options = m_driverOpts; |
| 347 | inspect.Labels = m_labels; |
| 348 | inspect.Status = std::map<std::string, std::string>{ |
| 349 | {"HostPath", m_hostPath.string()}, |
| 350 | {"SizeBytes", std::to_string(m_sizeBytes)}, |
| 351 | }; |
| 352 | |
| 353 | // Surface the recovery failure so callers can see why the volume is unusable. |
| 354 | if (FAILED(m_status.first)) |
| 355 | { |
| 356 | inspect.Status->emplace("Error", m_status.second); |
| 357 | } |
| 358 | |
| 359 | return wsl::shared::ToJson(inspect); |
| 360 | } |
| 361 | |
| 362 | WSLCVolumeInformation WSLCVhdVolumeImpl::GetVolumeInformation() const |
| 363 | { |
| 364 | WSLCVolumeInformation Info{}; |
| 365 | |
| 366 | THROW_HR_IF(E_UNEXPECTED, strcpy_s(Info.Name, m_name.c_str()) != 0); |
| 367 | THROW_HR_IF(E_UNEXPECTED, strcpy_s(Info.Driver, WSLCVhdVolumeDriver) != 0); |
| 368 | |
| 369 | return Info; |
| 370 | } |
| 371 | |
| 372 | void WSLCVhdVolumeImpl::OnDeleted() |
| 373 | { |
| 374 | Detach(); |
| 375 | LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_hostPath.c_str())); |
| 376 | } |
| 377 | |
| 378 | void WSLCVhdVolumeImpl::Detach() |
| 379 | try |
| 380 | { |
| 381 | if (!m_attached) |
| 382 | { |
| 383 | return; |
| 384 | } |
| 385 | |
| 386 | if (!m_virtualMachinePath.empty()) |
| 387 | { |
| 388 | m_virtualMachine.Unmount(m_virtualMachinePath.c_str()); |
| 389 | m_virtualMachinePath.clear(); |
| 390 | } |
| 391 | |
| 392 | m_virtualMachine.DetachDisk(m_lun); |
| 393 | m_attached = false; |
| 394 | } |
| 395 | CATCH_LOG(); |
| 396 | |
| 397 | } // namespace wsl::windows::service::wslc |