Add Uid/Gid/Fixed driver options to WSLC VHD volumes and expose via SDK#40476
Conversation
… 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>
There was a problem hiding this comment.
Pull request overview
Adds support for additional vhd named-volume driver options (POSIX Uid/Gid/Mode and Fixed allocation) and exposes them through the public WSLC C SDK and WinRT projection so SDK callers can create volumes usable by non-root container users and optionally pre-allocate VHD space.
Changes:
- Added a shared
OptionParserutility and updated the VHD volume driver to parse/validateSizeBytes,Fixed,Uid/Gid, andMode, applying ownership/permissions at volume creation time. - Extended the C SDK
WslcVhdRequirementscontract (new flags + fields) and updatedWslcCreateSessionVhdVolume/WslcSetSessionSettingsVhdbehavior accordingly. - Added/updated Windows tests for driver-option parsing and SDK behavior, plus a new localized error string for invalid volume options.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/windows/WSLCTests.cpp | Expands VHD named-volume option parsing tests; adds end-to-end ownership/mode and fixed-allocation coverage. |
| test/windows/WslcSdkTests.cpp | Updates SDK tests for fixed VHD support, new owner/mode flags, and validation behavior. |
| src/windows/wslcsession/WSLCVhdVolume.cpp | Implements parsing/validation for new VHD driver options and applies chown/chmod during volume creation; uses shared parser. |
| src/windows/wslcsession/OptionParser.h | Introduces typed driver-option parsing interface (templates + consumed-key tracking). |
| src/windows/wslcsession/OptionParser.cpp | Implements lookup, bool parsing, unknown-option rejection, and localized error throwing for OptionParser. |
| src/windows/wslcsession/CMakeLists.txt | Wires new OptionParser sources into the wslcsession build. |
| src/windows/WslcSDK/wslcsdk.h | Extends public C SDK types (WslcVhdRequirementsFlags, larger WslcVhdRequirements, updated session options size). |
| src/windows/WslcSDK/wslcsdk.cpp | Updates WslcCreateSessionVhdVolume to emit new driver options and WslcSetSessionSettingsVhd to reject unsupported flags. |
| src/windows/WslcSDK/winrt/wslcsdk.idl | Extends WinRT VhdRequirements projection with SetOwner/SetMode. |
| src/windows/WslcSDK/winrt/VhdRequirements.h | Adds WinRT wrapper method declarations for owner/mode setters. |
| src/windows/WslcSDK/winrt/VhdRequirements.cpp | Implements WinRT owner/mode setters by setting C SDK struct fields and flags. |
| localization/strings/en-US/Resources.resw | Adds localized MessageWslcInvalidVolumeOption string used for consistent option-validation errors. |
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>
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>
b06edbb to
a576eac
Compare
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>
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>
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>
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>
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>
* 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>
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>
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>
* 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>
JohnMcPMS
left a comment
There was a problem hiding this comment.
Went to lunch while it was open...
| _In_z_ PCSTR name; | ||
| _In_ uint64_t sizeBytes; // Desired size (for create/expand) | ||
| _In_ WslcVhdType type; | ||
| // The remaining fields are only honored by WslcCreateSessionVhdVolume. |
There was a problem hiding this comment.
Should we just make the session creation setting value only the size, rather than taking in a struct in which every field is ignored except the size? Or do we expect that these limitations are temporary and the type/flags/owner properties are eventually to be implemented?
Not saying you need to do it in this change, I will create one if it is deemed a good idea. An option is another struct (WslcSessionCreationVhdRequirements) that contains only the values valid for session creation that WslcVhdRequirements derives from. If/as fields are implemented for the primary VHD, they can be shifted without impacting code as written.
| template <typename T> | ||
| inline T OptionParser::ParseUnsignedValue(std::string_view Key, const std::string& Value, T Max) | ||
| { | ||
| static_assert(std::is_unsigned_v<T>, "OptionParser numeric accessors only support unsigned integer types"); |
There was a problem hiding this comment.
| static_assert(std::is_unsigned_v<T>, "OptionParser numeric accessors only support unsigned integer types"); | |
| static_assert(std::is_integral_v<T> && std::is_unsigned_v<T> && sizeof(T) <= sizeof(uint64_t), "OptionParser numeric accessors only support unsigned integer types up to 64 bits"); |
super-duper-nit: if an unsigned floating-point type were to exist (and T(-1) were meaningful), this would allow it.
nit: More realistically, a larger unsigned integral type might exist.
|
|
||
| const std::string* OptionParser::Find(std::string_view Key) | ||
| { | ||
| const auto it = m_options.find(std::string(Key)); |
There was a problem hiding this comment.
If you always have to convert to a std::string, consider a const std::string&. If there are an annoying number of cases where you hold a std::string_view, have an override/view specific function.
Summary
Adds POSIX
Uid/Gidand pre-allocatedFixeddriver options to the WSLC named-volumevhddriver, and plumbs them through the public C SDK and the WinRT projection.Today, containers running as a non-root user cannot write to their own persistent volumes —
mkfs.ext4leaves the root inode owned byroot:root, and thevhddriver only acceptedSizeBytes. The new options let the SDK consumer create a volume that is immediately usable by the in-container user.Fixedlets workloads that need predictable I/O pre-allocate the VHD instead of growing it dynamically.Driver options (named volumes)
SizeBytesFixedtrue/false/1/0)falseUidGidGidUidOwnership is baked into the ext4 root inode at format time via
mkfs.ext4 -E root_owner=UID:GID, not via a post-mountchown. The container user owns the root inode from the first mount and canchmodit with its own umask if it needs different permissions.All option parsing goes through a shared
OptionParserhelper (Required<T>/Optional<T>/OptionalBool/RejectUnknown) with errno-capture, end-pointer validation, leading-sign rejection, and consumed-key tracking. TheOpenpath uses the same parser so persisted metadata is validated identically on reload.C SDK (
wslcsdk.h/wslcsdk.dll)WslcCreateSessionVhdVolume:WSLC_VHD_TYPE_FIXED(was previouslyE_NOTIMPL)WSLCDriverOption[]based on the flag bitstypevalues and unknown flag bits withE_INVALIDARGso future additions cannot silently fail-openWslcSetSessionSettingsVhddoes not plumb owner/fixed through the session rootfs VHD path and rejectsflags != NONEwithE_INVALIDARGinstead of silently ignoring those fields.Important
ABI break:
WSLC_SESSION_OPTIONS_SIZEbumps80 → 88to match the wider embeddedWslcVhdRequirements. Callers ofwslcsdk.lib/wslcsdk.dllmust recompile against the new header.Caller usage
WinRT projection (
Microsoft.WSL.Containers)runtimeclass VhdRequirements { VhdRequirements(String name, UInt64 sizeInBytes, VhdType type); String Name { get; }; UInt64 SizeInBytes { get; }; VhdType Type { get; }; void SetOwner(UInt32 uid, UInt32 gid); }Pair-based
SetOwneravoids the half-set foot-gun that per-property setters would create. Default-constructedVhdRequirements(or any path that does not call the new setter) getsflags = NONE= old behavior.The WinRT projection has no test infrastructure in the repo (the existing
Name/SizeInBytes/Typeprojection has never had tests either). Behavior is fully covered at the C SDK layer that the projection delegates to.Tests
WSLCTests.cppNamedVolumeVhdOptionsParseTest— SizeBytes presence/range, unknown-key rejection (Bogus=...), sign rejectionNamedVolumesVhdOwnership—mkfs -E root_ownerend-to-end (stat -c "%u %g"on mount root)NamedVolumesVhdFixed— asserts on-diskfile_size >= requestedWslcSdkTests.cppE_INVALIDARGFixedpositive (size verification on disk)OWNERpositive (uid=65534, gid=65534)E_INVALIDARGflags=NONEwith non-zero uid/gid silently ignoredValidation status
cmake --buildclean (wslservice,wslcsession,wslcsdk,wslcsdkwinrt,wsltests)FormatSource.ps1 -ModifiedOnly $true— clang-format reports no changes