@samitouri / QOSAMI-WSL / commits / 3701271d

Protect critical WSL processes under heavy load with cgroup & isolate distro cgroups (#40519)

This PR reorganize the wsl cgroup with the follow structure: ``` # When systemd is enabled / # critical wsl processes live in root with no resource limit. --wsl-user # resource limited cgroup -- non-distro # for processes not belonging to a distro, like plugins -- distro-<init pid> # per distro root -- non-systemd # for non systemd user processes -- systemd # for systemd and it's cgroup tree # When systemd is not enabled / # critical wsl processes live in root with no resource limit. --wsl-user # resource limited cgroup -- non-distro # for processes not belonging to a distro, like plugins -- distro-<init pid> # per distro cgroup ``` The wsl-user cgroup has memory.max and cpu.max set so it can only take max - 32MiB of RAM and max - 0.01 CPU cores. Effectively reserving 32MiB RAM and 0.01 cores for processes not under this cgroup.

Feng Wang committed Jul 14, 2026 at 12:10 UTC 3701271ddc7d2b0b8398b72096d07f383898f26d
13 files changed +498 -70
src/linux/init/WslDistributionConfig.h
+1
@@ -96,6 +96,7 @@ struct WslDistributionConfig
96 wil::unique_fd BootStartWriteSocket;
97 wsl::shared::SocketChannel Plan9ControlChannel;
98 std::optional<pid_t> InitPid;
99 + std::optional<std::string> CgroupPath;
100 };
101
102 } // namespace wsl::linux
\ No newline at end of file
src/linux/init/config.cpp
+13 -9
@@ -992,17 +992,21 @@ try
992
993 if (Config.BootCommand.has_value())
994 {
995 - UtilCreateChildProcess("BootCommand", [Command = Config.BootCommand.value(), SavedSignals = g_SavedSignalActions]() {
996 - //
997 - // Restore default signal dispositions for the child process.
998 - //
995 + UtilCreateChildProcess(
996 + "BootCommand",
997 + [Command = Config.BootCommand.value(), SavedSignals = g_SavedSignalActions]() {
998 + //
999 + // Restore default signal dispositions for the child process.
1000 + //
1001
1000 - THROW_LAST_ERROR_IF(UtilSetSignalHandlers(SavedSignals, false) < 0);
1001 - THROW_LAST_ERROR_IF(UtilRestoreBlockedSignals() < 0);
1002 + THROW_LAST_ERROR_IF(UtilSetSignalHandlers(SavedSignals, false) < 0);
1003 + THROW_LAST_ERROR_IF(UtilRestoreBlockedSignals() < 0);
1004
1003 - execl("/bin/sh", "sh", "-c", Command.c_str(), nullptr);
1004 - LOG_ERROR("execl() failed, {}", errno);
1005 - });
1005 + execl("/bin/sh", "sh", "-c", Command.c_str(), nullptr);
1006 + LOG_ERROR("execl() failed, {}", errno);
1007 + },
1008 + {},
1009 + Config.CgroupPath);
1010 }
1011
1012 return 0;
src/linux/init/init.cpp
+34 -5
@@ -1179,14 +1179,17 @@ try
1179 }
1180
1181 SessionLeader = UtilCreateChildProcess(
1182 - "SessionLeader", [SessionLeaderFd = std::move(SessionLeaderFd), TtyFd = std::move(TtyFd), &Channel, &Config]() mutable {
1182 + "SessionLeader",
1183 + [SessionLeaderFd = std::move(SessionLeaderFd), TtyFd = std::move(TtyFd), &Channel, &Config]() mutable {
1184 umask(Config.Umask);
1185 Channel.Close();
1186
1187 THROW_LAST_ERROR_IF(UtilRestoreBlockedSignals() < 0);
1188
1189 SessionLeaderEntry(SessionLeaderFd.get(), TtyFd.get(), Config);
1189 - });
1190 + },
1191 + {},
1192 + Config.CgroupPath);
1193 }
1194 else
1195 {
@@ -1228,7 +1231,8 @@ try
1231 // of other session leaders. See https://github.com/microsoft/WSL/issues/9114.
1232
1233 SessionLeader = UtilCreateChildProcess(
1231 - "SessionLeader", [ListenSocket = std::move(ListenSocket), &Channel, &Config, Mask = Config.Umask, SocketAddress]() {
1234 + "SessionLeader",
1235 + [ListenSocket = std::move(ListenSocket), &Channel, &Config, Mask = Config.Umask, SocketAddress]() {
1236 umask(Mask);
1237 Channel.Close();
1238
@@ -1243,7 +1247,9 @@ try
1247 }
1248
1249 SessionLeaderEntryUtilityVm(channel, Config);
1246 - });
1250 + },
1251 + {},
1252 + Config.CgroupPath);
1253 }
1254
1255 if (SessionLeader < 0)
@@ -2304,6 +2310,24 @@ Return Value:
2310 unsetenv(LX_WSL2_DISTRO_INIT_PID);
2311 }
2312
2313 + //
2314 + // Get the per-distro cgroup path.
2315 + //
2316 +
2317 + const auto DistroCgroupPath = getenv(LX_WSL2_DISTRO_CGROUP_PATH);
2318 + if (DistroCgroupPath != nullptr)
2319 + {
2320 + if (access(DistroCgroupPath, F_OK) == 0)
2321 + {
2322 + Config.CgroupPath = DistroCgroupPath;
2323 + }
2324 + else
2325 + {
2326 + LOG_ERROR("Cgroup path {} does not exist", DistroCgroupPath);
2327 + }
2328 + unsetenv(LX_WSL2_DISTRO_CGROUP_PATH);
2329 + }
2330 +
2331 std::vector<gsl::byte> Buffer;
2332 if (Config.BootInit)
2333 {
@@ -2376,6 +2400,11 @@ Return Value:
2400
2401 CreateWslSystemdUnits(Config);
2402
2403 + if (Config.CgroupPath.has_value())
2404 + {
2405 + UtilTryMoveSelfToDistroCgroup(Config.CgroupPath.value(), true, "systemd");
2406 + }
2407 +
2408 const char* Argv[] = {INIT_PATH, nullptr};
2409 std::vector<const char*> Env;
2410 std::vector<std::string> Environment;
@@ -2520,7 +2549,7 @@ Return Value:
2549 break;
2550
2551 case LxInitCreateProcess:
2523 - ProcessCreateProcessMessage(transaction, Span);
2552 + ProcessCreateProcessMessage(transaction, Span, Config.CgroupPath);
2553 break;
2554
2555 default:
src/linux/init/main.cpp
+226 -17
@@ -102,6 +102,9 @@ Abstract:
102 #define syscall_arch (offsetof(struct seccomp_data, arch))
103
104 constexpr auto c_trueString = "1";
105 +constexpr size_t c_systemReservedMemory = 32 * 1024 * 1024; // 32MiB reserved for WSL system processes
106 +constexpr long c_cpuPeriodMicros = 100000;
107 +constexpr long c_systemReservedCpuMicros = 1000; // 0.01 Logical core reserved for WSL system processes
108
109 struct VmConfiguration
110 {
@@ -159,7 +162,8 @@ void LaunchInit(
162 const char* SharedMemoryRoot = nullptr,
163 const char* InstallPath = nullptr,
164 const char* UserProfile = nullptr,
162 - std::optional<pid_t> DistroInitPid = {});
165 + std::optional<pid_t> DistroInitPid = {},
166 + const char* DistroCgroupPath = nullptr);
167
168 void LaunchSystemDistro(
169 int SocketFd,
@@ -170,7 +174,8 @@ void LaunchSystemDistro(
174 const char* SharedMemoryRoot,
175 const char* InstallPath,
176 const char* UserProfile,
173 - pid_t DistroInitPid);
177 + pid_t DistroInitPid,
178 + const char* DistroCgroupPath);
179
180 std::map<unsigned long, std::string> ListDiskPartitions(const std::string& DeviceName, std::optional<unsigned long> WaitForIndex = {});
181
@@ -212,6 +217,8 @@ void WaitForBlockDevice(const char* Path);
217
218 int WaitForChild(pid_t Pid, const char* Name);
219
220 +void SetupWslUserCgroup();
221 +
222 int Chroot(const char* Target)
223
224 /*++
@@ -1419,7 +1426,8 @@ void LaunchInit(
1426 const char* SharedMemoryRoot,
1427 const char* InstallPath,
1428 const char* UserProfile,
1422 - std::optional<pid_t> DistroInitPid)
1429 + std::optional<pid_t> DistroInitPid,
1430 + const char* DistroCgroupPath)
1431
1432 /*++
1433
@@ -1458,6 +1466,8 @@ Arguments:
1466
1467 DistroInitPid - Supplies the pid of the user distribution's init process.
1468
1469 + DistroCgroupPath - Supplies the cgroup path of this distribution.
1470 +
1471 Return Value:
1472
1473 None. This method does not return.
@@ -1564,6 +1574,7 @@ try
1574 AddEnvironmentVariable(LX_WSL2_INSTALL_PATH, InstallPath);
1575 AddEnvironmentVariable(LX_WSL2_USER_PROFILE, UserProfile);
1576 AddEnvironmentVariable(LX_WSL2_NETWORKING_MODE_ENV, std::to_string(static_cast<int>(Config.NetworkingMode)).c_str());
1577 + AddEnvironmentVariable(LX_WSL2_DISTRO_CGROUP_PATH, DistroCgroupPath);
1578
1579 if (DistroInitPid.has_value())
1580 {
@@ -1665,7 +1676,8 @@ void LaunchSystemDistro(
1676 const char* SharedMemoryRoot,
1677 const char* InstallPath,
1678 const char* UserProfile,
1668 - pid_t DistroInitPid)
1679 + pid_t DistroInitPid,
1680 + const char* DistroCgroupPath)
1681
1682 /*++
1683
@@ -1702,6 +1714,8 @@ Arguments:
1714
1715 DistroInitPid - Supplies the pid of the user distribution's init process.
1716
1717 + DistroCgroupPath - Supplies the cgroup path of this distribution.
1718 +
1719 Return Value:
1720
1721 None. This method does not return.
@@ -1720,7 +1734,7 @@ try
1734 // Launch the init daemon, this method does not return.
1735 //
1736
1723 - LaunchInit(SocketFd, Target, true, Config, VmId, DistributionName, SharedMemoryRoot, InstallPath, UserProfile, DistroInitPid);
1737 + LaunchInit(SocketFd, Target, true, Config, VmId, DistributionName, SharedMemoryRoot, InstallPath, UserProfile, DistroInitPid, DistroCgroupPath);
1738 _exit(1);
1739 }
1740 catch (...)
@@ -2227,6 +2241,52 @@ void ProcessLaunchInitMessage(
2241
2242 THROW_LAST_ERROR_IF(MountDevice(Message->MountDeviceType, Message->DeviceId, DISTRO_PATH, FsType, Message->Flags, MountOptions) < 0);
2243
2244 + auto MiniInitDirectChildPidPath = std::filesystem::read_symlink(PROCFS_PATH "/self");
2245 + pid_t MiniInitDirectChildPid = std::stoul(MiniInitDirectChildPidPath.string());
2246 +
2247 + bool bootInit = false;
2248 + bool enableGuiApps = Config.EnableGuiApps;
2249 + {
2250 + wil::unique_file File{fopen(DISTRO_PATH ETC_PATH "/wsl.conf", "r")};
2251 + if (File)
2252 + {
2253 + std::vector<ConfigKey> ConfigKeys = {ConfigKey("boot.systemd", bootInit), ConfigKey("general.guiApplications", enableGuiApps)};
2254 + ParseConfigFile(ConfigKeys, File.get(), CFG_SKIP_UNKNOWN_VALUES, STRING_TO_WSTRING(CONFIG_FILE));
2255 + }
2256 + }
2257 +
2258 + //
2259 + // Set up the per-distro cgroup before potentially forking into two inits.
2260 + //
2261 +
2262 + std::string DistroCgroupPath{};
2263 + if (access(WSL_USER_CGROUP_PATH, F_OK) == 0)
2264 + {
2265 + DistroCgroupPath = UtilGetDistroCgroupPath(MiniInitDirectChildPid);
2266 +
2267 + auto cleanup = wil::scope_exit([&]() {
2268 + rmdir((DistroCgroupPath + WSL_USER_NON_SYSTEMD_CGROUP_DIR).c_str());
2269 + rmdir((DistroCgroupPath + WSL_USER_SYSTEMD_CGROUP_DIR).c_str());
2270 + rmdir(DistroCgroupPath.c_str());
2271 + DistroCgroupPath.clear();
2272 + });
2273 +
2274 + try
2275 + {
2276 + THROW_LAST_ERROR_IF(UtilMkdir(DistroCgroupPath.c_str(), 0755) < 0);
2277 +
2278 + if (bootInit)
2279 + {
2280 + THROW_LAST_ERROR_IF(UtilEnableAllCgroupControllers(DistroCgroupPath) < 0);
2281 + THROW_LAST_ERROR_IF(UtilMkdir((DistroCgroupPath + WSL_USER_SYSTEMD_CGROUP_DIR).c_str(), 0755) < 0);
2282 + THROW_LAST_ERROR_IF(UtilMkdir((DistroCgroupPath + WSL_USER_NON_SYSTEMD_CGROUP_DIR).c_str(), 0755) < 0);
2283 + }
2284 +
2285 + cleanup.release();
2286 + }
2287 + CATCH_LOG();
2288 + }
2289 +
2290 //
2291 // Allow /etc/wsl.conf in the user distro to opt-out of GUI support.
2292 //
@@ -2234,17 +2294,9 @@ void ProcessLaunchInitMessage(
2294 // of GUI app support because WslService is waiting to accept a connection.
2295 //
2296
2237 - bool enableGuiApps = Config.EnableGuiApps;
2297 if (Message->Flags & LxMiniInitMessageFlagLaunchSystemDistro && Config.EnableGuiApps)
2298 {
2299 Step = LxInitCreateInstanceStepLaunchSystemDistro;
2241 - wil::unique_file File{fopen(DISTRO_PATH ETC_PATH "/wsl.conf", "r")};
2242 - if (File)
2243 - {
2244 - std::vector<ConfigKey> ConfigKeys = {ConfigKey("general.guiApplications", enableGuiApps)};
2245 - ParseConfigFile(ConfigKeys, File.get(), CFG_SKIP_UNKNOWN_VALUES, STRING_TO_WSTRING(CONFIG_FILE));
2246 - File.reset();
2247 - }
2300
2301 //
2302 // If the distro did not opt-out of GUI applications, continue launching the system distro.
@@ -2301,7 +2353,8 @@ void ProcessLaunchInitMessage(
2353 wsl::shared::string::FromSpan(Buffer, Message->SharedMemoryRootOffset),
2354 wsl::shared::string::FromSpan(Buffer, Message->InstallPathOffset),
2355 wsl::shared::string::FromSpan(Buffer, Message->UserProfileOffset),
2304 - ChildPid);
2356 + ChildPid,
2357 + DistroCgroupPath.empty() ? nullptr : DistroCgroupPath.c_str());
2358 }
2359 }
2360
@@ -2322,7 +2375,9 @@ void ProcessLaunchInitMessage(
2375 wsl::shared::string::FromSpan(Buffer, Message->DistributionNameOffset),
2376 nullptr,
2377 wsl::shared::string::FromSpan(Buffer, Message->InstallPathOffset),
2325 - wsl::shared::string::FromSpan(Buffer, Message->UserProfileOffset));
2378 + wsl::shared::string::FromSpan(Buffer, Message->UserProfileOffset),
2379 + std::nullopt,
2380 + DistroCgroupPath.empty() ? nullptr : DistroCgroupPath.c_str());
2381 }
2382 catch (...)
2383 {
@@ -3007,6 +3062,11 @@ try
3062 Config.EnableSafeMode = true;
3063 }
3064
3065 + if (EarlyConfig->IsolateDistroCgroup && access(CGROUP_MOUNTPOINT "/cgroup.controllers", F_OK) == 0)
3066 + {
3067 + SetupWslUserCgroup();
3068 + }
3069 +
3070 //
3071 // Establish the connection for the guest network service.
3072 //
@@ -3227,7 +3287,14 @@ try
3287 return ProcessMountFolderMessage(Transaction, Buffer);
3288
3289 case LxInitCreateProcess:
3230 - return ProcessCreateProcessMessage(Transaction, Buffer);
3290 + if (access(WSL_USER_NON_DISTRO_CGROUP_PATH, F_OK) == 0)
3291 + {
3292 + return ProcessCreateProcessMessage(Transaction, Buffer, WSL_USER_NON_DISTRO_CGROUP_PATH);
3293 + }
3294 + else
3295 + {
3296 + return ProcessCreateProcessMessage(Transaction, Buffer, std::nullopt);
3297 + }
3298
3299 case LxMiniInitMessageWaitForPmemDevice:
3300 {
@@ -3657,6 +3724,102 @@ void EnableDebugMode(const std::string& Mode)
3724 }
3725 }
3726
3727 +void SetupWslUserCgroup()
3728 +
3729 +/*++
3730 +
3731 +Routine Description:
3732 +
3733 + This routine creates a memory-limited cgroup for user processes. All user workloads
3734 + (systemd, session leaders, boot commands) are placed into this cgroup so that they
3735 + cannot exhaust all VM memory. This reserves a fixed amount of memory for critical
3736 + WSL system processes (mini_init, GNS, Plan9, WSL init) that remain in the root cgroup.
3737 +
3738 + The memory.max limit is set to totalram - c_systemReservedMemory, which provides a
3739 + hard cap. When this limit is reached, the cgroup-local OOM killer activates and only
3740 + kills processes within wsl-user, leaving system processes unaffected.
3741 +
3742 + The cpu.max limit is set to (nproc * c_cpuPeriodMicros - c_systemReservedCpuMicros) per
3743 + c_cpuPeriodMicros period, reserving a small portion of the CPU for WSL system processes so they remain
3744 + schedulable even when user workloads saturate every CPU.
3745 +
3746 +Arguments:
3747 +
3748 + None.
3749 +
3750 +Return Value:
3751 +
3752 + None.
3753 +
3754 +--*/
3755 +
3756 +{
3757 + struct sysinfo info = {};
3758 + if (sysinfo(&info) < 0)
3759 + {
3760 + LOG_ERROR("sysinfo failed {}", errno);
3761 + return;
3762 + }
3763 +
3764 + uint64_t totalRam = static_cast<uint64_t>(info.totalram) * info.mem_unit;
3765 +
3766 + if (totalRam <= c_systemReservedMemory)
3767 + {
3768 + LOG_WARNING("Total RAM ({}) is too small to reserve {} for system processes", totalRam, c_systemReservedMemory);
3769 + return;
3770 + }
3771 +
3772 + if (UtilEnableAllCgroupControllers(CGROUP_MOUNTPOINT) < 0)
3773 + {
3774 + LOG_ERROR("Failed to enable cgroup controllers for root {}", errno);
3775 + return;
3776 + }
3777 +
3778 + if (UtilMkdir(WSL_USER_CGROUP_PATH, 0755) < 0)
3779 + {
3780 + LOG_ERROR("Failed to create wsl-user cgroup directory {}", errno);
3781 + return;
3782 + }
3783 +
3784 + if (UtilEnableAllCgroupControllers(WSL_USER_CGROUP_PATH) < 0)
3785 + {
3786 + LOG_ERROR("Failed to enable cgroup controllers for wsl-user {}", errno);
3787 + return;
3788 + }
3789 +
3790 + if (UtilMkdir(WSL_USER_NON_DISTRO_CGROUP_PATH, 0755) < 0)
3791 + {
3792 + LOG_ERROR("Failed to create wsl-user non-distro cgroup directory {}", errno);
3793 + return;
3794 + }
3795 +
3796 + auto userMemoryMax = std::to_string(totalRam - c_systemReservedMemory);
3797 + if (WriteToFile(WSL_USER_CGROUP_PATH "/memory.max", userMemoryMax.c_str()) < 0)
3798 + {
3799 + LOG_ERROR("Failed to set memory.max for wsl-user cgroup {}", errno);
3800 + return;
3801 + }
3802 +
3803 + LOG_INFO("WSL user cgroup created with memory.max={} (totalram={}, reserved={})", userMemoryMax, totalRam, c_systemReservedMemory);
3804 +
3805 + const long nproc = get_nprocs();
3806 + if (nproc <= 0)
3807 + {
3808 + LOG_WARNING("get_nprocs returned {}, skipping cpu.max", nproc);
3809 + return;
3810 + }
3811 +
3812 + const long cpuQuota = (nproc * c_cpuPeriodMicros) - c_systemReservedCpuMicros;
3813 + auto userCpuMax = std::format("{} {}", cpuQuota, c_cpuPeriodMicros);
3814 + if (WriteToFile(WSL_USER_CGROUP_PATH "/cpu.max", userCpuMax.c_str()) < 0)
3815 + {
3816 + LOG_ERROR("Failed to set cpu.max for wsl-user cgroup {}", errno);
3817 + return;
3818 + }
3819 +
3820 + LOG_INFO("WSL user cgroup cpu.max={} (nproc={}, reserved={}us)", userCpuMax, nproc, c_systemReservedCpuMicros);
3821 +}
3822 +
3823 int main(int Argc, char* Argv[])
3824 {
3825 std::vector<gsl::byte> Buffer;
@@ -3864,7 +4027,12 @@ int main(int Argc, char* Argv[])
4027 }
4028 }
4029
3867 - UtilMount(nullptr, CGROUP_MOUNTPOINT, CGROUP2_DEVICE, 0, nullptr);
4030 + if (UtilMount(nullptr, CGROUP_MOUNTPOINT, CGROUP2_DEVICE, 0, nullptr) < 0)
4031 + {
4032 + Result = -1;
4033 + LOG_ERROR("Failed to mount cgroup2: {}", errno);
4034 + goto ErrorExit;
4035 + }
4036
4037 UtilSetThreadName("mini_init");
4038
@@ -3971,6 +4139,47 @@ int main(int Argc, char* Argv[])
4139
4140 sync();
4141
4142 + //
4143 + // Clear the distro cgroup
4144 + //
4145 +
4146 + auto CgroupDir = UtilGetDistroCgroupPath(Result);
4147 + if (access(CgroupDir.c_str(), F_OK) == 0)
4148 + {
4149 + LOG_INFO("Process {} exited, removing cgroup {}", Result, CgroupDir);
4150 +
4151 + //
4152 + // Recursively rmdir the cgroup subtree.
4153 + //
4154 +
4155 + try
4156 + {
4157 + std::vector<std::string> dirs;
4158 + for (const auto& entry : std::filesystem::recursive_directory_iterator(
4159 + CgroupDir, std::filesystem::directory_options::skip_permission_denied))
4160 + {
4161 + if (entry.is_directory())
4162 + {
4163 + dirs.emplace_back(entry.path().string());
4164 + }
4165 + }
4166 +
4167 + for (auto it = dirs.rbegin(); it != dirs.rend(); ++it)
4168 + {
4169 + if (rmdir(it->c_str()) < 0 && errno != ENOENT)
4170 + {
4171 + LOG_ERROR("rmdir({}) failed {}", *it, errno);
4172 + }
4173 + }
4174 +
4175 + if (rmdir(CgroupDir.c_str()) < 0 && errno != ENOENT)
4176 + {
4177 + LOG_ERROR("rmdir({}) failed {}", CgroupDir, errno);
4178 + }
4179 + }
4180 + CATCH_LOG();
4181 + }
4182 +
4183 //
4184 // Send a message with the child's pid to the service.
4185 //
src/linux/init/util.cpp
+78 -21
@@ -3419,7 +3419,7 @@ Return Value:
3419 return 0;
3420 }
3421
3422 -int ProcessCreateProcessMessage(wsl::shared::Transaction& Transaction, gsl::span<gsl::byte> Buffer)
3422 +int ProcessCreateProcessMessage(wsl::shared::Transaction& Transaction, gsl::span<gsl::byte> Buffer, const std::optional<std::string>& DistroCgroupPath)
3423 {
3424 auto* Message = gslhelpers::try_get_struct<CREATE_PROCESS_MESSAGE>(Buffer);
3425 if (!Message)
@@ -3454,30 +3454,34 @@ int ProcessCreateProcessMessage(wsl::shared::Transaction& Transaction, gsl::span
3454
3455 auto ControlPipe = wil::unique_pipe::create(O_CLOEXEC);
3456
3457 - const int ChildPid = UtilCreateChildProcess("CreateChildProcess", [&]() {
3458 - try
3459 - {
3460 - wil::unique_fd ProcessSocket{UtilAcceptVsock(ListenSocket.get(), SocketAddress, SESSION_LEADER_ACCEPT_TIMEOUT_MS)};
3461 - THROW_LAST_ERROR_IF(!ProcessSocket);
3462 -
3463 - THROW_LAST_ERROR_IF(dup2(ProcessSocket.get(), STDIN_FILENO) < 0);
3464 - THROW_LAST_ERROR_IF(dup2(ProcessSocket.get(), STDOUT_FILENO) < 0);
3465 - execv(Path, (char* const*)(ArgumentArray.data()));
3457 + const int ChildPid = UtilCreateChildProcess(
3458 + "CreateChildProcess",
3459 + [&]() {
3460 + try
3461 + {
3462 + wil::unique_fd ProcessSocket{UtilAcceptVsock(ListenSocket.get(), SocketAddress, SESSION_LEADER_ACCEPT_TIMEOUT_MS)};
3463 + THROW_LAST_ERROR_IF(!ProcessSocket);
3464
3467 - // If this point is reached, an error needs to be reported back since execv() failed.
3468 - THROW_LAST_ERROR();
3469 - }
3470 - catch (...)
3471 - {
3472 - auto error = wil::ResultFromCaughtException();
3473 - LOG_ERROR("Command execution failed: {}", errno);
3465 + THROW_LAST_ERROR_IF(dup2(ProcessSocket.get(), STDIN_FILENO) < 0);
3466 + THROW_LAST_ERROR_IF(dup2(ProcessSocket.get(), STDOUT_FILENO) < 0);
3467 + execv(Path, (char* const*)(ArgumentArray.data()));
3468
3475 - if (write(ControlPipe.write().get(), &error, sizeof(error)) != sizeof(error))
3469 + // If this point is reached, an error needs to be reported back since execv() failed.
3470 + THROW_LAST_ERROR();
3471 + }
3472 + catch (...)
3473 {
3477 - LOG_ERROR("Failed to write command execution status: {}", errno);
3474 + auto error = wil::ResultFromCaughtException();
3475 + LOG_ERROR("Command execution failed: {}", errno);
3476 +
3477 + if (write(ControlPipe.write().get(), &error, sizeof(error)) != sizeof(error))
3478 + {
3479 + LOG_ERROR("Failed to write command execution status: {}", errno);
3480 + }
3481 }
3479 - }
3480 - });
3482 + },
3483 + {},
3484 + DistroCgroupPath);
3485
3486 THROW_LAST_ERROR_IF(ChildPid < 0);
3487 ControlPipe.write().reset();
@@ -3739,3 +3743,56 @@ try
3743 }).detach();
3744 }
3745 CATCH_LOG()
3746 +
3747 +std::string UtilGetDistroCgroupPath(pid_t DistroInitPid)
3748 +{
3749 + return std::format("{}/distro-{}", WSL_USER_CGROUP_PATH, DistroInitPid);
3750 +}
3751 +
3752 +int UtilEnableAllCgroupControllers(const std::string& CgroupPath)
3753 +{
3754 + // Only cpu and memory are required for wsl's resource limit.
3755 + if (WriteToFile((CgroupPath + "/cgroup.subtree_control").c_str(), "+cpu +memory") < 0)
3756 + {
3757 + LOG_ERROR("Failed to enable cgroup controllers for {}: {}", CgroupPath, errno);
3758 + return -1;
3759 + }
3760 + const char* const OptionalControllers[] = {"+pids", "+io", "+cpuset", "+hugetlb", "+rdma", "+misc"};
3761 + for (const auto Controller : OptionalControllers)
3762 + {
3763 + if (WriteToFile((CgroupPath + "/cgroup.subtree_control").c_str(), Controller) < 0)
3764 + {
3765 + LOG_WARNING("Failed to enable optional cgroup controller {} for {}: {}", Controller, CgroupPath, errno);
3766 + }
3767 + }
3768 + return 0;
3769 +}
3770 +
3771 +void UtilTryMoveSelfToDistroCgroup(const std::string& CgroupPath, bool IsSystemd, const std::string& LogSubject)
3772 +try
3773 +{
3774 + std::string ProcsFile{};
3775 + if (IsSystemd)
3776 + {
3777 + ProcsFile = CgroupPath + WSL_USER_SYSTEMD_CGROUP_DIR + "/cgroup.procs";
3778 + }
3779 + else
3780 + {
3781 + auto NonSystemdCgroupPath = CgroupPath + WSL_USER_NON_SYSTEMD_CGROUP_DIR;
3782 + auto NonSystemdCgroupExists = access(NonSystemdCgroupPath.c_str(), F_OK) == 0;
3783 + if (NonSystemdCgroupExists)
3784 + {
3785 + ProcsFile = CgroupPath + WSL_USER_NON_SYSTEMD_CGROUP_DIR "/cgroup.procs";
3786 + }
3787 + else
3788 + {
3789 + ProcsFile = CgroupPath + "/cgroup.procs";
3790 + }
3791 + }
3792 +
3793 + if (WriteToFile(ProcsFile.c_str(), "0") < 0)
3794 + {
3795 + LOG_WARNING("Failed to move process to cgroup {} for {}: {}", CgroupPath, LogSubject, errno);
3796 + }
3797 +}
3798 +CATCH_LOG();
src/linux/init/util.h
+19 -2
@@ -44,6 +44,10 @@ struct WslDistributionConfig;
44
45 #define CGROUP_MOUNTPOINT "/sys/fs/cgroup"
46 #define CGROUP2_DEVICE "cgroup2"
47 +#define WSL_USER_CGROUP_PATH CGROUP_MOUNTPOINT "/wsl-user"
48 +#define WSL_USER_SYSTEMD_CGROUP_DIR "/systemd"
49 +#define WSL_USER_NON_SYSTEMD_CGROUP_DIR "/non-systemd"
50 +#define WSL_USER_NON_DISTRO_CGROUP_PATH WSL_USER_CGROUP_PATH "/non-distro"
51 #define MOUNT_COMMAND "/bin/mount"
52 #define MOUNT_FSTAB_ARG "-a"
53 #define MOUNT_INTERNAL_ONLY_ARG "-i"
@@ -136,8 +140,10 @@ wil::unique_fd UtilConnectVsock(
140 // Needs to be declared before UtilCreateChildProcess().
141 void UtilSetThreadName(const char* Name);
142
143 +void UtilTryMoveSelfToDistroCgroup(const std::string& CgroupPath, bool IsSystemd, const std::string& LogSubject);
144 +
145 template <typename TMethod>
140 -int UtilCreateChildProcess(const char* ChildName, TMethod&& ChildFunction, std::optional<int> CloneFlags = {})
146 +int UtilCreateChildProcess(const char* ChildName, TMethod&& ChildFunction, std::optional<int> CloneFlags = {}, std::optional<std::string> CgroupPath = {})
147
148 /*++
149
@@ -154,6 +160,8 @@ Arguments:
160 CloneFlags - Supplies an optional value containing flags to use for the clone syscall.
161 If no flags are specified, fork is used instead.
162
163 + CgroupPath - Supplies an optional value containing the path of the cgroup to try move the child process into.
164 +
165 Return Value:
166
167 The pid of the child process on success, -1 on failure. The child process does not return.
@@ -182,6 +190,11 @@ Return Value:
190 return ChildPid;
191 }
192
193 + if (CgroupPath.has_value())
194 + {
195 + UtilTryMoveSelfToDistroCgroup(CgroupPath.value(), false, ChildName);
196 + }
197 +
198 try
199 {
200 UtilSetThreadName(ChildName);
@@ -329,4 +342,8 @@ int WriteToFile(const char* Path, const char* Content, int OpenFlags = O_WRONLY
342 // Starts a background thread that performs memory compaction and optional cache reclaim when the VM is idle.
343 void StartMemoryReductionThread(LX_MINI_INIT_MEMORY_RECLAIM_MODE Mode);
344
332 -int ProcessCreateProcessMessage(wsl::shared::Transaction& Transaction, gsl::span<gsl::byte> Buffer);
\ No newline at end of file
345 +int ProcessCreateProcessMessage(wsl::shared::Transaction& Transaction, gsl::span<gsl::byte> Buffer, const std::optional<std::string>& DistroCgroupPath);
346 +
347 +std::string UtilGetDistroCgroupPath(pid_t DistroInitPid);
348 +
349 +int UtilEnableAllCgroupControllers(const std::string& CgroupPath);
src/shared/inc/lxinitshared.h
+3
@@ -273,6 +273,7 @@ Abstract:
273 #define LX_WSL2_DISTRO_READ_ONLY_ENV "WSL_DISTRO_READ_ONLY"
274 #define LX_WSL2_NETWORKING_MODE_ENV "WSL2_NETWORKING_MODE"
275 #define LX_WSL2_DISTRO_INIT_PID "WSL2_DISTRO_INIT_PID"
276 +#define LX_WSL2_DISTRO_CGROUP_PATH "WSL2_DISTRO_CGROUP_PATH"
277
278 //
279 // Command line arguments shared between init & mini_init
@@ -1274,6 +1275,7 @@ typedef struct _LX_MINI_INIT_EARLY_CONFIG_MESSAGE
1275 bool EnableDnsTunneling;
1276 bool EnableSafeMode;
1277 bool DefaultKernel;
1278 + bool IsolateDistroCgroup;
1279 unsigned int KernelModulesDeviceId;
1280 unsigned int HostnameOffset;
1281 unsigned int KernelModulesListOffset;
@@ -1290,6 +1292,7 @@ typedef struct _LX_MINI_INIT_EARLY_CONFIG_MESSAGE
1292 FIELD(EnableDnsTunneling),
1293 FIELD(EnableSafeMode),
1294 FIELD(DefaultKernel),
1295 + FIELD(IsolateDistroCgroup),
1296 FIELD(KernelModulesDeviceId),
1297 STRING_FIELD(HostnameOffset),
1298 STRING_FIELD(KernelModulesListOffset));
src/windows/common/WslCoreConfig.cpp
+1
@@ -110,6 +110,7 @@ void wsl::core::Config::ParseConfigFile(_In_opt_ LPCWSTR ConfigFilePath, _In_opt
110 ConfigKey(ConfigSetting::InstanceIdleTimeout, InstanceIdleTimeout),
111 ConfigKey(ConfigSetting::LoadDefaultKernelModules, LoadDefaultKernelModules, &LoadKernelModulesPresence),
112 ConfigKey(ConfigSetting::LoadKernelModules, userKernelModules, &LoadKernelModulesPresence),
113 + ConfigKey(ConfigSetting::IsolateDistroCgroup, IsolateDistroCgroup),
114
115 // Features that were previously experimental (the old header is maintained for compatibility).
116 ConfigKey({ConfigSetting::NetworkingMode, ConfigSetting::Experimental::NetworkingMode}, wsl::core::NetworkingModes, NetworkingMode, &NetworkingModePresence),
src/windows/common/WslCoreConfig.h
+9 -6
@@ -27,12 +27,13 @@ Abstract:
27 T_VALUE(c, EnableHostAddressLoopback), T_VALUE(c, EnableHostFileSystemAccess), T_VALUE(c, EnableIpv6), \
28 T_VALUE(c, EnableLocalhostRelay), T_VALUE(c, EnableNestedVirtualization), T_VALUE(c, EnableSafeMode), \
29 T_VALUE(c, EnableSparseVhd), T_VALUE(c, EnableVirtio), T_VALUE(c, EnableVirtio9p), T_VALUE(c, EnableVirtioFs), \
30 - T_ENUM(c, FirewallConfigPresence), T_VALUE(c, KernelBootTimeout), T_SET(c, KernelCommandLine), T_VALUE(c, KernelDebugPort), \
31 - T_STRING(c, KernelModulesList), T_SET(c, KernelModulesPath), T_SET(c, KernelPath), T_VALUE(c, LoadDefaultKernelModules), \
32 - T_PRESENT(c, LoadKernelModulesPresence), T_VALUE(c, MaximumMemorySizeBytes), T_VALUE(c, MaximumProcessorCount), \
33 - T_ENUM(c, MemoryReclaim), T_VALUE(c, MemorySizeBytes), T_VALUE(c, MountDeviceTimeout), T_ENUM(c, NetworkingMode), \
34 - T_VALUE(c, ProcessorCount), T_SET(c, SwapFilePath), T_VALUE(c, SwapSizeBytes), T_VALUE(c, SwiotlbSizeBytes), \
35 - T_SET(c, SystemDistroPath), T_VALUE(c, VhdSizeBytes), T_VALUE(c, VmIdleTimeout), T_SET(c, VmSwitch)
30 + T_ENUM(c, FirewallConfigPresence), T_VALUE(c, IsolateDistroCgroup), T_VALUE(c, KernelBootTimeout), \
31 + T_SET(c, KernelCommandLine), T_VALUE(c, KernelDebugPort), T_STRING(c, KernelModulesList), T_SET(c, KernelModulesPath), \
32 + T_SET(c, KernelPath), T_VALUE(c, LoadDefaultKernelModules), T_PRESENT(c, LoadKernelModulesPresence), \
33 + T_VALUE(c, MaximumMemorySizeBytes), T_VALUE(c, MaximumProcessorCount), T_ENUM(c, MemoryReclaim), \
34 + T_VALUE(c, MemorySizeBytes), T_VALUE(c, MountDeviceTimeout), T_ENUM(c, NetworkingMode), T_VALUE(c, ProcessorCount), \
35 + T_SET(c, SwapFilePath), T_VALUE(c, SwapSizeBytes), T_VALUE(c, SwiotlbSizeBytes), T_SET(c, SystemDistroPath), \
36 + T_VALUE(c, VhdSizeBytes), T_VALUE(c, VmIdleTimeout), T_SET(c, VmSwitch)
37
38 namespace wsl::core {
39 constexpr auto ToString(ConfigKeyPresence key)
@@ -277,6 +278,7 @@ namespace ConfigSetting {
278 static constexpr auto AutoProxy = "wsl2.autoProxy";
279 static constexpr auto LoadKernelModules = "wsl2.loadKernelModules";
280 static constexpr auto LoadDefaultKernelModules = "wsl2.loadDefaultKernelModules";
281 + static constexpr auto IsolateDistroCgroup = "wsl2.isolateDistroCgroup";
282
283 namespace Experimental {
284 static constexpr auto NetworkingMode = "experimental.networkingMode";
@@ -379,6 +381,7 @@ struct Config
381 std::filesystem::path CrashDumpFolder;
382 int MaxCrashDumpCount = 10;
383 UINT64 SwiotlbSizeBytes = 0;
384 + bool IsolateDistroCgroup = true;
385
386 // Temporary config value to help root cause the truncated archive errors in SetVersion()
387 bool SetVersionDebug = false;
src/windows/service/exe/WslCoreVm.cpp
+1
@@ -548,6 +548,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
548 // Consomme forwards DNS via the host proxy, so the dedicated DNS hvsocket is only used by NAT and Mirrored modes.
549 message->EnableDnsTunneling = m_vmConfig.EnableDnsTunneling && m_vmConfig.NetworkingMode != NetworkingMode::Consomme;
550 message->DefaultKernel = m_defaultKernel;
551 + message->IsolateDistroCgroup = m_vmConfig.IsolateDistroCgroup;
552 message->KernelModulesDeviceId = m_kernelModulesDeviceId;
553 message.WriteString(message->HostnameOffset, wsl::windows::common::filesystem::GetLinuxHostName());
554 message.WriteString(message->KernelModulesListOffset, m_vmConfig.KernelModulesList);
test/windows/Common.cpp
+7 -2
@@ -1460,9 +1460,9 @@ std::wstring LxssWriteWslConfig(const std::wstring& Content)
1460 }
1461
1462 // writes distro specific settings /etc/wsl.conf
1463 -std::string LxssWriteWslDistroConfig(const std::string& Content)
1463 +std::string LxssWriteWslDistroConfig(const std::string& Content, LPCWSTR DistributionName)
1464 {
1465 - std::string path = std::format("\\\\wsl.localhost\\{}\\etc\\wsl.conf", LXSS_DISTRO_NAME_TEST);
1465 + std::string path = std::format("\\\\wsl.localhost\\{}\\etc\\wsl.conf", wsl::shared::string::WideToMultiByte(DistributionName));
1466
1467 std::ifstream distroConfigRead(path);
1468 auto previousContent = std::string{std::istreambuf_iterator<char>(distroConfigRead), {}};
@@ -1648,6 +1648,11 @@ std::wstring LxssGenerateTestConfig(TestConfigDefaults Default)
1648 // TODO: Remove once SetVersion() truncated archive error is root caused.
1649 newConfig += L"\n[experimental]\nSetVersionDebug=true\n[wsl2]\n";
1650
1651 + if (Default.isolateDistroCgroup.has_value())
1652 + {
1653 + newConfig += boolOptionToString(L"isolateDistroCgroup", Default.isolateDistroCgroup, true);
1654 + }
1655 +
1656 return newConfig;
1657 }
1658
test/windows/Common.h
+8 -7
@@ -537,7 +537,7 @@ wil::unique_handle GetNonElevatedToken(TOKEN_TYPE Type = TokenPrimary);
537
538 std::wstring LxssWriteWslConfig(const std::wstring& Content);
539
540 -std::string LxssWriteWslDistroConfig(const std::string& Content);
540 +std::string LxssWriteWslDistroConfig(const std::string& Content, LPCWSTR DistributionName = LXSS_DISTRO_NAME_TEST_L);
541
542 enum class DrvFsMode
543 {
@@ -574,6 +574,7 @@ struct TestConfigDefaults
574 std::optional<bool> hostAddressLoopback;
575 int crashDumpCount = 100;
576 std::optional<std::wstring> CrashDumpFolder;
577 + std::optional<bool> isolateDistroCgroup;
578 };
579
580 std::wstring LxssGenerateTestConfig(TestConfigDefaults Default = {});
@@ -611,16 +612,16 @@ void TerminateDistribution(LPCWSTR DistributionName = LXSS_DISTRO_NAME_TEST_L);
612
613 void Trim(std::wstring& string);
614
614 -inline auto EnableSystemd(const std::string& extraConfig = "")
615 +inline auto EnableSystemd(const std::string& extraConfig = "", LPCWSTR distroName = LXSS_DISTRO_NAME_TEST_L)
616 {
617 // enable systemd on the test distro by editing /etc/wsl.conf
617 - LxssWriteWslDistroConfig("[boot]\nsystemd=true\n" + extraConfig);
618 - TerminateDistribution();
618 + LxssWriteWslDistroConfig("[boot]\nsystemd=true\n" + extraConfig, distroName);
619 + TerminateDistribution(distroName);
620
620 - return wil::scope_exit([] {
621 + return wil::scope_exit([distroName] {
622 // clean up wsl.conf file
622 - LxsstuLaunchWsl(LXSST_REMOVE_DISTRO_CONF_COMMAND_LINE);
623 - TerminateDistribution();
623 + LxsstuLaunchWsl(std::format(L"-d {} " LXSST_REMOVE_DISTRO_CONF_COMMAND_LINE, distroName));
624 + TerminateDistribution(distroName);
625 });
626 }
627
test/windows/UnitTests.cpp
+98 -1
@@ -6808,6 +6808,9 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
6808
6809 WSL2_TEST_METHOD(CGroupv1)
6810 {
6811 + // cgroupv1 conflicts with the per-distro cgroup hierarchy
6812 + WslConfigChange config(LxssGenerateTestConfig({.isolateDistroCgroup = false}));
6813 +
6814 auto expectedMount = [](const char* path, const wchar_t* expected) {
6815 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"findmnt -ln '{}' || true", path));
6816
@@ -6830,7 +6833,7 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
6833 expectedMount("/sys/fs/cgroup/cpu", L"/sys/fs/cgroup/cpu cgroup cgroup rw,nosuid,nodev,noexec,relatime,cpu\n");
6834
6835 // Validate that having cgroup_no_v1=all causes the distribution to fall back to v2.
6833 - WslConfigChange wslConfig(LxssGenerateTestConfig({.kernelCommandLine = L"cgroup_no_v1=all"}));
6836 + config.Update(LxssGenerateTestConfig({.kernelCommandLine = L"cgroup_no_v1=all", .isolateDistroCgroup = false}));
6837
6838 expectedMount("/sys/fs/cgroup/unified", L"");
6839 expectedMount("/sys/fs/cgroup", L"/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate\n");
@@ -7553,5 +7556,99 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
7556 VERIFY_ARE_EQUAL(readFile(secondPath), wsl::shared::string::WideToMultiByte(fileContent));
7557 }
7558
7559 + void ValidateIsolatedCgroupLayout(bool systemd)
7560 + {
7561 + constexpr auto secondDistroName = L"cgroup-test-distro";
7562 +
7563 + // Ensure no stale state from a previous run.
7564 + LxsstuLaunchWsl(std::format(L"--terminate {}", secondDistroName));
7565 + LxsstuLaunchWsl(std::format(L"--unregister {}", secondDistroName));
7566 +
7567 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
7568 + LxsstuLaunchWsl(std::format(L"--terminate {}", secondDistroName));
7569 + LxsstuLaunchWsl(std::format(L"--unregister {}", secondDistroName));
7570 + });
7571 +
7572 + // Import the second distro.
7573 + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", secondDistroName, g_testDistroPath)), 0L);
7574 +
7575 + std::optional<decltype(EnableSystemd())> systemdCleanup;
7576 + std::optional<decltype(EnableSystemd())> systemdCleanup2;
7577 + if (systemd)
7578 + {
7579 + systemdCleanup.emplace(EnableSystemd());
7580 + systemdCleanup2.emplace(EnableSystemd("", secondDistroName));
7581 + }
7582 +
7583 + auto getCgroup = [](LPCWSTR distro) {
7584 + auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} cat -e /proc/self/cgroup", distro));
7585 + return out;
7586 + };
7587 +
7588 + const auto cgroup1 = getCgroup(LXSS_DISTRO_NAME_TEST_L);
7589 + const auto cgroup2 = getCgroup(secondDistroName);
7590 +
7591 + LogInfo("test_distro cgroup: %ls", cgroup1.c_str());
7592 + LogInfo("%ls cgroup: %ls", secondDistroName, cgroup2.c_str());
7593 +
7594 + const std::wstring prefix = L"0::/wsl-user/distro-";
7595 + VERIFY_IS_TRUE(cgroup1.starts_with(prefix));
7596 + VERIFY_IS_TRUE(cgroup2.starts_with(prefix));
7597 + VERIFY_ARE_NOT_EQUAL(cgroup1, cgroup2);
7598 +
7599 + // Terminate both distros -- this should trigger cleanup of their per-distro cgroups.
7600 + TerminateDistribution(LXSS_DISTRO_NAME_TEST_L);
7601 + TerminateDistribution(secondDistroName);
7602 +
7603 + // Re-start the default test_distro and confirm that exactly one distro-<pid> cgroup remains:
7604 + // the one belonging to the distro we just started to perform the check. The stale cgroups of
7605 + // the two terminated distros must have been removed.
7606 + //
7607 + // N.B. Mini_init cleans up per-distro cgroups asynchronously from its SIGCHLD reaper after
7608 + // wsl --terminate returns. On slower hosts (e.g. CI pipelines) the cleanup of the two terminated distros
7609 + // can still be in flight when this check runs, so retry until cleanup completes.
7610 + VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
7611 + [&]() {
7612 + auto [out2, _] =
7613 + LxsstuLaunchWslAndCaptureOutput(L"/bin/sh -c \"ls -1 /sys/fs/cgroup/wsl-user | grep -c '^distro-'\"");
7614 + THROW_HR_IF(E_UNEXPECTED, out2 != std::wstring(L"1\n"));
7615 + },
7616 + std::chrono::seconds(1),
7617 + std::chrono::seconds(30)));
7618 + }
7619 +
7620 + WSL2_TEST_METHOD(IsolatedCgroupLayout)
7621 + {
7622 + ValidateIsolatedCgroupLayout(false);
7623 + }
7624 +
7625 + WSL2_TEST_METHOD(IsolatedCgroupLayoutSystemd)
7626 + {
7627 + ValidateIsolatedCgroupLayout(true);
7628 + }
7629 +
7630 + WSL2_TEST_METHOD(IsolatedCgroupLayoutDisabled)
7631 + {
7632 + WslConfigChange config(LxssGenerateTestConfig({.isolateDistroCgroup = false}));
7633 +
7634 + auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/self/cgroup");
7635 + while (!out.empty() && (out.back() == L'\n' || out.back() == L'\r'))
7636 + {
7637 + out.pop_back();
7638 + }
7639 +
7640 + LogInfo("cgroup with isolateDistroCgroup=false: %ls", out.c_str());
7641 +
7642 + VERIFY_ARE_EQUAL(out, std::wstring(L"0::/"));
7643 +
7644 + auto [exists, __] =
7645 + LxsstuLaunchWslAndCaptureOutput(L"/bin/sh -c \"[ -d /sys/fs/cgroup/wsl-user ] && echo yes || echo no\"");
7646 + while (!exists.empty() && (exists.back() == L'\n' || exists.back() == L'\r'))
7647 + {
7648 + exists.pop_back();
7649 + }
7650 + VERIFY_ARE_EQUAL(exists, std::wstring(L"no"));
7651 + }
7652 +
7653 }; // namespace UnitTests
7654 } // namespace UnitTests