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.