Skip to content

Deferred mounting volumes and mapping ports to Start#14320

Draft
kvega005 wants to merge 15 commits intomicrosoft:feature/wsl-for-appsfrom
kvega005:user/kevinve/map_ports_and_mount_volumes_at_start
Draft

Deferred mounting volumes and mapping ports to Start#14320
kvega005 wants to merge 15 commits intomicrosoft:feature/wsl-for-appsfrom
kvega005:user/kevinve/map_ports_and_mount_volumes_at_start

Conversation

@kvega005
Copy link

Summary of the Pull Request

This PR defers port reservation and volume mounting to after the Docker container is started. Previously these resources were set up during Create(), but they only need to be active while the container is running. This also enables bridge mode support, where Docker assigns the VM port dynamically during container start.

Changes

Deferred resource setup (WSLAContainer)

  • Moved volume mounting and port reservation from Create() to Start()
  • Added OnStopped() to release ports and unmount volumes when the container stops
  • Resources are cleaned up on error (scope_exit guards) and on normal stop/event

Two-phase port binding (WSLAVirtualMachine / localhost.cpp)

  • Added ReserveHostPort() — binds a host port without a relay target
  • MapPort() activates relaying on an already-reserved port
  • Simplified UnmapPort() — removed unused LinuxPort parameter

Bridge mode VM port resolution

  • After StartContainer(), for bridge mode only: inspects the container to discover Docker-assigned VM ports, then activates relays
  • For host mode: VM port == container port, no inspect needed

Port relay:

  • Added support for reserving a host port without specifying a VM port. This allows the host port to be claimed at Start() time before Docker assigns the VM-side port.
  • After StartContainer(), the container is inspected to discover the VM ports Docker assigned (for bridge mode), and then the relay is activated with the now-known VM ports.

PR Checklist

  • Closes: Link to issue #xxx
  • Communication: I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected
  • Tests: Added/updated if needed and all pass
  • Localization: All end user facing strings can be localized
  • Dev docs: Added/updated if needed
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

Validation Steps Performed

@kvega005 kvega005 marked this pull request as ready for review February 28, 2026 00:20
@kvega005 kvega005 requested a review from a team as a code owner February 28, 2026 00:20
Copilot AI review requested due to automatic review settings February 28, 2026 00:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors WSLA container lifecycle so port reservation/relay activation and Windows-folder volume mounting occur at Start() time (and are released on stop), enabling dynamic VM-port assignment in bridge networking mode.

Changes:

  • Deferred WSLA container volume mounting + host port reservation from Create() to Start(), with cleanup on stop/error.
  • Added two-phase port binding: reserve host port first, then activate relay once the VM-side port is known (incl. Docker inspect for bridge mode).
  • Updated schemas/tests to support the new flow (Docker NetworkSettings.Ports, HostConfig.Binds, revised Unmap signature, new regression test).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/windows/WSLATests.cpp Updates Unmap calls and adds a regression test covering deferred port/volume setup at Start/Stop.
src/windows/wslrelay/localhost.cpp Adds “inactive reservation” behavior (LinuxPort==0) and allows later activation by updating LinuxPort.
src/windows/wslasession/WSLAVirtualMachine.h Introduces ReserveHostPort() and simplifies UnmapPort() signature.
src/windows/wslasession/WSLAVirtualMachine.cpp Implements reserve/map/unmap via the relay channel with LinuxPort=0 for reservation/unmap.
src/windows/wslasession/WSLASession.h Updates COM method signature for UnmapVmPort.
src/windows/wslasession/WSLASession.cpp Routes UnmapVmPort to the simplified VM UnmapPort.
src/windows/wslasession/WSLAContainerMetadata.h Adds runtime-only Mounted state tracking for volume mounts.
src/windows/wslasession/WSLAContainer.h Stores network mode and splits COM wrapper disconnection from resource release.
src/windows/wslasession/WSLAContainer.cpp Defers mounts/port reservation to Start, resolves bridge ports post-start via inspect, adds OnStopped cleanup.
src/windows/wslaservice/inc/wslaservice.idl Changes IWSLASession::UnmapVmPort signature.
src/windows/inc/docker_schema.h Adds HostConfig.Binds, InspectContainer.NetworkSettings.Ports, and ContainerInfo.HostConfig.
src/windows/common/WSLAContainerLauncher.h Adds SetName() to enable tests to rename launchers.
src/windows/common/WSLAContainerLauncher.cpp Implements SetName().

