@samitouri / QOSAMI-WSL / commits / 41498d35

Add Uid/Gid/Fixed driver options to WSLC VHD volumes and expose via SDK (#40476)

* Add Uid/Gid/Mode/Fixed driver options to WSLC VHD volumes; expose via SDK The WSLC named-volume "vhd" driver only supported a single SizeBytes option, so containers running as a non-root user could not write to their own persistent volumes (mkfs.ext4 leaves the root owned by root:root with mode 0755). It also could not produce a fully-allocated VHD, which some workloads need for predictable I/O. Service side ============ * Adds new VHD driver options on top of SizeBytes: - Fixed=true|false pre-allocate the underlying VHD - Uid=<n> chown the volume root to uid (paired with Gid) - Gid=<n> chown the volume root to gid (paired with Uid) - Mode=<octal> chmod the volume root, max 07777, must be > 0 * Extracts a reusable OptionParser helper for typed option parsing with errno-capture, end-pointer validation, leading-sign rejection, consumed-key tracking, and a final RejectUnknown() pass. Used by WSLCVhdVolume's Create and Open paths so persisted metadata is validated identically on reload. Public C SDK ============ WslcVhdRequirements grows three new uint32_t fields (uid/gid/mode) and a WslcVhdRequirementsFlags bitmask. WslcCreateSessionVhdVolume: * honors WSLC_VHD_TYPE_FIXED (was previously E_NOTIMPL) * dynamically builds WSLCDriverOption[] based on which flags are set * rejects unknown type values, unknown flag bits, and mode == 0 with E_INVALIDARG so future flag additions cannot be silently ignored by older SDK versions and obvious foot-guns are caught client-side. WslcSetSessionSettingsVhd does NOT plumb owner/mode/fixed through the session rootfs VHD path, and now rejects flags != NONE with E_INVALIDARG instead of silently ignoring them. WSLC_SESSION_OPTIONS_SIZE bumps 80 -> 96 to match the wider embedded WslcVhdRequirements; this is an ABI break, callers must recompile. WinRT projection ================ VhdRequirements gains: void SetOwner(UInt32 uid, UInt32 gid); void SetMode(UInt32 mode); These set the corresponding flag bit and field on the underlying struct. Pair-based SetOwner avoids the half-set foot-gun that per-property setters would create. Tests ===== * WSLCTests.cpp: NamedVolumeVhdOptionsParseTest covers SizeBytes, unknown keys, sign rejection, range and base validation; a positive owner+mode test exercises chown/chmod end-to-end; a Fixed-allocation test asserts on-disk file_size >= requested size. * WslcSdkTests.cpp adds invalid-type, fixed-allocation, owner+mode positive, mode-out-of-range negative, mode==0 negative, unknown flag negative, and flags=NONE-ignores-uid/gid positive cases. The WinRT projection has no test infrastructure in the repo and is not unit-tested; behavior is covered at the C SDK layer that the projection delegates to. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address Copilot review feedback on PR #40476 Five findings from the Copilot pull-request reviewer: 1. wslcsdk.cpp: WslcCreateSessionVhdVolume unconditionally formatted options->uid / gid / mode via std::to_string and std::format even when the corresponding flag was not set. The header documents those fields as honored only when the flag is set, so a defensive caller could leave them uninitialized. Reading uninitialized memory is UB. Now only materialize uid/gid strings when FLAG_OWNER is set, and mode string when FLAG_MODE is set. 2. wslcsdk.idl: SetOwner/SetMode comments said they 'have no effect' on a VhdRequirements used with the session rootfs VHD. With the newly-strict WslcSetSessionSettingsVhd those flags now produce E_INVALIDARG instead of being silently ignored. Updated the IDL doc-comments to say the assignment will fail. 3. WSLCVhdVolume.cpp: service-side parser still accepted Mode=0, leaving direct COM callers (and persisted metadata reload) able to bypass the SDK-side check. Mode==0 is now rejected by Parse() for parity across all entry points. 4. WslcSdkTests.cpp: the owner+mode positive case only created and deleted the volume; nothing actually verified that chown/chmod were applied. Now mounts the volume into a debian:latest container and runs 'stat -c %u %g %a /data', asserting the output matches the requested 65534 65534 750. 5. OptionParser.h: lifetime-contract doc-comment was misleading — it implied accessors return references into the input map. In practice only Find() returns a pointer (used internally); the numeric/bool accessors return parsed values by value. Reworded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Mode=0 negative test for WSLC vhd parser Reviewer pointed out the service-side Mode parse tests had thorough coverage for non-octal, too-large, signed, and empty values, but no explicit case for the documented invalid value Mode=0 (spec is 1..07777). Mode==0 was already rejected by Parse() in the prior commit; this just locks the behavior in place. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate VhdRequirements::SetMode arguments at WinRT boundary Reviewer noted that the IDL doc-comment promised SetMode rejected out-of-range/zero values, but the WinRT setter blindly stored the value and validation only fired hours later inside CreateVolume. SetMode now throws hresult_invalid_argument for mode == 0 or mode > 07777 so callers see immediate failure at the API boundary. SetOwner doesn't need a parallel check — uid/gid are uint32_t and all values are valid POSIX user/group IDs. Also tightened the IDL comment to say validation happens at the setter (not deferred to creation). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Echo caller-provided value in SizeBytes/Mode validation errors Reviewer noted the SizeBytes==0 and Mode==0 rejection paths in VhdVolumeOptions::Parse hard-coded the literal 0 in their error messages instead of echoing the original input from DriverOpts. If a caller passed SizeBytes=00 or Mode=000, the error said '0', diverging from OptionParser's usual 'Invalid value for option <name>: <original>' wording. Both keys are guaranteed present in DriverOpts when these checks fire (Required<> already succeeded for SizeBytes; Mode.has_value() is the precondition for the Mode check), so .at() will not throw. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reject mode>07777 in C SDK and trim verbose comments The C SDK only rejected mode==0; the WinRT setter and the public header both promise mode<=07777 too. Aligning all three layers so callers see immediate, consistent E_INVALIDARG. Also a comment-bloat pass on this PR: kept "why" notes (uid/gid foot-gun, chmod 0 rationale, c_str lifetime), dropped restatements of what the code already says. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Move volume Uid/Gid into mkfs -E root_owner; drop Mode option Per OneBlue review feedback: bake ownership into the ext4 root inode at format time (mkfs.ext4 -E root_owner=UID:GID) instead of spawning a post-mount chown helper inside the VM. For a fresh volume the root is the only user-visible inode so this is equivalent — anything the container later creates inherits its own uid/gid. Drop the Mode option entirely. Containers that need non-default permissions can chmod from inside (it's a per-process concern); the SDK surface stays minimal. Also drops the now-unused Base parameter from OptionParser. ABI: WslcVhdRequirements shrinks 40 -> 32 bytes; WSLC_SESSION_OPTIONS_SIZE 96 -> 88. WSLC_VHD_REQ_FLAG_MODE and VhdRequirements::SetMode are removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * WSLC: tidy comments and add Ext4Format Uid/Gid contract assert * wslcsdk.cpp: drop stale 'Owner / mode' wording from VHD-rootfs rejection comment. * wslcsdk.idl: clarify that owner-on-rootfs fails at property-set time (via SetSessionSettingsVhd), not at session creation. * WSLCVirtualMachine.cpp::Ext4Format: assert Uid.has_value() == Gid.has_value() so a future caller bypassing the parser can't silently drop ownership. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * WSLC: drop misleading std::move on Ext4Format mkfs args WSLCProcessLauncher's constructor takes its arguments vector by const-ref, so std::move(args) here is a no-op and only obscures intent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * WSLC: trim verbose comments around VHD options Compress over-explained rationale comments in WSLCVhdVolume.cpp, WSLCVirtualMachine.cpp, OptionParser.h, wslcsdk.cpp/idl, and the matching tests. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * WSLC: address reviewer feedback on VHD option parser * OptionParser.h: include <cerrno> directly so the header is self-contained. * OptionParser: distinguish unknown keys from invalid values. RejectUnknown now throws via ThrowUnknown with a new MessageWslcUnknownVolumeOption string ('Unknown option: ...') instead of the misleading 'Invalid value for option ...' message. * WSLCVirtualMachine::Ext4Format: replace WI_ASSERT with THROW_HR_IF so a paired-Uid/Gid contract violation surfaces as a structured failure instead of a process-termination assert in production builds. * WslcSdkTests::SessionCreateVhd: add wil::scope_exit cleanup for the Fixed-VHD sub-test so a mid-test VERIFY failure can't leak the volume. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Ben Hillis committed May 13, 2026 at 12:41 UTC 41498d358d7adcf5208206f2dad3e49c36c7a808
14 files changed +554 -42
localization/strings/en-US/Resources.resw
+8
@@ -2254,6 +2254,14 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2254 <value>Missing required option: '{}'</value>
2255 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2256 </data>
2257 + <data name = "MessageWslcInvalidVolumeOption" xml:space = "preserve" >
2258 + <value>Invalid value for option '{}': '{}'</value>
2259 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2260 + </data>
2261 + <data name = "MessageWslcUnknownVolumeOption" xml:space = "preserve" >
2262 + <value>Unknown option: '{}'</value>
2263 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2264 + </data>
2265 <data name = "MessageWslcInvalidVolumeType" xml:space = "preserve" >
2266 <value>Unsupported volume type: '{}'</value>
2267 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/WslcSDK/winrt/VhdRequirements.cpp
+7
@@ -40,6 +40,13 @@ winrt::Microsoft::WSL::Containers::VhdType VhdRequirements::Type()
40 return static_cast<winrt::Microsoft::WSL::Containers::VhdType>(m_vhdRequirements.type);
41 }
42
43 +void VhdRequirements::SetOwner(uint32_t uid, uint32_t gid)
44 +{
45 + m_vhdRequirements.uid = uid;
46 + m_vhdRequirements.gid = gid;
47 + m_vhdRequirements.flags = m_vhdRequirements.flags | WSLC_VHD_REQ_FLAG_OWNER;
48 +}
49 +
50 WslcVhdRequirements* VhdRequirements::ToStructPointer()
51 {
52 return &m_vhdRequirements;
src/windows/WslcSDK/winrt/VhdRequirements.h
+2
@@ -26,6 +26,8 @@ struct VhdRequirements : VhdRequirementsT<VhdRequirements>
26 uint64_t SizeInBytes();
27 winrt::Microsoft::WSL::Containers::VhdType Type();
28
29 + void SetOwner(uint32_t uid, uint32_t gid);
30 +
31 WslcVhdRequirements* ToStructPointer();
32
33 private:
src/windows/WslcSDK/winrt/wslcsdk.idl
+5
@@ -55,5 +55,10 @@ namespace Microsoft.WSL.Containers
55 String Name { get; };
56 UInt64 SizeInBytes { get; };
57 VhdType Type { get; };
58 +
59 + // Sets owner uid/gid on the volume root inode at mkfs time. Only
60 + // supported on named volumes; setting this on a SessionSettings
61 + // VhdRequirements fails at property-set time with E_INVALIDARG.
62 + void SetOwner(UInt32 uid, UInt32 gid);
63 };
64 }
\ No newline at end of file
src/windows/WslcSDK/wslcsdk.cpp
+36 -6
@@ -493,16 +493,42 @@ try
493
494 RETURN_HR_IF_NULL(E_INVALIDARG, options->name);
495 RETURN_HR_IF(E_INVALIDARG, options->sizeBytes == 0);
496 - RETURN_HR_IF(E_NOTIMPL, options->type != WSLC_VHD_TYPE_DYNAMIC);
496 +
497 + // Reject unknown flag bits so future additions can't be silently ignored.
498 + constexpr WslcVhdRequirementsFlags c_knownFlags = WSLC_VHD_REQ_FLAG_OWNER;
499 + RETURN_HR_IF(E_INVALIDARG, (options->flags & ~c_knownFlags) != WSLC_VHD_REQ_FLAG_NONE);
500 +
501 + // Hold uid/gid strings at function scope so the c_str() pointers stored
502 + // in driverOpts stay valid through CreateVolume.
503 + const auto sizeStr = std::to_string(options->sizeBytes);
504 + std::string uidStr;
505 + std::string gidStr;
506 +
507 + std::vector<WSLCDriverOption> driverOpts;
508 + driverOpts.push_back({"SizeBytes", sizeStr.c_str()});
509 +
510 + if (options->type == WSLC_VHD_TYPE_FIXED)
511 + {
512 + driverOpts.push_back({"Fixed", "true"});
513 + }
514 + else
515 + {
516 + RETURN_HR_IF(E_INVALIDARG, options->type != WSLC_VHD_TYPE_DYNAMIC);
517 + }
518 +
519 + if (WI_IsFlagSet(options->flags, WSLC_VHD_REQ_FLAG_OWNER))
520 + {
521 + uidStr = std::to_string(options->uid);
522 + gidStr = std::to_string(options->gid);
523 + driverOpts.push_back({"Uid", uidStr.c_str()});
524 + driverOpts.push_back({"Gid", gidStr.c_str()});
525 + }
526
527 WSLCVolumeOptions volumeOptions{};
528 volumeOptions.Name = options->name;
529 volumeOptions.Driver = "vhd";
501 -
502 - auto sizeStr = std::to_string(options->sizeBytes);
503 - WSLCDriverOption driverOpts[] = {{"SizeBytes", sizeStr.c_str()}};
504 - volumeOptions.DriverOpts = driverOpts;
505 - volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
530 + volumeOptions.DriverOpts = driverOpts.data();
531 + volumeOptions.DriverOptsCount = static_cast<ULONG>(driverOpts.size());
532
533 WSLCVolumeInformation volumeInfo{};
534 return errorInfoWrapper.CaptureResult(internalType->session->CreateVolume(&volumeOptions, &volumeInfo));
@@ -532,6 +558,10 @@ try
558 RETURN_HR_IF(E_INVALIDARG, vhdRequirements->sizeBytes == 0);
559 RETURN_HR_IF(E_NOTIMPL, vhdRequirements->type != WSLC_VHD_TYPE_DYNAMIC);
560
561 + // Owner is only honored on named volumes; reject here so callers can't
562 + // mistakenly believe it applied to the session rootfs VHD.
563 + RETURN_HR_IF(E_INVALIDARG, vhdRequirements->flags != WSLC_VHD_REQ_FLAG_NONE);
564 +
565 internalType->vhdRequirements = *vhdRequirements;
566 }
567 else
src/windows/WslcSDK/wslcsdk.h
+17 -2
@@ -40,7 +40,7 @@ EXTERN_C_START
40 #define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */
41
42 // Session values
43 -#define WSLC_SESSION_OPTIONS_SIZE 80
43 +#define WSLC_SESSION_OPTIONS_SIZE 88
44 #define WSLC_SESSION_OPTIONS_ALIGNMENT 8
45
46 typedef struct WslcSessionSettings
@@ -80,15 +80,30 @@ typedef enum WslcContainerNetworkingMode
80 typedef enum WslcVhdType
81 {
82 WSLC_VHD_TYPE_DYNAMIC = 0, // Expanding VHDX (default)
83 - WSLC_VHD_TYPE_FIXED = 1
83 + WSLC_VHD_TYPE_FIXED = 1 // Fixed-allocation VHDX (only honored by WslcCreateSessionVhdVolume)
84 } WslcVhdType;
85
86 +typedef enum WslcVhdRequirementsFlags
87 +{
88 + WSLC_VHD_REQ_FLAG_NONE = 0x00000000,
89 + // When set, WslcVhdRequirements::uid and gid are honored. When clear,
90 + // those fields are ignored and the volume is left owned by root:root.
91 + WSLC_VHD_REQ_FLAG_OWNER = 0x00000001,
92 +} WslcVhdRequirementsFlags;
93 +
94 +DEFINE_ENUM_FLAG_OPERATORS(WslcVhdRequirementsFlags);
95 +
96 typedef struct WslcVhdRequirements
97 {
98 // Ignored by WslcSetSessionSettingsVhd
99 _In_z_ PCSTR name;
100 _In_ uint64_t sizeBytes; // Desired size (for create/expand)
101 _In_ WslcVhdType type;
102 + // The remaining fields are only honored by WslcCreateSessionVhdVolume.
103 + // WslcSetSessionSettingsVhd rejects non-NONE flags with E_INVALIDARG.
104 + _In_ WslcVhdRequirementsFlags flags;
105 + _In_ uint32_t uid; // honored iff (flags & WSLC_VHD_REQ_FLAG_OWNER)
106 + _In_ uint32_t gid; // honored iff (flags & WSLC_VHD_REQ_FLAG_OWNER)
107 } WslcVhdRequirements;
108
109 typedef enum WslcSessionFeatureFlags
src/windows/wslcsession/CMakeLists.txt
+2
@@ -26,6 +26,7 @@ set(SOURCES
26 DockerEventTracker.cpp
27 DockerHTTPClient.cpp
28 IORelay.cpp
29 + OptionParser.cpp
30 ServiceProcessLauncher.cpp
31 )
32
@@ -33,6 +34,7 @@ set(HEADERS
34 DockerEventTracker.h
35 DockerHTTPClient.h
36 IORelay.h
37 + OptionParser.h
38 ServiceProcessLauncher.h
39 WSLCContainer.h
40 WSLCContainerMetadata.h
src/windows/wslcsession/OptionParser.cpp new
+82
@@ -0,0 +1,82 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + OptionParser.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of OptionParser. Handles the non-template error/lookup
12 + primitives that the templated accessors in OptionParser.h call into.
13 +
14 +--*/
15 +
16 +#include "precomp.h"
17 +#include "OptionParser.h"
18 +
19 +using wsl::shared::Localization;
20 +
21 +namespace wsl::windows::service::wslc {
22 +
23 +OptionParser::OptionParser(const std::map<std::string, std::string>& Options) noexcept : m_options(Options)
24 +{
25 +}
26 +
27 +const std::string* OptionParser::Find(std::string_view Key)
28 +{
29 + const auto it = m_options.find(std::string(Key));
30 + if (it == m_options.end())
31 + {
32 + return nullptr;
33 + }
34 +
35 + m_consumed.insert(it->first);
36 + return &it->second;
37 +}
38 +
39 +std::optional<bool> OptionParser::OptionalBool(std::string_view Key)
40 +{
41 + const auto* value = Find(Key);
42 + if (value == nullptr)
43 + {
44 + return std::nullopt;
45 + }
46 +
47 + const auto parsed = wsl::shared::string::ParseBool(value->c_str());
48 + if (!parsed.has_value())
49 + {
50 + ThrowInvalid(Key, *value);
51 + }
52 +
53 + return parsed;
54 +}
55 +
56 +void OptionParser::RejectUnknown()
57 +{
58 + for (const auto& [key, value] : m_options)
59 + {
60 + if (m_consumed.find(key) == m_consumed.end())
61 + {
62 + ThrowUnknown(key);
63 + }
64 + }
65 +}
66 +
67 +void OptionParser::ThrowInvalid(std::string_view Key, const std::string& Value)
68 +{
69 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcInvalidVolumeOption(std::string(Key), Value));
70 +}
71 +
72 +void OptionParser::ThrowMissing(std::string_view Key)
73 +{
74 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcMissingVolumeOption(std::string(Key)));
75 +}
76 +
77 +void OptionParser::ThrowUnknown(std::string_view Key)
78 +{
79 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcUnknownVolumeOption(std::string(Key)));
80 +}
81 +
82 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/OptionParser.h new
+121
@@ -0,0 +1,121 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + OptionParser.h
8 +
9 +Abstract:
10 +
11 + Helper for parsing string-keyed driver options (e.g. WSLC volume driver
12 + options) into typed values with consistent error reporting.
13 +
14 + Each accessor records the keys it consumed so RejectUnknown() can flag
15 + options the caller passed but the driver does not support. All errors are
16 + surfaced through the WSLC localized message strings.
17 +
18 +--*/
19 +
20 +#pragma once
21 +
22 +#include <cerrno>
23 +#include <limits>
24 +#include <map>
25 +#include <optional>
26 +#include <set>
27 +#include <string>
28 +#include <string_view>
29 +#include <type_traits>
30 +
31 +namespace wsl::windows::service::wslc {
32 +
33 +class OptionParser
34 +{
35 +public:
36 + NON_COPYABLE(OptionParser);
37 +
38 + // Options map must outlive this OptionParser.
39 + explicit OptionParser(const std::map<std::string, std::string>& Options) noexcept;
40 +
41 + // Required unsigned integer. Throws E_INVALIDARG if missing, empty,
42 + // signed, non-decimal, overflows, or > Max.
43 + template <typename T>
44 + T Required(std::string_view Key, T Max = (std::numeric_limits<T>::max)());
45 +
46 + // As Required, but returns nullopt when the key is absent.
47 + template <typename T>
48 + std::optional<T> Optional(std::string_view Key, T Max = (std::numeric_limits<T>::max)());
49 +
50 + // Parses an optional boolean via wsl::shared::string::ParseBool
51 + // ("0"/"1"/"true"/"false", case-insensitive). Throws on unknown values.
52 + std::optional<bool> OptionalBool(std::string_view Key);
53 +
54 + // Throws E_INVALIDARG on the first option that was supplied but never consumed.
55 + void RejectUnknown();
56 +
57 +private:
58 + // Returns the value for Key (nullptr if absent) and marks it consumed.
59 + const std::string* Find(std::string_view Key);
60 +
61 + [[noreturn]] static void ThrowInvalid(std::string_view Key, const std::string& Value);
62 + [[noreturn]] static void ThrowMissing(std::string_view Key);
63 + [[noreturn]] static void ThrowUnknown(std::string_view Key);
64 +
65 + template <typename T>
66 + static T ParseUnsignedValue(std::string_view Key, const std::string& Value, T Max);
67 +
68 + const std::map<std::string, std::string>& m_options;
69 + std::set<std::string, std::less<>> m_consumed;
70 +};
71 +
72 +template <typename T>
73 +inline T OptionParser::Required(std::string_view Key, T Max)
74 +{
75 + const auto* value = Find(Key);
76 + if (value == nullptr)
77 + {
78 + ThrowMissing(Key);
79 + }
80 +
81 + return ParseUnsignedValue<T>(Key, *value, Max);
82 +}
83 +
84 +template <typename T>
85 +inline std::optional<T> OptionParser::Optional(std::string_view Key, T Max)
86 +{
87 + const auto* value = Find(Key);
88 + if (value == nullptr)
89 + {
90 + return std::nullopt;
91 + }
92 +
93 + return ParseUnsignedValue<T>(Key, *value, Max);
94 +}
95 +
96 +template <typename T>
97 +inline T OptionParser::ParseUnsignedValue(std::string_view Key, const std::string& Value, T Max)
98 +{
99 + static_assert(std::is_unsigned_v<T>, "OptionParser numeric accessors only support unsigned integer types");
100 +
101 + // strtoull treats a leading '-' as unsigned wraparound and accepts '+';
102 + // reject both (and empty input) up-front.
103 + if (Value.empty() || Value.front() == '-' || Value.front() == '+')
104 + {
105 + ThrowInvalid(Key, Value);
106 + }
107 +
108 + errno = 0;
109 + char* end = nullptr;
110 + const auto parsed = wsl::shared::string::ToUInt64(Value.c_str(), &end, 10);
111 + // Capture errno immediately so debug allocators/logging hooks can't stomp it.
112 + const int parseErrno = errno;
113 + if (parseErrno != 0 || end == nullptr || *end != '\0' || parsed > static_cast<uint64_t>(Max))
114 + {
115 + ThrowInvalid(Key, Value);
116 + }
117 +
118 + return static_cast<T>(parsed);
119 +}
120 +
121 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCVhdVolume.cpp
+48 -22
@@ -14,6 +14,7 @@ Abstract:
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"
@@ -26,6 +27,45 @@ using wsl::shared::Localization;
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;
@@ -44,22 +84,6 @@ namespace {
84 return name;
85 }
86
47 - ULONGLONG ParseSizeBytes(std::map<std::string, std::string>& DriverOpts)
48 - {
49 - const auto it = DriverOpts.find("SizeBytes");
50 - THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcMissingVolumeOption("SizeBytes"), it == DriverOpts.end());
51 -
52 - auto& value = it->second;
53 - THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageInvalidSize(value), value.empty() || value[0] == '-');
54 -
55 - errno = 0;
56 - char* end = nullptr;
57 - auto sizeBytes = wsl::shared::string::ToUInt64(value.c_str(), &end);
58 - THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageInvalidSize(value), errno != 0 || *end != '\0' || sizeBytes == 0);
59 -
60 - return sizeBytes;
61 - }
62 -
87 } // namespace
88
89 WSLCVhdVolumeImpl::WSLCVhdVolumeImpl(
@@ -98,7 +122,7 @@ std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Create(
122 DockerHTTPClient& DockerClient)
123 {
124 std::string name = (Name != nullptr && Name[0] != '\0') ? std::string(Name) : GenerateName();
101 - auto sizeBytes = ParseSizeBytes(DriverOpts);
125 + const auto opts = VhdVolumeOptions::Parse(DriverOpts);
126 auto hostPath = StoragePath / "volumes" / (name + ".vhdx");
127
128 auto createVhdCleanup =
@@ -107,12 +131,14 @@ std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Create(
131 std::filesystem::create_directories(hostPath.parent_path());
132
133 const auto tokenInfo = wil::get_token_information<TOKEN_USER>(GetCurrentProcessToken());
110 - wsl::core::filesystem::CreateVhd(hostPath.c_str(), sizeBytes, tokenInfo->User.Sid, false, false);
134 + wsl::core::filesystem::CreateVhd(hostPath.c_str(), opts.SizeBytes, tokenInfo->User.Sid, false, opts.Fixed);
135
136 auto [lun, device] = VirtualMachine.AttachDisk(hostPath.c_str(), false);
137 auto attachCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.DetachDisk(lun); });
138
115 - VirtualMachine.Ext4Format(device);
139 + // Ownership is baked into the ext4 root inode at format time so the
140 + // container user can write without a post-mount chown.
141 + VirtualMachine.Ext4Format(device, opts.Uid, opts.Gid);
142
143 auto virtualMachinePath = std::format("/mnt/wslc-volumes/{}", name);
144 VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "", 0);
@@ -147,7 +173,7 @@ std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Create(
173 auto createdVolume = DockerClient.CreateVolume(request);
174
175 auto volume = std::make_unique<WSLCVhdVolumeImpl>(
150 - std::move(name), std::move(hostPath), sizeBytes, lun, std::move(virtualMachinePath), std::move(DriverOpts), std::move(Labels), VirtualMachine, DockerClient);
176 + std::move(name), std::move(hostPath), opts.SizeBytes, lun, std::move(virtualMachinePath), std::move(DriverOpts), std::move(Labels), VirtualMachine, DockerClient);
177 volume->m_createdAt = createdVolume.CreatedAt;
178
179 mountCleanup.release();
@@ -176,7 +202,7 @@ std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Open(
202
203 auto hostPath = std::filesystem::path(hostPathIt->second);
204 auto driverOpts = metadata.DriverOpts;
179 - auto sizeBytes = ParseSizeBytes(driverOpts);
205 + const auto opts = VhdVolumeOptions::Parse(driverOpts);
206
207 THROW_HR_IF(E_INVALIDARG, !Volume.Options.has_value());
208 auto deviceIt = Volume.Options->find("device");
@@ -201,7 +227,7 @@ std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Open(
227 auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
228
229 auto volume = std::make_unique<WSLCVhdVolumeImpl>(
204 - std::string{Volume.Name}, std::move(hostPath), sizeBytes, lun, std::move(virtualMachinePath), std::move(driverOpts), std::move(userLabels), VirtualMachine, DockerClient);
230 + std::string{Volume.Name}, std::move(hostPath), opts.SizeBytes, lun, std::move(virtualMachinePath), std::move(driverOpts), std::move(userLabels), VirtualMachine, DockerClient);
231 volume->m_createdAt = Volume.CreatedAt;
232
233 mountCleanup.release();
src/windows/wslcsession/WSLCVirtualMachine.cpp
+17 -2
@@ -479,10 +479,25 @@ std::pair<ULONG, std::string> WSLCVirtualMachine::AttachDisk(_In_ PCWSTR Path, _
479 return {Lun, Device};
480 }
481
482 -void WSLCVirtualMachine::Ext4Format(const std::string& Device)
482 +void WSLCVirtualMachine::Ext4Format(const std::string& Device, std::optional<uint32_t> Uid, std::optional<uint32_t> Gid)
483 {
484 constexpr auto mkfsPath = "/usr/sbin/mkfs.ext4";
485 - ServiceProcessLauncher launcher(mkfsPath, {mkfsPath, Device});
485 +
486 + // Uid/Gid must be paired; the named-volume parser enforces this for user
487 + // input — this guards future internal callers that bypass it.
488 + THROW_HR_IF(E_UNEXPECTED, Uid.has_value() != Gid.has_value());
489 +
490 + std::vector<std::string> args = {mkfsPath};
491 + std::string rootOwner;
492 + if (Uid.has_value() && Gid.has_value())
493 + {
494 + rootOwner = std::format("root_owner={}:{}", *Uid, *Gid);
495 + args.push_back("-E");
496 + args.push_back(rootOwner);
497 + }
498 + args.push_back(Device);
499 +
500 + ServiceProcessLauncher launcher(mkfsPath, args);
501 auto result = launcher.Launch(*this).WaitAndCaptureOutput();
502
503 THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
src/windows/wslcsession/WSLCVirtualMachine.h
+2 -1
@@ -22,6 +22,7 @@ Abstract:
22 #include "WSLCContainerMetadata.h"
23 #include <thread>
24 #include <filesystem>
25 +#include <optional>
26
27 namespace wsl::windows::service::wslc {
28
@@ -148,7 +149,7 @@ public:
149
150 std::pair<ULONG, std::string> AttachDisk(_In_ PCWSTR Path, _In_ BOOL ReadOnly);
151 void DetachDisk(_In_ ULONG Lun);
151 - void Ext4Format(_In_ const std::string& Device);
152 + void Ext4Format(_In_ const std::string& Device, _In_ std::optional<uint32_t> Uid = std::nullopt, _In_ std::optional<uint32_t> Gid = std::nullopt);
153 void Mount(_In_ LPCSTR Source, _In_ LPCSTR Target, _In_ LPCSTR Type, _In_ LPCSTR Options, _In_ ULONG Flags);
154
155 wil::unique_socket ConnectUnixSocket(_In_ const char* Path);
test/windows/WSLCTests.cpp
+123 -7
@@ -4118,26 +4118,142 @@ class WSLCTests
4118
4119 // Invalid SizeBytes values.
4120 WSLCDriverOption emptySize[] = {{"SizeBytes", ""}};
4121 - validateInvalidOptionsFailure(emptySize, ARRAYSIZE(emptySize), E_INVALIDARG, L"Invalid size: ");
4121 + validateInvalidOptionsFailure(emptySize, ARRAYSIZE(emptySize), E_INVALIDARG, L"Invalid value for option 'SizeBytes': ''");
4122
4123 WSLCDriverOption zeroSize[] = {{"SizeBytes", "0"}};
4124 - validateInvalidOptionsFailure(zeroSize, ARRAYSIZE(zeroSize), E_INVALIDARG, L"Invalid size: 0");
4124 + validateInvalidOptionsFailure(zeroSize, ARRAYSIZE(zeroSize), E_INVALIDARG, L"Invalid value for option 'SizeBytes': '0'");
4125
4126 WSLCDriverOption invalidSizeAbc[] = {{"SizeBytes", "abc"}};
4127 - validateInvalidOptionsFailure(invalidSizeAbc, ARRAYSIZE(invalidSizeAbc), E_INVALIDARG, L"Invalid size: abc");
4127 + validateInvalidOptionsFailure(
4128 + invalidSizeAbc, ARRAYSIZE(invalidSizeAbc), E_INVALIDARG, L"Invalid value for option 'SizeBytes': 'abc'");
4129
4130 WSLCDriverOption invalidSizeMixed[] = {{"SizeBytes", "123abc"}};
4130 - validateInvalidOptionsFailure(invalidSizeMixed, ARRAYSIZE(invalidSizeMixed), E_INVALIDARG, L"Invalid size: 123abc");
4131 + validateInvalidOptionsFailure(
4132 + invalidSizeMixed, ARRAYSIZE(invalidSizeMixed), E_INVALIDARG, L"Invalid value for option 'SizeBytes': '123abc'");
4133
4134 WSLCDriverOption invalidSizeSign[] = {{"SizeBytes", "+-1"}};
4133 - validateInvalidOptionsFailure(invalidSizeSign, ARRAYSIZE(invalidSizeSign), E_INVALIDARG, L"Invalid size: +-1");
4135 + validateInvalidOptionsFailure(
4136 + invalidSizeSign, ARRAYSIZE(invalidSizeSign), E_INVALIDARG, L"Invalid value for option 'SizeBytes': '+-1'");
4137
4138 WSLCDriverOption invalidSizeOverflow[] = {{"SizeBytes", "18446744073709551616"}};
4139 validateInvalidOptionsFailure(
4137 - invalidSizeOverflow, ARRAYSIZE(invalidSizeOverflow), E_INVALIDARG, L"Invalid size: 18446744073709551616");
4140 + invalidSizeOverflow,
4141 + ARRAYSIZE(invalidSizeOverflow),
4142 + E_INVALIDARG,
4143 + L"Invalid value for option 'SizeBytes': '18446744073709551616'");
4144
4145 WSLCDriverOption invalidSizeNeg[] = {{"SizeBytes", "-1"}};
4140 - validateInvalidOptionsFailure(invalidSizeNeg, ARRAYSIZE(invalidSizeNeg), E_INVALIDARG, L"Invalid size: -1");
4146 + validateInvalidOptionsFailure(
4147 + invalidSizeNeg, ARRAYSIZE(invalidSizeNeg), E_INVALIDARG, L"Invalid value for option 'SizeBytes': '-1'");
4148 +
4149 + // Invalid Fixed values.
4150 + WSLCDriverOption invalidFixed[] = {{"SizeBytes", "1073741824"}, {"Fixed", "yes"}};
4151 + validateInvalidOptionsFailure(
4152 + invalidFixed, ARRAYSIZE(invalidFixed), E_INVALIDARG, L"Invalid value for option 'Fixed': 'yes'");
4153 +
4154 + WSLCDriverOption emptyFixed[] = {{"SizeBytes", "1073741824"}, {"Fixed", ""}};
4155 + validateInvalidOptionsFailure(emptyFixed, ARRAYSIZE(emptyFixed), E_INVALIDARG, L"Invalid value for option 'Fixed': ''");
4156 +
4157 + // Invalid Uid values. Tests pair Uid with a valid Gid because Parse
4158 + // requires both to be present together.
4159 + WSLCDriverOption negUid[] = {{"SizeBytes", "1073741824"}, {"Uid", "-1"}, {"Gid", "0"}};
4160 + validateInvalidOptionsFailure(negUid, ARRAYSIZE(negUid), E_INVALIDARG, L"Invalid value for option 'Uid': '-1'");
4161 +
4162 + WSLCDriverOption abcUid[] = {{"SizeBytes", "1073741824"}, {"Uid", "abc"}, {"Gid", "0"}};
4163 + validateInvalidOptionsFailure(abcUid, ARRAYSIZE(abcUid), E_INVALIDARG, L"Invalid value for option 'Uid': 'abc'");
4164 +
4165 + WSLCDriverOption hugeUid[] = {{"SizeBytes", "1073741824"}, {"Uid", "4294967296"}, {"Gid", "0"}}; // 2^32, exceeds uint32_t max
4166 + validateInvalidOptionsFailure(hugeUid, ARRAYSIZE(hugeUid), E_INVALIDARG, L"Invalid value for option 'Uid': '4294967296'");
4167 +
4168 + // Invalid Gid values.
4169 + WSLCDriverOption negGid[] = {{"SizeBytes", "1073741824"}, {"Uid", "0"}, {"Gid", "-1"}};
4170 + validateInvalidOptionsFailure(negGid, ARRAYSIZE(negGid), E_INVALIDARG, L"Invalid value for option 'Gid': '-1'");
4171 +
4172 + // Uid without Gid (or vice versa) is rejected.
4173 + WSLCDriverOption uidOnly[] = {{"SizeBytes", "1073741824"}, {"Uid", "1000"}};
4174 + validateInvalidOptionsFailure(uidOnly, ARRAYSIZE(uidOnly), E_INVALIDARG, L"Missing required option: 'Gid'");
4175 +
4176 + WSLCDriverOption gidOnly[] = {{"SizeBytes", "1073741824"}, {"Gid", "1000"}};
4177 + validateInvalidOptionsFailure(gidOnly, ARRAYSIZE(gidOnly), E_INVALIDARG, L"Missing required option: 'Uid'");
4178 +
4179 + // Unknown options are rejected (catches typos and unsupported keys).
4180 + WSLCDriverOption unknownOpt[] = {{"SizeBytes", "1073741824"}, {"Bogus", "value"}};
4181 + validateInvalidOptionsFailure(unknownOpt, ARRAYSIZE(unknownOpt), E_INVALIDARG, L"Unknown option: 'Bogus'");
4182 + }
4183 +
4184 + WSLC_TEST_METHOD(NamedVolumesVhdOwnership)
4185 + {
4186 + // Verify Uid/Gid are baked into the root inode at mkfs time so a
4187 + // non-root container user can write to the volume.
4188 + const std::string volumeName = "wslc-test-vhd-ownership";
4189 +
4190 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
4191 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
4192 +
4193 + // nobody/nogroup are typically uid=65534 / gid=65534 on Debian.
4194 + WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}, {"Uid", "65534"}, {"Gid", "65534"}};
4195 +
4196 + WSLCVolumeOptions volumeOptions{};
4197 + volumeOptions.Name = volumeName.c_str();
4198 + volumeOptions.Driver = "vhd";
4199 + volumeOptions.DriverOpts = driverOpts;
4200 + volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
4201 +
4202 + WSLCVolumeInformation volInfo{};
4203 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
4204 +
4205 + // A container running as 'nobody' should be able to write to the volume.
4206 + {
4207 + WSLCContainerLauncher writer(
4208 + "debian:latest", "vhd-ownership-writer", {"/bin/sh", "-c", "echo non-root >/data/marker.txt"});
4209 + writer.AddNamedVolume(volumeName, "/data", false);
4210 + writer.SetUser("nobody:nogroup");
4211 +
4212 + auto writerContainer = writer.Launch(*m_defaultSession);
4213 + auto writerProcess = writerContainer.GetInitProcess();
4214 + ValidateProcessOutput(writerProcess, {});
4215 + }
4216 +
4217 + // Verify the file is owned by the same uid/gid as the volume root.
4218 + {
4219 + WSLCContainerLauncher checker(
4220 + "debian:latest", "vhd-ownership-checker", {"/bin/sh", "-c", "stat -c '%u %g' /data && cat /data/marker.txt"});
4221 + checker.AddNamedVolume(volumeName, "/data", true);
4222 +
4223 + auto checkerContainer = checker.Launch(*m_defaultSession);
4224 + auto checkerProcess = checkerContainer.GetInitProcess();
4225 + ValidateProcessOutput(checkerProcess, {{1, "65534 65534\nnon-root\n"}});
4226 + }
4227 + }
4228 +
4229 + WSLC_TEST_METHOD(NamedVolumesVhdFixed)
4230 + {
4231 + // Fixed=true produces a .vhdx whose on-disk size is at least SizeBytes.
4232 + const std::string volumeName = "wslc-test-vhd-fixed";
4233 + const std::filesystem::path volumeVhdPath = m_storagePath / "volumes" / (volumeName + ".vhdx");
4234 + constexpr ULONGLONG c_sizeBytes = 64 * _1MB;
4235 +
4236 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
4237 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
4238 +
4239 + const auto sizeBytesStr = std::to_string(c_sizeBytes);
4240 + WSLCDriverOption driverOpts[] = {{"SizeBytes", sizeBytesStr.c_str()}, {"Fixed", "true"}};
4241 +
4242 + WSLCVolumeOptions volumeOptions{};
4243 + volumeOptions.Name = volumeName.c_str();
4244 + volumeOptions.Driver = "vhd";
4245 + volumeOptions.DriverOpts = driverOpts;
4246 + volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
4247 +
4248 + WSLCVolumeInformation volInfo{};
4249 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
4250 +
4251 + VERIFY_IS_TRUE(std::filesystem::exists(volumeVhdPath));
4252 + const auto fileSize = std::filesystem::file_size(volumeVhdPath);
4253 +
4254 + // A dynamic VHD for a 64MB volume is typically a few MB; a fixed VHD
4255 + // pre-allocates the full payload (>= SizeBytes).
4256 + VERIFY_IS_GREATER_THAN_OR_EQUAL(fileSize, c_sizeBytes);
4257 }
4258
4259 WSLC_TEST_METHOD(ListAndInspectNamedVolumesTest)
test/windows/WslcSdkTests.cpp
+84 -2
@@ -1992,13 +1992,95 @@ class WslcSdkTests
1992 VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG);
1993 }
1994
1995 - // Negative: fixed VHD type is not yet supported.
1995 + // Negative: invalid VHD type must fail.
1996 {
1997 WslcVhdRequirements vhd{};
1998 vhd.name = c_volumeName;
1999 vhd.sizeBytes = c_vhdSizeBytes;
2000 + vhd.type = static_cast<WslcVhdType>(42);
2001 + VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG);
2002 + }
2003 +
2004 + // Positive: fixed-allocation VHD; on-disk file size must be >= SizeBytes.
2005 + {
2006 + constexpr auto c_fixedVolumeName = "wslc-sdk-vhd-fixed";
2007 + constexpr auto c_fixedSizeBytes = 64ull * _1MB;
2008 + WslcVhdRequirements vhd{};
2009 + vhd.name = c_fixedVolumeName;
2010 + vhd.sizeBytes = c_fixedSizeBytes;
2011 vhd.type = WSLC_VHD_TYPE_FIXED;
2001 - VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_NOTIMPL);
2012 + wil::unique_cotaskmem_string errorMsg;
2013 + VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg));
2014 +
2015 + auto deleteVolume =
2016 + wil::scope_exit([&]() { LOG_IF_FAILED(WslcDeleteSessionVhdVolume(session.get(), c_fixedVolumeName, nullptr)); });
2017 +
2018 + std::filesystem::path expectedVhdPath = vhdSessionStorage / "volumes" / (std::string(c_fixedVolumeName) + ".vhdx");
2019 + VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath));
2020 + VERIFY_IS_GREATER_THAN_OR_EQUAL(std::filesystem::file_size(expectedVhdPath), c_fixedSizeBytes);
2021 + }
2022 +
2023 + // Positive: owner flags are honored — uid/gid baked into the volume root
2024 + // inode at mkfs time. Verify by stat-ing the mount inside a container.
2025 + {
2026 + constexpr auto c_ownedVolumeName = "wslc-sdk-vhd-owned";
2027 + WslcVhdRequirements vhd{};
2028 + vhd.name = c_ownedVolumeName;
2029 + vhd.sizeBytes = c_vhdSizeBytes;
2030 + vhd.type = WSLC_VHD_TYPE_DYNAMIC;
2031 + vhd.flags = WSLC_VHD_REQ_FLAG_OWNER;
2032 + vhd.uid = 65534; // nobody
2033 + vhd.gid = 65534; // nogroup
2034 + wil::unique_cotaskmem_string errorMsg;
2035 + VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg));
2036 +
2037 + auto deleteVolume =
2038 + wil::scope_exit([&]() { LOG_IF_FAILED(WslcDeleteSessionVhdVolume(session.get(), c_ownedVolumeName, nullptr)); });
2039 +
2040 + WslcProcessSettings procSettings;
2041 + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2042 + const char* argv[] = {"/usr/bin/stat", "-c", "%u %g", "/data"};
2043 + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2044 +
2045 + WslcContainerSettings containerSettings;
2046 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2047 + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2048 +
2049 + WslcContainerNamedVolume namedVol{};
2050 + namedVol.name = c_ownedVolumeName;
2051 + namedVol.containerPath = "/data";
2052 + namedVol.readOnly = FALSE;
2053 + VERIFY_SUCCEEDED(WslcSetContainerSettingsNamedVolumes(&containerSettings, &namedVol, 1));
2054 +
2055 + auto output = RunContainerAndCapture(session.get(), containerSettings);
2056 + VERIFY_ARE_EQUAL(output.stdoutOutput, "65534 65534\n");
2057 + }
2058 +
2059 + // Negative: unknown flag bits are rejected.
2060 + {
2061 + WslcVhdRequirements vhd{};
2062 + vhd.name = c_volumeName;
2063 + vhd.sizeBytes = c_vhdSizeBytes;
2064 + vhd.type = WSLC_VHD_TYPE_DYNAMIC;
2065 + vhd.flags = static_cast<WslcVhdRequirementsFlags>(0x80000000);
2066 + VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG);
2067 + }
2068 +
2069 + // Positive: flags=NONE silently ignores uid/gid (volume defaults to root:root).
2070 + {
2071 + constexpr auto c_unflaggedVolumeName = "wslc-sdk-vhd-unflagged";
2072 + WslcVhdRequirements vhd{};
2073 + vhd.name = c_unflaggedVolumeName;
2074 + vhd.sizeBytes = c_vhdSizeBytes;
2075 + vhd.type = WSLC_VHD_TYPE_DYNAMIC;
2076 + vhd.flags = WSLC_VHD_REQ_FLAG_NONE;
2077 + vhd.uid = 1000;
2078 + vhd.gid = 1000;
2079 + wil::unique_cotaskmem_string errorMsg;
2080 + VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg));
2081 +
2082 + wil::unique_cotaskmem_string deleteErr;
2083 + VERIFY_SUCCEEDED(WslcDeleteSessionVhdVolume(session.get(), c_unflaggedVolumeName, &deleteErr));
2084 }
2085 }
2086