Add per-container resource limits and upstream sync workflow (#40384)

Kevin Vega committed May 7, 2026 at 09:17 UTC fa1d8e617f6d57a3577b5ad4e51ad1c5301d4694
7 files changed +203 -2
src/windows/common/WSLCContainerLauncher.cpp
+28
@@ -174,6 +174,29 @@ void WSLCContainerLauncher::SetDnsOptions(std::vector<std::string>&& DnsOptions)
174 m_dnsOptions = std::move(DnsOptions);
175 }
176
177 +void WSLCContainerLauncher::SetMemoryLimit(std::int64_t Bytes)
178 +{
179 + m_memoryBytes = Bytes;
180 +}
181 +
182 +void WSLCContainerLauncher::SetNanoCpus(std::int64_t NanoCpus)
183 +{
184 + m_nanoCpus = NanoCpus;
185 +}
186 +
187 +void WSLCContainerLauncher::AddUlimit(const std::string& Name, std::int64_t Soft, std::int64_t Hard)
188 +{
189 + // Store a copy of the name string to keep the WSLCUlimit pointer valid.
190 + const auto& name = m_ulimitNames.emplace_back(Name);
191 +
192 + WSLCUlimit ulimit{};
193 + ulimit.Name = name.c_str();
194 + ulimit.Soft = Soft;
195 + ulimit.Hard = Hard;
196 +
197 + m_ulimits.push_back(ulimit);
198 +}
199 +
200 void wsl::windows::common::WSLCContainerLauncher::AddVolume(const std::wstring& HostPath, const std::string& ContainerPath, bool ReadOnly)
201 {
202 // Store a copy of the path strings to the launcher to ensure the pointers in WSLCVolume remain valid.
@@ -332,6 +355,11 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
355 options.TmpfsCount = static_cast<ULONG>(m_tmpfsMounts.size());
356 options.Tmpfs = m_tmpfsMounts.size() > 0 ? m_tmpfsMounts.data() : nullptr;
357
358 + options.MemoryBytes = m_memoryBytes;
359 + options.NanoCpus = m_nanoCpus;
360 + options.UlimitsCount = static_cast<ULONG>(m_ulimits.size());
361 + options.Ulimits = m_ulimits.size() > 0 ? m_ulimits.data() : nullptr;
362 +
363 // TODO: Support volumes, ports, flags, container networking mode, etc.
364 wil::com_ptr<IWSLCContainer> container;
365 auto result = Session.CreateContainer(&options, &container);
src/windows/common/WSLCContainerLauncher.h
+7
@@ -80,6 +80,9 @@ public:
80 void SetDnsServers(std::vector<std::string>&& DnsServers);
81 void SetDnsSearchDomains(std::vector<std::string>&& DnsSearchDomains);
82 void SetDnsOptions(std::vector<std::string>&& DnsOptions);
83 + void SetMemoryLimit(std::int64_t Bytes);
84 + void SetNanoCpus(std::int64_t NanoCpus);
85 + void AddUlimit(const std::string& Name, std::int64_t Soft, std::int64_t Hard);
86
87 using WSLCProcessLauncher::FormatResult;
88 using WSLCProcessLauncher::SetUser;
@@ -111,5 +114,9 @@ private:
114 std::vector<WSLCTmpfsMount> m_tmpfsMounts;
115 std::deque<std::string> m_tmpfsContainerPaths;
116 std::deque<std::string> m_tmpfsOptions;
117 + std::int64_t m_memoryBytes = 0;
118 + std::int64_t m_nanoCpus = 0;
119 + std::vector<WSLCUlimit> m_ulimits;
120 + std::deque<std::string> m_ulimitNames;
121 };
122 } // namespace wsl::windows::common
src/windows/inc/docker_schema.h
+15 -1
@@ -203,6 +203,15 @@ struct PortMapping
203 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(PortMapping, HostIp, HostPort);
204 };
205
206 +struct Ulimit
207 +{
208 + std::string Name;
209 + std::int64_t Soft{};
210 + std::int64_t Hard{};
211 +
212 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Ulimit, Name, Soft, Hard);
213 +};
214 +
215 struct HostConfig
216 {
217 std::vector<Mount> Mounts;
@@ -217,8 +226,13 @@ struct HostConfig
226 std::optional<ULONGLONG> ShmSize;
227 std::optional<std::vector<DeviceMapping>> Devices;
228
229 + // Per-container resource limits. 0 means "no limit" (Docker default).
230 + std::int64_t Memory{};
231 + std::int64_t NanoCpus{};
232 + std::optional<std::vector<Ulimit>> Ulimits;
233 +
234 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
221 - HostConfig, Mounts, PortBindings, NetworkMode, Init, Dns, DnsSearch, DnsOptions, Binds, Tmpfs, Devices, ShmSize);
235 + HostConfig, Mounts, PortBindings, NetworkMode, Init, Dns, DnsSearch, DnsOptions, Binds, Tmpfs, Devices, ShmSize, Memory, NanoCpus, Ulimits);
236 };
237
238 struct CreateContainer
src/windows/inc/wslc_schema.h
+13 -1
@@ -49,11 +49,23 @@ struct InspectState
49 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectState, Status, Running, ExitCode, StartedAt, FinishedAt);
50 };
51
52 +struct Ulimit
53 +{
54 + std::string Name;
55 + std::int64_t Soft{};
56 + std::int64_t Hard{};
57 +
58 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Ulimit, Name, Soft, Hard);
59 +};
60 +
61 struct InspectHostConfig
62 {
63 std::string NetworkMode;
64 + std::int64_t Memory{};
65 + std::int64_t NanoCpus{};
66 + std::vector<Ulimit> Ulimits;
67
56 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectHostConfig, NetworkMode);
68 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectHostConfig, NetworkMode, Memory, NanoCpus, Ulimits);
69 };
70
71 struct InspectContainerConfig
src/windows/service/inc/wslc.idl
+12
@@ -226,6 +226,13 @@ typedef struct _WSLCTmpfsMount
226 [unique] LPCSTR Options;
227 } WSLCTmpfsMount;
228
229 +typedef struct _WSLCUlimit
230 +{
231 + [string] LPCSTR Name;
232 + LONGLONG Soft;
233 + LONGLONG Hard;
234 +} WSLCUlimit;
235 +
236 typedef enum _WSLCContainerNetworkType
237 {
238 WSLCContainerNetworkTypeNone = 0,
@@ -293,6 +300,11 @@ typedef struct _WSLCContainerOptions
300
301 [unique, size_is(NamedVolumesCount)] WSLCNamedVolume* NamedVolumes;
302 ULONG NamedVolumesCount;
303 +
304 + LONGLONG MemoryBytes;
305 + LONGLONG NanoCpus;
306 + [unique, size_is(UlimitsCount)] const WSLCUlimit* Ulimits;
307 + ULONG UlimitsCount;
308 } WSLCContainerOptions;
309
310 typedef enum _WSLCContainerState
src/windows/wslcsession/WSLCContainer.cpp
+32
@@ -1138,6 +1138,17 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1138 wslcInspect.State.FinishedAt = dockerInspect.State.FinishedAt;
1139
1140 wslcInspect.HostConfig.NetworkMode = dockerInspect.HostConfig.NetworkMode;
1141 + wslcInspect.HostConfig.Memory = dockerInspect.HostConfig.Memory;
1142 + wslcInspect.HostConfig.NanoCpus = dockerInspect.HostConfig.NanoCpus;
1143 +
1144 + if (dockerInspect.HostConfig.Ulimits.has_value())
1145 + {
1146 + wslcInspect.HostConfig.Ulimits.reserve(dockerInspect.HostConfig.Ulimits->size());
1147 + for (const auto& ulimit : dockerInspect.HostConfig.Ulimits.value())
1148 + {
1149 + wslcInspect.HostConfig.Ulimits.push_back({ulimit.Name, ulimit.Soft, ulimit.Hard});
1150 + }
1151 + }
1152
1153 wslcInspect.Config.Env = dockerInspect.Config.Env;
1154 wslcInspect.Config.Cmd = dockerInspect.Config.Cmd;
@@ -1300,6 +1311,27 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1311
1312 request.HostConfig.Init = WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsInit);
1313
1314 + request.HostConfig.Memory = containerOptions.MemoryBytes;
1315 + request.HostConfig.NanoCpus = containerOptions.NanoCpus;
1316 +
1317 + if (containerOptions.UlimitsCount > 0)
1318 + {
1319 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Ulimits, "Ulimits is null with UlimitsCount=%lu", containerOptions.UlimitsCount);
1320 +
1321 + std::vector<wsl::windows::common::docker_schema::Ulimit> ulimits;
1322 + ulimits.reserve(containerOptions.UlimitsCount);
1323 +
1324 + for (ULONG i = 0; i < containerOptions.UlimitsCount; i++)
1325 + {
1326 + const auto& ulimit = containerOptions.Ulimits[i];
1327 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, ulimit.Name, "Ulimits[%lu].Name is null", i);
1328 +
1329 + ulimits.push_back({ulimit.Name, ulimit.Soft, ulimit.Hard});
1330 + }
1331 +
1332 + request.HostConfig.Ulimits = std::move(ulimits);
1333 + }
1334 +
1335 if (containerOptions.ShmSize > 0)
1336 {
1337 request.HostConfig.ShmSize = containerOptions.ShmSize;
test/windows/WSLCTests.cpp
+96
@@ -7454,6 +7454,102 @@ class WSLCTests
7454 }
7455 }
7456
7457 + WSLC_TEST_METHOD(ContainerResourceLimits)
7458 + {
7459 + // Validate per-container memory limit is applied (cgroup v2: /sys/fs/cgroup/memory.max).
7460 + {
7461 + constexpr std::int64_t memoryBytes = 64 * 1024 * 1024; // 64 MiB
7462 + WSLCContainerLauncher launcher("debian:latest", "test-container-memory-limit", {"cat", "/sys/fs/cgroup/memory.max"});
7463 + launcher.SetMemoryLimit(memoryBytes);
7464 +
7465 + ValidateContainerOutput(launcher, {{1, std::format("{}\n", memoryBytes)}});
7466 + }
7467 +
7468 + // Validate per-container CPU quota is applied (cgroup v2: /sys/fs/cgroup/cpu.max).
7469 + // NanoCpus = 1.5 * 1e9 -> quota=150000 period=100000.
7470 + {
7471 + constexpr std::int64_t nanoCpus = 1'500'000'000ll;
7472 + WSLCContainerLauncher launcher("debian:latest", "test-container-cpu-limit", {"cat", "/sys/fs/cgroup/cpu.max"});
7473 + launcher.SetNanoCpus(nanoCpus);
7474 +
7475 + ValidateContainerOutput(launcher, {{1, "150000 100000\n"}});
7476 + }
7477 +
7478 + // Validate ulimit (nofile) is applied to the init process.
7479 + {
7480 + WSLCContainerLauncher launcher("debian:latest", "test-container-ulimit", {"sh", "-c", "ulimit -Sn; ulimit -Hn"});
7481 + launcher.AddUlimit("nofile", 1234, 5678);
7482 +
7483 + ValidateContainerOutput(launcher, {{1, "1234\n5678\n"}});
7484 + }
7485 +
7486 + // Validate that the configured limits are reported back via container.Inspect().
7487 + {
7488 + constexpr std::int64_t memoryBytes = 64 * 1024 * 1024;
7489 + constexpr std::int64_t nanoCpus = 500'000'000ll;
7490 +
7491 + WSLCContainerLauncher launcher("debian:latest", "test-container-limits-inspect", {"true"});
7492 + launcher.SetMemoryLimit(memoryBytes);
7493 + launcher.SetNanoCpus(nanoCpus);
7494 + launcher.AddUlimit("nofile", 1234, 5678);
7495 +
7496 + auto container = launcher.Create(*m_defaultSession);
7497 + auto hostConfig = container.Inspect().HostConfig;
7498 +
7499 + VERIFY_ARE_EQUAL(memoryBytes, hostConfig.Memory);
7500 + VERIFY_ARE_EQUAL(nanoCpus, hostConfig.NanoCpus);
7501 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), hostConfig.Ulimits.size());
7502 + VERIFY_ARE_EQUAL(std::string("nofile"), hostConfig.Ulimits[0].Name);
7503 + VERIFY_ARE_EQUAL(1234ll, hostConfig.Ulimits[0].Soft);
7504 + VERIFY_ARE_EQUAL(5678ll, hostConfig.Ulimits[0].Hard);
7505 + }
7506 +
7507 + // Validate inspect defaults when no limits are configured: Memory/NanoCpus are 0 ("no limit") and Ulimits is empty.
7508 + {
7509 + WSLCContainerLauncher launcher("debian:latest", "test-container-limits-inspect-defaults", {"true"});
7510 +
7511 + auto container = launcher.Create(*m_defaultSession);
7512 + auto hostConfig = container.Inspect().HostConfig;
7513 +
7514 + VERIFY_ARE_EQUAL(0ll, hostConfig.Memory);
7515 + VERIFY_ARE_EQUAL(0ll, hostConfig.NanoCpus);
7516 + VERIFY_IS_TRUE(hostConfig.Ulimits.empty());
7517 + }
7518 +
7519 + // Validate that multiple ulimits round-trip through inspect in the order they were configured.
7520 + {
7521 + WSLCContainerLauncher launcher("debian:latest", "test-container-limits-inspect-multi", {"true"});
7522 + launcher.AddUlimit("nofile", 1234, 5678);
7523 + launcher.AddUlimit("nproc", 100, 200);
7524 +
7525 + auto container = launcher.Create(*m_defaultSession);
7526 + auto ulimits = container.Inspect().HostConfig.Ulimits;
7527 +
7528 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), ulimits.size());
7529 + VERIFY_ARE_EQUAL(std::string("nofile"), ulimits[0].Name);
7530 + VERIFY_ARE_EQUAL(1234ll, ulimits[0].Soft);
7531 + VERIFY_ARE_EQUAL(5678ll, ulimits[0].Hard);
7532 + VERIFY_ARE_EQUAL(std::string("nproc"), ulimits[1].Name);
7533 + VERIFY_ARE_EQUAL(100ll, ulimits[1].Soft);
7534 + VERIFY_ARE_EQUAL(200ll, ulimits[1].Hard);
7535 + }
7536 +
7537 + // Validate that a Ulimit entry with a null Name is rejected.
7538 + {
7539 + WSLCUlimit ulimit{.Name = nullptr, .Soft = 1, .Hard = 1};
7540 +
7541 + WSLCContainerOptions options{};
7542 + options.Image = "debian:latest";
7543 + options.Name = "test-ulimit-null-name";
7544 + options.Ulimits = &ulimit;
7545 + options.UlimitsCount = 1;
7546 +
7547 + wil::com_ptr<IWSLCContainer> container;
7548 + auto hr = m_defaultSession->CreateContainer(&options, &container);
7549 + VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
7550 + }
7551 + }
7552 +
7553 WSLC_TEST_METHOD(ContainerAttach)
7554 {
7555 // Validate attach behavior in a non-tty process.