@kvega005 kvega005 marked this pull request as draft February 28, 2026 01:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Comment on lines 1067 to +1075
HRESULT WSLASession::UnmapVmPort(int Family, short WindowsPort, short LinuxPort)
try
{
COMServiceExecutionContext context;

auto lock = m_lock.lock_shared();
THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);

m_virtualMachine->UnmapPort(Family, WindowsPort, LinuxPort);
m_virtualMachine->UnmapPort(Family, WindowsPort);
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinuxPort is no longer used in WSLASession::UnmapVmPort, which can trigger an unreferenced-parameter warning under MSVC (and the repo uses warnings-as-errors in some configs). To keep the COM signature intact while avoiding /WX issues, consider adding UNREFERENCED_PARAMETER(LinuxPort); (or omitting the parameter name in the definition).

Copilot uses AI. Check for mistakes.
Comment on lines +588 to +592
if (it->second->LinuxPort == 0 && message->LinuxPort != 0)
{
it->second->LinuxPort = message->LinuxPort;
update = true;
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PortRelay::LinuxPort is now mutated at runtime (to activate a previously reserved port) while the accept thread concurrently reads it via isActive()/LaunchRelay(). Because LinuxPort is a plain integer with no synchronization, this introduces a data race (undefined behavior). Consider either (1) making LinuxPort an std::atomic<uint32_t> (or uint16_t to match the message) and using atomic loads/stores, or (2) stopping/joining the accept thread before updating LinuxPort, then restarting it after the update, so reads/writes are synchronized.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment on lines 481 to +496
try
{
m_dockerClient.StartContainer(m_id);
}
CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str());

// For bridge mode, inspect the container to discover the VM ports Docker assigned.
if (m_networkMode == "bridge")
{
auto dockerInspect = m_dockerClient.InspectContainer(m_id);
ResolveVmPorts(m_mappedPorts, dockerInspect);
}

// Activate port relays now that VM ports are known.
ActivatePortRelays(m_mappedPorts, m_virtualMachine);

Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any step after StartContainer() succeeds (InspectContainer/ResolveVmPorts/ActivatePortRelays) throws, the container will remain running but the scope_exit cleanups will unmount volumes and unreserve host ports, and the object will remain in the Created state. This can leave an orphaned running container with broken volumes/port exposure. Consider adding a scope_exit that stops/kills the container (best-effort) once StartContainer has succeeded, and only releases it after Transition(Running) + successful relay activation.

Copilot uses AI. Check for mistakes.
Comment on lines +4476 to +4485
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 1);

// Start the container — volume should now be mounted.
VERIFY_SUCCEEDED(container->Get().Start(WSLAContainerStartFlagsNone));
VERIFY_ARE_EQUAL(container->State(), WslaContainerStateRunning);
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 0);

// Verify the volume is unmounted after container is stopped.
VERIFY_SUCCEEDED(container->Get().Stop(WSLASignalSIGKILL, 0));
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 1);
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test validates deferred volume mounts by grepping for any mount under '/mnt/', but the session can legitimately have other mounts (or leftover mounts from earlier tests), making this check flaky and not specific to the container volume under test. Prefer asserting on the exact expected mountpoint (e.g., /mnt/wsla//volumes/0) using the existing ExpectMount helper, and/or isolate by resetting/creating a session like other volume tests do.

Suggested change
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 1);
// Start the container — volume should now be mounted.
VERIFY_SUCCEEDED(container->Get().Start(WSLAContainerStartFlagsNone));
VERIFY_ARE_EQUAL(container->State(), WslaContainerStateRunning);
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 0);
// Verify the volume is unmounted after container is stopped.
VERIFY_SUCCEEDED(container->Get().Stop(WSLASignalSIGKILL, 0));
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 1);
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/wsla/deferred-volume/volumes/0$'"}, 1);
// Start the container — volume should now be mounted.
VERIFY_SUCCEEDED(container->Get().Start(WSLAContainerStartFlagsNone));
VERIFY_ARE_EQUAL(container->State(), WslaContainerStateRunning);
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/wsla/deferred-volume/volumes/0$'"}, 0);
// Verify the volume is unmounted after container is stopped.
VERIFY_SUCCEEDED(container->Get().Stop(WSLASignalSIGKILL, 0));
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/wsla/deferred-volume/volumes/0$'"}, 1);

Copilot uses AI. Check for mistakes.
@kvega005 kvega005 marked this pull request as ready for review February 28, 2026 18:23
{
if (e.VmPort != 0)
{
continue; // Host mode port — VM port is already known.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean by "host mode port" here?

{
auto dockerInspect = m_dockerClient.InspectContainer(m_id);
ResolveVmPorts(m_mappedPorts, dockerInspect);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the networking mode is not "bridge", then we should not call ActivatePortRelays. Is the idea that the CLI/API will sanity check that if the port mappings are provided, then the networking mode would have to be 'bridge'? In the standard case, for host network containers, the port mapping is not passed by the user.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The port relays need to activated in host mode as well

{
auto dockerInspect = m_dockerClient.InspectContainer(m_id);
ResolveVmPorts(m_mappedPorts, dockerInspect);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any of these above calls fail, do we want to stop/delete the started container? Otherwise we will have a started container without the VM ports resolved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's actually a good point. I need to rethink this a bit. I think if we wait to start the container then fail to activate the port relays, we would inadvertently have allowed the init process to run for a brief period of time. This means there may be a side effect. We probably do not want to fail the start container call after the init process has ran for some time.


void WSLAVirtualMachine::ReserveHostPort(_In_ int Family, _In_ short WindowsPort)
{
MapPortImpl(Family, WindowsPort, 0, false);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this last bool parameter is very confusing. Ideally we should use a meaningful enum here to indicate that false means "we are NOT unmapping ports" and true means "we are unmapping ports"

At the very least, add comments next to these.

void WSLAVirtualMachine::UnmapPort(_In_ int Family, _In_ short WindowsPort)
{
MapPortImpl(Family, WindowsPort, LinuxPort, true);
MapPortImpl(Family, WindowsPort, 0, true);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: again, a comment saying unmap does not require LinuxPort to be provided could be very helpful for readability

for (auto& e : ports)
{
if (!e->Pending)
if (!e->Pending && e->IsActive())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pending and active checks are not happening atomically, could this cause a race? Similar questions for the port mapping related functionality happening from within this thread

VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning);

// Start container 2 — should fail because the host port is already reserved by container 1.
VERIFY_ARE_EQUAL(container2.Get().Start(WSLAContainerStartFlagsNone), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more meaningful error message around port mapping failure due to conflict would be useful

WSLA_CONTAINER_STATE m_state = WslaContainerStateInvalid;
WSLASession& m_wslaSession;
WSLAVirtualMachine& m_virtualMachine;
std::string m_networkMode;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend storing the networking mode as WSLA_CONTAINER_NETWORK_TYPE instead of a string here


void WSLAVirtualMachine::ReserveHostPort(_In_ int Family, _In_ short WindowsPort)
{
MapPortImpl(Family, WindowsPort, 0, false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to be able to map host ports without actually starting to relay, we should flags to signal the difference between:

  1. I have previously reserved this port, and now I want to start relaying to guest port X
  2. I want to map this host port for the first time

Otherwise this can create a race where if let's say container A is starting and does 1), container B could come and steal the port by doing 2) before container A is done allocating the port fully

{
for (auto& e : ports)
{
if (e.VmPort != 0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be possible right ? ResolveVmPorts() is only called in bridge mode, and in this mode we always set VmPort to 0

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the case, I'd recommend switching to a WI_ASSERT here


bool IsActive() const
{
return LinuxPort != 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinuxPort should be changed to an std::atomic here since we're now reading & writing it from multiple threads.

In truth we should probably rewrite this relay logic, but for now let's just make it an std::atomic

{
auto dockerInspect = m_dockerClient.InspectContainer(m_id);
ResolveVmPorts(m_mappedPorts, dockerInspect);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The port relays need to activated in host mode as well

@kvega005 kvega005 marked this pull request as draft March 3, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants