master
cpp 8,050 lines 343 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 UnitTests.cpp
8
9 Abstract:
10
11 This file contains unit tests for WSL.
12
13 --*/
14
15 #include "precomp.h"
16
17 #include "Common.h"
18 #include "install.h"
19 #include <AclAPI.h>
20 #include <fstream>
21 #include <sstream>
22 #include <filesystem>
23 #include "wslservice.h"
24 #include "registry.hpp"
25 #include "helpers.hpp"
26 #include "svccomm.hpp"
27 #include "ConsoleState.h"
28 #include "lxfsshares.h"
29 #include <userenv.h>
30 #include <nlohmann/json.hpp>
31 #include "Distribution.h"
32 #include "WslCoreConfigInterface.h"
33 #include "CommandLine.h"
34 #include "retryshared.h"
35
36 #define LXSST_TEST_USERNAME L"kerneltest"
37
38 #define LXSST_LXFS_TEST_DIR L"lxfstest"
39 #define LXSST_LXFS_MKDIR_COMMAND_LINE \
40 L"/bin/bash -c \"mkdir /" LXSST_LXFS_TEST_DIR "; chown 1000:1001 /" LXSST_LXFS_TEST_DIR L"\""
41 #define LXSST_LXFS_CLEANUP_COMMAND_LINE L"/bin/bash -c \"rm -rf /" LXSST_LXFS_TEST_DIR L"\""
42 #define LXSST_LXFS_TEST_SUB_DIR L"testdir"
43
44 #define LXSST_FSTAB_BACKUP_COMMAND_LINE L"/bin/bash -c 'cp /etc/fstab /etc/fstab.bak'"
45 #define LXSST_FSTAB_SETUP_COMMAND_LINE L"/bin/bash -c 'echo C:\\\\ /mnt/c drvfs metadata 0 0 >> /etc/fstab'"
46 #define LXSST_FSTAB_CLEANUP_COMMAND_LINE L"/bin/bash -c \"cp /etc/fstab.bak /etc/fstab\""
47
48 #define LXSST_IMPORT_DISTRO_TEST_DIR L"C:\\importtest\\"
49
50 #define LXSST_UID_ROOT 0
51 #define LXSST_GID_ROOT 0
52 #define LXSST_USERNAME_ROOT L"root"
53
54 #define LXSS_OOBE_COMPLETE_NAME L"OOBEComplete"
55
56 constexpr auto c_testDistributionEndpoint = L"http://127.0.0.1:12345/";
57 constexpr auto c_testDistributionJson =
58 LR"({
59 \"Distributions\":[
60 {
61 \"Name\": \"Debian\",
62 \"FriendlyName\": \"Debian\",
63 \"StoreAppId\": \"Dummy\",
64 \"Amd64\": true,
65 \"Arm64\": true,
66 \"Amd64PackageUrl\": null,
67 \"Arm64PackageUrl\": null,
68 \"PackageFamilyName\": \"Dummy\"
69 }
70 ]})";
71
72 using wsl::windows::common::wslutil::GetSystemErrorString;
73
74 extern std::wstring g_testDistroPath;
75
76 namespace UnitTests {
77 class UnitTests
78 {
79 WSL_TEST_CLASS(UnitTests)
80
81 TEST_CLASS_SETUP(TestClassSetup)
82 {
83 VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE);
84
85 // Build the unit tests on the Linux side
86 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(LXSST_TESTS_INSTALL_COMMAND_LINE), (DWORD)0);
87
88 return true;
89 }
90
91 TEST_CLASS_CLEANUP(TestClassCleanup)
92 {
93 LxsstuLaunchWsl(LXSST_LXFS_CLEANUP_COMMAND_LINE);
94 LxsstuUninitialize(FALSE);
95 return true;
96 }
97
98 TEST_METHOD_CLEANUP(MethodCleanup)
99 {
100 LxssLogKernelOutput();
101 return true;
102 }
103
104 // Note: This test should run first since other test cases create files extended attributes, which causes bdstar to emit warnings during export.
105 TEST_METHOD(ExportDistro)
106 {
107 constexpr auto tarPath = L"exported-test-distro.tar";
108 constexpr auto vhdPath = L"exported-test-distro.vhdx";
109 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
110 LOG_IF_WIN32_BOOL_FALSE(DeleteFile(tarPath));
111 LOG_IF_WIN32_BOOL_FALSE(DeleteFile(vhdPath));
112 });
113
114 {
115 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {}", LXSS_DISTRO_NAME_TEST_L, tarPath));
116
117 VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
118 VERIFY_ARE_EQUAL(err, L"");
119 }
120
121 // Validate that the file is a valid tar
122 {
123 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"bash -c 'tar tf {} | grep -iF /root/.bashrc'", tarPath));
124 VERIFY_ARE_EQUAL(out, L"./root/.bashrc\n");
125 VERIFY_ARE_EQUAL(err, L"");
126 }
127
128 // Validate that gzip compression works
129 {
130 auto [out, err] =
131 LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format tar.gz", LXSS_DISTRO_NAME_TEST_L, tarPath));
132
133 VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
134 VERIFY_ARE_EQUAL(err, L"");
135
136 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"gzip -t {}", tarPath)), 0L);
137 }
138
139 // Verify that xzip compression works
140 {
141 auto [out, err] =
142 LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format tar.xz", LXSS_DISTRO_NAME_TEST_L, tarPath));
143
144 VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
145 VERIFY_ARE_EQUAL(err, L"");
146
147 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"xz -t {}", tarPath)), 0L);
148 }
149
150 // Validate that exporting as vhd works
151 if (LxsstuVmMode())
152 {
153 WslShutdown(); // TODO: detach disk when distribution is stopped to remove this requirement.
154
155 auto [out, err] =
156 LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdPath));
157
158 VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
159 VERIFY_ARE_EQUAL(err, L"");
160
161 auto [vhdType, _] = LxsstuLaunchPowershellAndCaptureOutput(std::format(L"(Get-VHD '{}').VhdType", vhdPath));
162 VERIFY_ARE_EQUAL(vhdType, L"Dynamic\r\n");
163 }
164 else
165 {
166 auto [out, err] =
167 LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdPath), -1);
168
169 VERIFY_ARE_EQUAL(
170 out, FormatErrorMessage(L"This operation is only supported by WSL2.", L"Wsl/Service/WSL_E_WSL2_NEEDED"));
171 VERIFY_ARE_EQUAL(err, L"");
172 }
173
174 VerifyInvalidUsage(std::format(L"--export {} {} --format tar.gz --format tar.xz", LXSS_DISTRO_NAME_TEST_L, tarPath));
175 VerifyInvalidUsage(std::format(L"--export {} {} --format tar.xz --vhd", LXSS_DISTRO_NAME_TEST_L, tarPath));
176 }
177
178 WSL2_TEST_METHOD(SystemdSafeMode)
179 {
180 SKIP_TEST_UNSTABLE(); // TODO: Re-enable when this issue is solved in main.
181
182 auto revert = EnableSystemd();
183
184 // generate a new test config with safe mode enabled
185 WslConfigChange config(LxssGenerateTestConfig({.safeMode = true}));
186
187 // verify that even though systemd is enabled, safe mode prevents it from executing
188 VERIFY_IS_FALSE(IsSystemdRunning(L"--system", 1));
189
190 config.Update(L"");
191
192 // disable safe mode and verify that it systemd runs
193 VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
194 }
195
196 WSL2_TEST_METHOD(SystemdDisabled)
197 {
198 // tests that systemd does not run without the wsl.conf option enabled
199 // run and check the output of systemctl --system
200 VERIFY_IS_FALSE(IsSystemdRunning(L"--system", 1));
201 }
202
203 WSL2_TEST_METHOD(SystemdSystem)
204 {
205 auto cleanup = wil::scope_exit([] {
206 // clean up wsl.conf file
207 const std::wstring disableSystemdCmd(LXSST_REMOVE_DISTRO_CONF_COMMAND_LINE);
208 LxsstuLaunchWsl(disableSystemdCmd);
209 TerminateDistribution();
210 });
211
212 auto revert = EnableSystemd();
213 VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
214
215 // Validate that systemd-networkd-wait-online.service is masked.
216 std::wstring out;
217 std::wstring err;
218 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"systemctl show -p LoadState systemd-networkd-wait-online.service");
219 VERIFY_ARE_EQUAL(out, L"LoadState=masked\n");
220
221 // Validate that NetworkManager-wait-online.service is masked.
222 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"systemctl show -p LoadState NetworkManager-wait-online.service");
223 VERIFY_ARE_EQUAL(out, L"LoadState=masked\n");
224
225 // Validate that console-getty.service is masked (tty devices are shared at VM level across distros).
226 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"systemctl show -p LoadState console-getty.service");
227 VERIFY_ARE_EQUAL(out, L"LoadState=masked\n");
228 }
229
230 WSL2_TEST_METHOD(SystemdUser)
231 {
232 // enable systemd before creating the user.
233 // if not called first, the runtime directories needed for --user will not have been created
234 auto cleanup = EnableSystemd();
235
236 // create test user and run test as that user
237 ULONG TestUid;
238 ULONG TestGid;
239 CreateUser(LXSST_TEST_USERNAME, &TestUid, &TestGid);
240 auto userCleanup = wil::scope_exit([]() { LxsstuLaunchWsl(L"userdel " LXSST_TEST_USERNAME); });
241
242 auto validateUserSession = [&]() {
243 // verify that the user service is running
244 const std::wstring isServiceActiveCmd =
245 std::format(L"-u {} systemctl is-active user@{}.service ; exit 0", LXSST_TEST_USERNAME, TestUid);
246 std::wstring out;
247 std::wstring err;
248
249 try
250 {
251 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(isServiceActiveCmd.data());
252 }
253 CATCH_LOG();
254
255 Trim(out);
256
257 if (out.compare(L"active") != 0)
258 {
259 LogError(
260 "Unexpected output from systemd: %ls. Stderr: %ls, cmd: %ls", out.c_str(), err.c_str(), isServiceActiveCmd.c_str());
261 VERIFY_FAIL();
262 }
263
264 // Verify that /run/user/<uid> is a writable tmpfs mount visible in both mount namespaces.
265 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"touch /run/user/" + std::to_wstring(TestUid) + L"/dummy-test-file"), 0u);
266 auto command = L"mount | grep -iF 'tmpfs on /run/user/" + std::to_wstring(TestUid) + L" type tmpfs (rw'";
267 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command), 0u);
268
269 const auto nonElevatedToken = GetNonElevatedToken();
270 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command, nullptr, nullptr, nullptr, nonElevatedToken.get()), 0u);
271 };
272
273 // Validate user sessions state with gui apps disabled.
274 WslConfigChange config(LxssGenerateTestConfig({.guiApplications = false}));
275 {
276 validateUserSession();
277
278 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $DISPLAY", LXSST_TEST_USERNAME));
279 VERIFY_ARE_EQUAL(out, L"\n");
280
281 // N.B. The XDG_RUNTIME_DIR variable is always set by init even if gui apps are disabled.
282 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $XDG_RUNTIME_DIR", LXSST_TEST_USERNAME));
283 VERIFY_ARE_EQUAL(out, std::format(L"/run/user/{}\n", TestUid));
284 }
285
286 // Validate user sessions state with gui apps enabled.
287 {
288 config.Update(LxssGenerateTestConfig({.guiApplications = true}));
289
290 validateUserSession();
291 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $DISPLAY", LXSST_TEST_USERNAME));
292 VERIFY_ARE_EQUAL(out, L":0\n");
293
294 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $XDG_RUNTIME_DIR", LXSST_TEST_USERNAME));
295 VERIFY_ARE_EQUAL(out, std::format(L"/run/user/{}\n", TestUid));
296 }
297
298 // Create a 'broken' /run/user and validate that the warning is correctly displayed.
299 {
300 TerminateDistribution();
301
302 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"chmod 000 /run/user"), 0L);
303
304 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-u {} echo OK", LXSST_TEST_USERNAME));
305
306 VERIFY_ARE_EQUAL(out, L"OK\n");
307 VERIFY_ARE_EQUAL(
308 err, L"wsl: Failed to start the systemd user session for 'kerneltest'. See journalctl for more details.\n");
309 }
310 }
311
312 static bool IsSystemdRunning(const std::wstring& SystemdScope, int ExpectedExitCode = 0)
313 {
314 // run and check the output of systemctl --system
315 const auto systemctlCmd = std::format(L"systemctl '{}' is-system-running ; exit 0", SystemdScope);
316 std::wstring out;
317 std::wstring error;
318
319 // capture the output of systemctl and trim for good measure
320 try
321 {
322 std::tie(out, error) = LxsstuLaunchWslAndCaptureOutput(systemctlCmd.c_str(), ExpectedExitCode);
323 }
324 CATCH_LOG()
325 Trim(out);
326
327 // ensure that systemd is either running in a degraded or running state
328 if ((out.compare(L"degraded") == 0) || (out.compare(L"running") == 0))
329 {
330 return true;
331 }
332 LogInfo(
333 "Error when checking if systemd is running: %ls (scope: %ls, stderr: %ls)", out.c_str(), SystemdScope.c_str(), error.c_str());
334 return false;
335 }
336
337 WSL2_TEST_METHOD(SystemdNoClearTmpUnit)
338 {
339 // The X11 socket is only created when gui applications are enabled.
340 WslConfigChange config(LxssGenerateTestConfig({.guiApplications = true}));
341
342 // ensures that we don't leave state on exit
343 auto cleanup = EnableSystemd("initTimeout=0");
344
345 // Wait for systemd to be started
346 VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
347 [&]() { THROW_HR_IF(E_UNEXPECTED, !IsSystemdRunning(L"--system")); }, std::chrono::seconds(1), std::chrono::minutes(1)));
348
349 // Validate that the X11 socket has not been deleted
350 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d /tmp/.X11-unix"), 0L);
351 }
352
353 WSL2_TEST_METHOD(BinfmtStatusIsLocked)
354 {
355 //
356 // Validates the protection mechanism for the cross-distro binfmt wipe bug.
357 //
358 // Fix: per-distro init bind-mounts a read-only file over
359 // /proc/sys/fs/binfmt_misc/status before exec'ing the distro's init
360 // (see LockBinfmtStatusReadOnly in src/linux/init/init.cpp). systemd-shutdown's
361 // disable_binfmt() writes "-1" to that file to clear the kernel-global
362 // binfmt_misc table at shutdown; with the bind-mount in place the write
363 // fails with EROFS so the entries shared with other running distros
364 // survive. Per-entry operations (registering new entries via /register,
365 // unregistering individual entries via the entry file) are unaffected.
366 //
367
368 // Default: bind-mount must be in place.
369 {
370 // EnableSystemd raises /proc/sys/fs/nr_open VM-wide; without a full
371 // VM teardown that bumped value persists across distro restarts and
372 // breaks later tests like ResourceLimits that assume the kernel default.
373 auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
374 auto cleanupSystemd = EnableSystemd();
375
376 // /status is its own mount point.
377 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mountpoint -q /proc/sys/fs/binfmt_misc/status"), 0u);
378
379 // Reading /status returns the lock-file content ("enabled\n") so
380 // callers that just check whether binfmt_misc is enabled still get a
381 // sensible answer.
382 {
383 auto [status, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/fs/binfmt_misc/status");
384 VERIFY_ARE_EQUAL(status, L"enabled\n");
385 }
386
387 // Direct write to /status — the wipe vector — must fail with EROFS.
388 // The shell's redirection error ("cannot create ...: Read-only file
389 // system") goes to the shell's stderr when the `>` open fails.
390 {
391 auto [_, err] = LxsstuLaunchWslAndCaptureOutput(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/status; exit 0'");
392 VERIFY_IS_TRUE(err.find(L"Read-only file system") != std::wstring::npos);
393 }
394
395 // WSLInterop survives the failed wipe attempt.
396 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/WSLInterop"), 0L);
397
398 // Runtime registration via /register still works (we only block /status).
399 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sh -c 'echo \":wsltestbinfmt:M::WSLTESTMAGIC::/bin/echo:\" > /proc/sys/fs/binfmt_misc/register'"), 0L);
400
401 // binfmt_misc is VM-global, so a leftover wsltestbinfmt entry would
402 // cascade into later tests. Always remove it on scope exit.
403 auto cleanupTestEntry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
404 LxsstuLaunchWsl(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/wsltestbinfmt 2>/dev/null || true'");
405 });
406
407 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/wsltestbinfmt"), 0L);
408
409 // Per-entry unregister (writing -1 to the entry file, not /status) still works.
410 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/wsltestbinfmt'"), 0L);
411 VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/wsltestbinfmt"), 0L);
412 cleanupTestEntry.release();
413
414 // Interop still works.
415 {
416 auto [cmd, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
417 VERIFY_ARE_EQUAL(cmd, L"ok\r\n");
418 }
419 }
420
421 // protectBinfmt=false: bind-mount must NOT be installed (kill switch).
422 // EnableSystemd's cleanup re-launches the distro to revert wsl.conf and
423 // then terminates it; that termination invokes systemd-shutdown's
424 // disable_binfmt() which wipes the kernel-global table because
425 // protectBinfmt=false leaves /status writable. WslShutdown registered
426 // FIRST (runs LAST in LIFO unwind) ensures the VM is fully torn down
427 // after the wipe, so the next test starts a fresh VM where mini_init
428 // re-registers WSLInterop.
429 {
430 auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
431 auto cleanupSystemd = EnableSystemd("protectBinfmt=false");
432
433 VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"mountpoint -q /proc/sys/fs/binfmt_misc/status"), 0L);
434 }
435 }
436
437 WSL2_TEST_METHOD(SystemdKillInitTerminatesDistro)
438 {
439 WslConfigChange config(LxssGenerateTestConfig() + L"[general]\ninstanceIdleTimeout=-1");
440 auto revert = EnableSystemd("initTimeout=0");
441 // Wait for systemd to start
442 VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
443 [&]() { THROW_HR_IF(E_UNEXPECTED, !IsSystemdRunning(L"--system")); }, std::chrono::seconds(1), std::chrono::minutes(1)));
444
445 // Kill the WSL init process
446 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"kill -9 2"), 0L);
447
448 // Wait for the distro to exit.
449 VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
450 [&]() { THROW_HR_IF(E_ABORT, GetDistroState() == LxssDistributionStateRunning); }, std::chrono::seconds(1), std::chrono::seconds(30)));
451
452 // Verify that a new WSL command succeeds (the distro restarts cleanly).
453 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"echo hello");
454 VERIFY_ARE_EQUAL(out, L"hello\n");
455 }
456
457 WSL2_TEST_METHOD(BinfmtSurvivesDistroTermination)
458 {
459 //
460 // Regression test for the "Exec format error" bug: binfmt_misc registrations
461 // (most importantly WSLInterop) must survive when a peer systemd-enabled distro
462 // terminates. Before this fix, systemd-shutdown's disable_binfmt() wrote `-1`
463 // to /proc/sys/fs/binfmt_misc/status, which clears the entire binfmt_misc
464 // entry table. binfmt_misc itself is a single kernel-global registry — it is
465 // not isolated per distro — so that one write wiped WSLInterop for every
466 // running distro and broke Windows interop everywhere.
467 //
468
469 constexpr auto peerDistroName = L"binfmt-peer-test";
470
471 // EnableSystemd raises /proc/sys/fs/nr_open VM-wide; without a full
472 // VM teardown that bumped value persists across distro restarts and
473 // breaks later tests like ResourceLimits that assume the kernel default.
474 auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
475
476 // Enable systemd on the primary test distro.
477 auto cleanupSystemd = EnableSystemd();
478
479 // Import a second distro from the same tarball as the test distro.
480 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", peerDistroName, g_testDistroPath)), 0L);
481
482 auto cleanupPeer =
483 wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(std::format(L"--unregister {}", peerDistroName)); });
484
485 // Enable systemd in the peer distro (no helper exists for non-test distros).
486 VERIFY_ARE_EQUAL(
487 LxsstuLaunchWsl(std::format(L"-d {} -- sh -c \"mkdir -p /etc && printf '[boot]\\nsystemd=true\\n' > /etc/wsl.conf\"", peerDistroName)),
488 0L);
489
490 // Terminate so the config takes effect on next start.
491 TerminateDistribution(peerDistroName);
492
493 // Verify interop works in both distros (this also starts the peer with systemd).
494 {
495 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo alive");
496 VERIFY_ARE_EQUAL(out, L"alive\r\n");
497 }
498
499 {
500 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} -- cmd.exe /c echo alive", peerDistroName));
501 VERIFY_ARE_EQUAL(out, L"alive\r\n");
502 }
503
504 // Terminate the peer distro — this triggers systemd shutdown. Without
505 // the fix, systemd-shutdown's disable_binfmt() would clear the kernel-
506 // global binfmt_misc table for every running distro.
507 TerminateDistribution(peerDistroName);
508
509 // Verify interop still works in the primary distro.
510 {
511 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo survived");
512 VERIFY_ARE_EQUAL(out, L"survived\r\n");
513 }
514
515 // Verify the binfmt entry still exists and carries the F (fix-binary) flag.
516 // The F flag is required so the kernel resolves the interpreter at
517 // registration time, making the entry independent of mount-namespace state.
518 {
519 auto [flags, _] = LxsstuLaunchWslAndCaptureOutput(L"grep ^flags /proc/sys/fs/binfmt_misc/WSLInterop");
520 VERIFY_IS_TRUE(flags.find(L"F") != std::wstring::npos);
521 }
522 }
523
524 WSL2_TEST_METHOD(SharedMountSurvivesDistroTermination)
525 {
526 constexpr auto peerDistroName = L"mount-guard-peer-test";
527
528 auto validate = [&](const std::string& automountRoot) {
529 const auto extraConfig = automountRoot.empty() ? "" : std::format("[automount]\nroot={}\n", automountRoot);
530 const auto effectiveAutomountRoot = automountRoot.empty() ? "/mnt" : automountRoot;
531 const auto mountPoint = std::format(L"{}/wsl/mount-guard-test", wsl::shared::string::MultiByteToWide(effectiveAutomountRoot));
532
533 auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
534 auto cleanupSystemd = EnableSystemd(extraConfig);
535
536 LxsstuLaunchWsl(std::format(L"--unregister {}", peerDistroName));
537 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", peerDistroName, g_testDistroPath)), 0L);
538 auto cleanupPeer = wil::scope_exit_log(
539 WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(std::format(L"--unregister {}", peerDistroName)); });
540
541 auto cleanupPeerSystemd = EnableSystemd(extraConfig, peerDistroName);
542
543 VERIFY_ARE_EQUAL(
544 LxsstuLaunchWsl(std::format(L"-d {} -- sh -c \"systemctl is-system-running | grep -Eq 'running|degraded'\"", peerDistroName)), 0L);
545
546 VERIFY_ARE_EQUAL(
547 LxsstuLaunchWsl(std::format(
548 L"sh -c 'mkdir -p {0} && mount -t tmpfs -o size=4M mount-guard-test {0} && echo survived > {0}/marker'", mountPoint)),
549 0L);
550 auto cleanupMount = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
551 LxsstuLaunchWsl(std::format(L"sh -c 'umount {0} 2>/dev/null || true; rmdir {0} 2>/dev/null || true'", mountPoint));
552 });
553
554 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} -- findmnt -n {}", peerDistroName, mountPoint)), 0L);
555 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} -- grep -qx survived {}/marker", peerDistroName, mountPoint)), 0L);
556
557 TerminateDistribution(peerDistroName);
558
559 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"findmnt -n {}", mountPoint)), 0L);
560 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"grep -qx survived {}/marker", mountPoint)), 0L);
561 };
562
563 validate("");
564 validate("/wsl-test-mount");
565 }
566
567 WSL2_TEST_METHOD(ConfigUpdateLanguage)
568 {
569 // Validates that init populates $LANG from the distro locale configuration file.
570 // ConfigUpdateLanguage reads /etc/default/locale first, then /etc/locale.conf, and uses
571 // the first file that exists. See ConfigUpdateLanguage in src/linux/init/config.cpp.
572
573 DistroFileChange defaultLocale(L"/etc/default/locale", LxsstuLaunchWsl(L"test -f /etc/default/locale") == 0);
574 DistroFileChange localeConf(L"/etc/locale.conf", LxsstuLaunchWsl(L"test -f /etc/locale.conf") == 0);
575
576 const auto readLang = []() { return LxsstuLaunchWslAndCaptureOutput(L"printenv LANG").first; };
577
578 // Only /etc/default/locale is present (Debian/Ubuntu).
579 {
580 defaultLocale.Delete();
581 localeConf.Delete();
582 defaultLocale.SetContent(L"LANG=de_DE.UTF-8\n");
583 TerminateDistribution();
584 VERIFY_ARE_EQUAL(readLang(), L"de_DE.UTF-8\n");
585 }
586
587 // Only /etc/locale.conf is present (Fedora, Arch, openSUSE, ...).
588 {
589 defaultLocale.Delete();
590 localeConf.Delete();
591 localeConf.SetContent(L"LANG=\"fr_FR.UTF-8\"\n");
592 TerminateDistribution();
593 VERIFY_ARE_EQUAL(readLang(), L"fr_FR.UTF-8\n");
594 }
595
596 // Both files are present: /etc/default/locale takes precedence because it is read first.
597 {
598 defaultLocale.Delete();
599 localeConf.Delete();
600 defaultLocale.SetContent(L"LANG=ja_JP.UTF-8\n");
601 localeConf.SetContent(L"LANG=en_US.UTF-8\n");
602 TerminateDistribution();
603 VERIFY_ARE_EQUAL(readLang(), L"ja_JP.UTF-8\n");
604 }
605 }
606
607 TEST_METHOD(Dup)
608 {
609 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dup", L"Dup"));
610 }
611
612 WSL1_TEST_METHOD(Epoll)
613 {
614 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests epoll", L"Epoll"));
615 }
616
617 TEST_METHOD(EventFd)
618 {
619
620 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests eventfd", L"EventFd"));
621 }
622
623 TEST_METHOD(Flock)
624 {
625
626 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests flock", L"Flock"));
627 }
628
629 WSL1_TEST_METHOD(Fork)
630 {
631 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fork", L"Fork"));
632 }
633
634 WSL1_TEST_METHOD(FsCommonLxFs)
635 {
636 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fscommon", L"fscommon_lxfs"));
637 }
638
639 TEST_METHOD(GetSetId)
640 {
641 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests get_set_id", L"get_set_id"));
642 }
643
644 WSL1_TEST_METHOD(Inotify)
645 {
646 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests inotify", L"INOTIFY"));
647 }
648
649 #if !defined(_ARM64_)
650
651 TEST_METHOD(ResourceLimits)
652 {
653 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests resourcelimits", L"resourcelimits"));
654 }
655
656 TEST_METHOD(Select)
657 {
658 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests select", L"Select"));
659 }
660
661 #endif
662
663 TEST_METHOD(Madvise)
664 {
665 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests madvise", L"madvise"));
666 }
667
668 TEST_METHOD(Mprotect)
669 {
670 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests mprotect", L"mprotect"));
671 }
672
673 WSL1_TEST_METHOD(Pipe)
674 {
675 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests pipe", L"Pipe"));
676 }
677
678 WSL1_TEST_METHOD(Sched)
679 {
680 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sched", L"sched"));
681 }
682
683 WSL1_TEST_METHOD(SocketNonblocking)
684 {
685 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests socket_nonblock", L"socket_nonblocking"));
686 }
687
688 WSL1_TEST_METHOD(Splice)
689 {
690 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests splice", L"Splice"));
691 }
692
693 WSL1_TEST_METHOD(Sysfs)
694 {
695 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sysfs", L"SysFs"));
696 }
697
698 WSL1_TEST_METHOD(Tty)
699 {
700 auto OriginalHandles = UseOriginalStdHandles();
701
702 auto Restore = wil::scope_exit([&OriginalHandles]() { RestoreTestStdHandles(OriginalHandles); });
703
704 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests tty", L"tty"));
705 }
706
707 WSL1_TEST_METHOD(Utimensat)
708 {
709 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests utimensat", L"Utimensat"));
710 }
711
712 TEST_METHOD(WaitPid)
713 {
714 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests waitpid", L"WaitPid"));
715 }
716
717 TEST_METHOD(Brk)
718 {
719 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests brk", L"brk"));
720 }
721
722 TEST_METHOD(Mremap)
723 {
724 // This is disabled because of intermittent test failures in WSL1 mode.
725 // TODO: Enable this test once the underlying issue is resolved:
726 SKIP_TEST_UNSTABLE();
727
728 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests mremap", L"mremap"));
729 }
730
731 TEST_METHOD(VfsAccess)
732 {
733 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests vfsaccess", L"vfsaccess"));
734 }
735
736 WSL1_TEST_METHOD(DevPt)
737 {
738 auto OriginalHandles = UseOriginalStdHandles();
739
740 auto Restore = wil::scope_exit([&OriginalHandles]() { RestoreTestStdHandles(OriginalHandles); });
741
742 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dev_pt", L"dev_pt"));
743
744 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dev_pt_2", L"dev_pt_2"));
745 }
746
747 WSL1_TEST_METHOD(Timer)
748 {
749 // This is disabled because of intermittent test failures.
750 // TODO: Enable this test once the underlying issue is resolved.
751 SKIP_TEST_UNSTABLE();
752
753 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests timer", L"timer"));
754 }
755
756 WSL1_TEST_METHOD(SysInfo)
757 {
758 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sysinfo", L"Sysinfo"));
759 }
760
761 TEST_METHOD(TimerFd)
762 {
763 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests timerfd", L"timerfd"));
764 }
765
766 WSL1_TEST_METHOD(Ioprio)
767 {
768 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests ioprio", L"Ioprio"));
769 }
770
771 TEST_METHOD(Interop)
772 {
773 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests interop", L"interop"));
774
775 //
776 // Run wsl.exe with a very long command line. This ensures that the buffer
777 // resizing logic that is used by the WSL init daemon is able to correctly
778 // handle very long messages.
779 //
780 // N.B. /bin/true ignores all arguments and always returns 0.
781 //
782
783 std::wstring Command{L"/bin/true "};
784 Command += std::wstring(0x1000, L'x');
785 VERIFY_IS_TRUE(LxsstuLaunchWsl(Command.c_str()) == 0);
786
787 // Validate that windows executable can run from the linux filesystem. See: https://github.com/microsoft/WSL/issues/10812
788 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"cp /mnt/c/Program\\ Files/WSL/wsl.exe /tmp"), 0L);
789 auto [out, _] =
790 LxsstuLaunchWslAndCaptureOutput(L"WSLENV=WSL_UTF8 WSL_UTF8=1 WSL_INTEROP=/run/WSL/1_interop /tmp/wsl.exe --version");
791
792 VERIFY_IS_TRUE(out.find(TEXT(WSL_PACKAGE_VERSION)) != std::string::npos);
793 }
794
795 static std::wstring FormUserCommandLine(_In_ const std::wstring& Username, _In_ ULONG Uid, _In_ ULONG Gid)
796 {
797 return std::format(L"/data/test/wsl_unit_tests user {} {} {}", Username, Uid, Gid);
798 }
799
800 TEST_METHOD(User)
801 {
802 //
803 // Create a test user and run the test as that user.
804 //
805
806 ULONG TestUid;
807 ULONG TestGid;
808 CreateUser(LXSST_TEST_USERNAME, &TestUid, &TestGid);
809 std::wstring CommandLine = FormUserCommandLine(LXSST_TEST_USERNAME, TestUid, TestGid);
810 LogInfo("Running test as user %s", LXSST_TEST_USERNAME);
811 VERIFY_NO_THROW(LxsstuRunTest(CommandLine.c_str(), L"user", LXSST_TEST_USERNAME));
812
813 //
814 // Add the user to 64 more groups to make sure > 32 groups is supported.
815 //
816
817 {
818 DistroFileChange groups(L"/etc/group", true);
819 CommandLine = std::format(L"-- for i in $(seq 1 64); do groupadd group$i; usermod -a -G group$i {}; done", LXSST_TEST_USERNAME);
820 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(CommandLine), (DWORD)0);
821 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"{} {} {}", WSL_USER_ARG_LONG, LXSST_TEST_USERNAME, "echo success")), (DWORD)0);
822 }
823
824 //
825 // Run the test as root.
826 //
827
828 ULONG RootUid;
829 ULONG RootGid;
830 CreateUser(LXSST_USERNAME_ROOT, &RootUid, &RootGid);
831 CommandLine = FormUserCommandLine(LXSST_USERNAME_ROOT, LXSST_UID_ROOT, LXSST_GID_ROOT);
832 LogInfo("Running test as user %s", LXSST_USERNAME_ROOT);
833 VERIFY_NO_THROW(LxsstuRunTest(CommandLine.c_str(), L"user", LXSST_USERNAME_ROOT));
834
835 //
836 // Set the default user to the newly created user.
837 //
838 // N.B. Modifying the default UID should cause the instance to be recreated and the plan9 server launched as the default user.
839 //
840
841 const auto wslSupport =
842 wil::CoCreateInstance<LxssUserSession, IWslSupport>(CLSCTX_LOCAL_SERVER | CLSCTX_ENABLE_CLOAKING | CLSCTX_ENABLE_AAA);
843
844 ULONG Version;
845 ULONG DefaultUid;
846 wil::unique_cotaskmem_array_ptr<wil::unique_cotaskmem_ansistring> DefaultEnvironment{};
847 ULONG WslFlags;
848 VERIFY_SUCCEEDED(wslSupport->GetDistributionConfiguration(
849 LXSS_DISTRO_NAME_TEST_L, &Version, &DefaultUid, DefaultEnvironment.size_address<ULONG>(), &DefaultEnvironment, &WslFlags));
850
851 VERIFY_SUCCEEDED(wslSupport->SetDistributionConfiguration(LXSS_DISTRO_NAME_TEST_L, TestUid, WslFlags));
852 auto cleanup = wil::scope_exit([&] {
853 try
854 {
855 VERIFY_SUCCEEDED(wslSupport->SetDistributionConfiguration(LXSS_DISTRO_NAME_TEST_L, DefaultUid, WslFlags));
856 }
857 catch (...)
858 {
859 LogError("Error while restoring default user");
860 }
861 });
862
863 //
864 // Create a new file using the 9p server.
865 //
866
867 const std::wstring Path = L"\\\\wsl.localhost\\" LXSS_DISTRO_NAME_TEST_L L"\\data\\test\\default_user_test";
868 const wil::unique_hfile File(CreateFile(
869 Path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
870
871 if (!File)
872 {
873 LogError("Failed to create file, error=%lu", GetLastError());
874 VERIFY_FAIL();
875 }
876
877 //
878 // Ensure the new file was created with the correct uid.
879 //
880
881 VERIFY_ARE_EQUAL(
882 LxsstuLaunchWsl(L"stat -c %U /data/test/default_user_test | grep -iF kerneltest", nullptr, nullptr, nullptr, nullptr), 0u);
883 }
884
885 WSL1_TEST_METHOD(Execve)
886 {
887 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests execve", L"Execve"));
888 }
889
890 TEST_METHOD(Xattr)
891 {
892 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests xattr", L"xattr"));
893 }
894
895 WSL1_TEST_METHOD(Namespace)
896 {
897 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests namespace", L"Namespace"));
898 }
899
900 TEST_METHOD(BinFmt)
901 {
902 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests binfmt", L"BinFmt"));
903
904 //
905 // Perform a shutdown since the binfmt test modifies the binfmt config.
906 //
907
908 WslShutdown();
909 }
910
911 TEST_METHOD(Cgroup)
912 {
913 //
914 // For WSL1, run the cgroup unit test. For WSL2, ensure the cgroupv2 filesystem is mounted in the expected location.
915 //
916
917 if (!LxsstuVmMode())
918 {
919 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests cgroup", L"cgroup"));
920 }
921 else
922 {
923 VERIFY_ARE_EQUAL(
924 LxsstuLaunchWsl(
925 L"mount | grep -iF 'cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)'", nullptr, nullptr, nullptr, nullptr),
926 0u);
927 }
928 }
929
930 WSL1_TEST_METHOD(Netlink)
931 {
932 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests netlink", L"Netlink"));
933 }
934
935 WSL1_TEST_METHOD(Random)
936 {
937 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests random", L"random"));
938 }
939
940 TEST_METHOD(Keymgmt)
941 {
942 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests keymgmt", L"Keymgmt"));
943 }
944
945 WSL1_TEST_METHOD(Shm)
946 {
947 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests shm", L"shm"));
948 }
949
950 WSL1_TEST_METHOD(Sem)
951 {
952 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sem", L"sem"));
953 }
954
955 WSL1_TEST_METHOD(Ttys)
956 {
957 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests ttys", L"Ttys"));
958 }
959
960 WSL1_TEST_METHOD(OverlayFs)
961 {
962 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests overlayfs", L"OverlayFs"));
963 }
964
965 TEST_METHOD(Auxv)
966 {
967 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests auxv", L"auxv"));
968 }
969
970 TEST_METHOD(WslInfo)
971 {
972 if (LxsstuVmMode())
973 {
974 // Ensure the `-n` option to not print newline works by validating newline counts.
975 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | wc -l | grep 1"), 0u);
976 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode -n | wc -l | grep 0"), 0u);
977
978 // Ensure various wslinfo functionally works as expected.
979 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'nat'"), 0u);
980
981 WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::None}));
982 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'none'"), 0u);
983
984 if (AreExperimentalNetworkingFeaturesSupported() && IsHyperVFirewallSupported())
985 {
986 config.Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
987 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'mirrored'"), 0u);
988 }
989
990 for (const auto enabled : {true, false})
991 {
992 config.Update(LxssGenerateTestConfig({.guiApplications = enabled}));
993
994 #ifdef WSL_DEV_INSTALL_PATH
995
996 VERIFY_ARE_EQUAL(
997 LxsstuLaunchWsl(std::format(L"wslinfo --msal-proxy-path | grep -iF $(wslpath '{}')", TEXT(WSL_DEV_INSTALL_PATH))), 0u);
998
999 #else
1000
1001 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --msal-proxy-path | grep -iF '/mnt/c/Program Files/WSL/msal.wsl.proxy.exe'"), 0u);
1002
1003 #endif
1004 }
1005 }
1006 else
1007 {
1008 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'wsl1'"), 0u);
1009 }
1010
1011 {
1012 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --version");
1013 VERIFY_ARE_EQUAL(out, std::format(L"{}\n", WSL_PACKAGE_VERSION));
1014 VERIFY_ARE_EQUAL(err, L"");
1015 }
1016
1017 {
1018 // Ensure the old version query command still works.
1019 const auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --wsl-version");
1020 VERIFY_ARE_EQUAL(out, std::format(L"{}\n", WSL_PACKAGE_VERSION));
1021 VERIFY_ARE_EQUAL(err, L"");
1022 }
1023
1024 {
1025 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --invalid", 1);
1026 VERIFY_ARE_EQUAL(out, L"");
1027 VERIFY_ARE_EQUAL(
1028 err,
1029 L"Invalid command line argument: --invalid\nPlease use 'wslinfo --help' to get a list of supported "
1030 L"arguments.\n");
1031 }
1032
1033 {
1034 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --vm-id -n");
1035 VERIFY_ARE_EQUAL(err, L"");
1036 if (LxsstuVmMode())
1037 {
1038 // Ensure that the response from wslinfo has the VM ID.
1039 auto guid = wsl::shared::string::ToGuid(out);
1040 VERIFY_IS_TRUE(guid.has_value());
1041 VERIFY_IS_FALSE(IsEqualGUID(guid.value(), GUID_NULL));
1042
1043 // Validate that the VM ID is not propagated to user commands.
1044 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"echo -n \"$WSL2_VM_ID\"");
1045 VERIFY_ARE_EQUAL(out, L"");
1046 VERIFY_ARE_EQUAL(err, L"");
1047 }
1048 else
1049 {
1050 VERIFY_ARE_EQUAL(out, L"wsl1");
1051 }
1052 }
1053 }
1054
1055 TEST_METHOD(FsTab)
1056 {
1057 //
1058 // Revert the fstab file and restart the instance so everything is back in
1059 // the default state after this test.
1060 //
1061
1062 auto cleanup = wil::scope_exit([&] {
1063 try
1064 {
1065 LxsstuLaunchWsl(LXSST_FSTAB_CLEANUP_COMMAND_LINE);
1066 TerminateDistribution();
1067 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0u);
1068 }
1069 catch (...)
1070 {
1071 LogError("Error while cleaning up the fstab");
1072 }
1073 });
1074
1075 //
1076 // Create an entry in the /etc/fstab file to explicitly mount C:.
1077 //
1078
1079 VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(LXSST_FSTAB_BACKUP_COMMAND_LINE));
1080 VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(LXSST_FSTAB_SETUP_COMMAND_LINE));
1081 TerminateDistribution();
1082 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0u);
1083
1084 //
1085 // The test will make sure /mnt/c is mounted with the options specified in
1086 // /etc/fstab, and that it's mounted only once.
1087 //
1088
1089 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fstab", L"fstab"));
1090 }
1091
1092 TEST_METHOD(X11SocketOverTmpMount)
1093 {
1094 if (!LxsstuVmMode())
1095 {
1096 return;
1097 }
1098
1099 auto cleanup = wil::scope_exit([&] {
1100 try
1101 {
1102 LxsstuLaunchWsl(LXSST_FSTAB_CLEANUP_COMMAND_LINE);
1103 TerminateDistribution();
1104 }
1105 catch (...)
1106 {
1107 LogError("Error while cleaning up the fstab");
1108 }
1109 });
1110
1111 WslConfigChange configChange(LxssGenerateTestConfig({.guiApplications = true}));
1112
1113 //
1114 // Create an entry in the /etc/fstab file to add a tmpfs over /tmp.
1115 //
1116
1117 VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(LXSST_FSTAB_BACKUP_COMMAND_LINE));
1118 VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(L"echo 'tmpfs /tmp tmpfs rw,nodev,nosuid,size=50M 0 0' > /etc/fstab"));
1119 TerminateDistribution();
1120
1121 auto ValidateBindMount = [](HANDLE Token) {
1122 //
1123 // Validate that the bind mount is present.
1124 //
1125
1126 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" mount | grep -iF 'none on /tmp/.X11-unix type tmpfs'", nullptr, nullptr, nullptr, Token), 0u);
1127 };
1128
1129 //
1130 // Verify that /tmp is mounted in both namespaces.
1131 //
1132
1133 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mount | grep -iF 'tmpfs on /tmp type tmpfs'", nullptr, nullptr, nullptr, nullptr), 0u);
1134
1135 const auto nonElevatedToken = GetNonElevatedToken();
1136 VERIFY_ARE_EQUAL(
1137 LxsstuLaunchWsl(L"mount | grep -iF 'tmpfs on /tmp type tmpfs'", nullptr, nullptr, nullptr, nonElevatedToken.get()), 0u);
1138
1139 //
1140 // Validate that the X11 bind mount is present and valid in both namespaces.
1141 //
1142
1143 ValidateBindMount(nullptr);
1144 ValidateBindMount(nonElevatedToken.get());
1145 }
1146
1147 TEST_METHOD(ImportDistro)
1148 {
1149 const auto tarFileName = LXSST_IMPORT_DISTRO_TEST_DIR L"test.tar";
1150 const auto rootfsDirectoryName = LXSST_IMPORT_DISTRO_TEST_DIR L"rootfs";
1151 const auto vhdFileName = LXSST_IMPORT_DISTRO_TEST_DIR L"ext4.vhdx";
1152 auto cleanup = wil::scope_exit([&] {
1153 try
1154 {
1155 VERIFY_IS_TRUE(DeleteFileW(tarFileName));
1156 VERIFY_IS_TRUE(RemoveDirectoryW(rootfsDirectoryName));
1157 VERIFY_IS_TRUE(DeleteFileW(vhdFileName));
1158 VERIFY_IS_TRUE(RemoveDirectoryW(LXSST_IMPORT_DISTRO_TEST_DIR));
1159 }
1160 catch (...)
1161 {
1162 LogError("Error during cleanup")
1163 }
1164 });
1165
1166 //
1167 // Create a dummy tar file, rootfs folder, and vhdx. These will be used
1168 // to ensure that the user cannot import a distribution over an existing one
1169 // even if distro registration registry keys are not present.
1170 //
1171
1172 VERIFY_IS_TRUE(CreateDirectoryW(LXSST_IMPORT_DISTRO_TEST_DIR, NULL));
1173 VERIFY_IS_TRUE(CreateDirectoryW(rootfsDirectoryName, NULL));
1174
1175 {
1176 const wil::unique_hfile tarFile{CreateFileW(
1177 tarFileName, GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)};
1178
1179 VERIFY_IS_FALSE(!tarFile);
1180
1181 const wil::unique_hfile vhdFile{CreateFileW(
1182 vhdFileName, GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)};
1183
1184 VERIFY_IS_FALSE(!vhdFile);
1185 }
1186
1187 auto validateOutput = [](LPCWSTR commandLine, const std::wstring& expectedOutput, DWORD expectedExitCode = -1) {
1188 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(commandLine, expectedExitCode);
1189 VERIFY_ARE_EQUAL(expectedOutput, out);
1190 VERIFY_ARE_EQUAL(L"", err);
1191 };
1192
1193 auto version = LxsstuVmMode() ? 2 : 1;
1194 auto commandLine = std::format(L"--import dummy {} {} --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, tarFileName, version);
1195 if (LxsstuVmMode())
1196 {
1197 validateOutput(
1198 commandLine.c_str(),
1199 FormatErrorMessage(
1200 std::format(L"Failed to create disk '{}ext4.vhdx': The file exists. ", LXSST_IMPORT_DISTRO_TEST_DIR),
1201 L"Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS"));
1202 }
1203 else
1204 {
1205 validateOutput(
1206 commandLine.c_str(), FormatErrorMessage(L"The file exists. ", L"Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS"));
1207 }
1208
1209 commandLine = std::format(L"--import dummy {} {} --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, vhdFileName, version);
1210 validateOutput(commandLine.c_str(), L"This looks like a VHD file. Use --vhd to import a VHD instead of a tar.\r\n");
1211
1212 if (!LxsstuVmMode())
1213 {
1214 commandLine = std::format(L"--import dummy {} {} --vhd --version 1", LXSST_IMPORT_DISTRO_TEST_DIR, vhdFileName);
1215 validateOutput(
1216 commandLine.c_str(),
1217 FormatErrorMessage(L"This operation is only supported by WSL2.", L"Wsl/Service/RegisterDistro/WSL_E_WSL2_NEEDED"));
1218 }
1219
1220 //
1221 // Verify that importing a distribution with a different name into the same path as an
1222 // already registered distribution (test_distro) returns the path-already-exists error.
1223 //
1224
1225 {
1226 const auto distroKey = OpenDistributionKey(LXSS_DISTRO_NAME_TEST_L);
1227 VERIFY_IS_TRUE(!!distroKey);
1228
1229 auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
1230 VERIFY_IS_FALSE(basePath.empty());
1231
1232 commandLine = std::format(L"--import path-conflict-distro \"{}\" \"{}\" --version {}", basePath, tarFileName, version);
1233 validateOutput(
1234 commandLine.c_str(),
1235 FormatErrorMessage(
1236 L"The supplied install location is already in use.", L"Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS"));
1237 }
1238
1239 //
1240 // Create and import a new distro that where /bin/sh is an absolute symlink.
1241 //
1242
1243 auto newDistroName = L"symlink_distro";
1244 auto newDistroTar = L"symlink_distro.tar";
1245 validateOutput(
1246 std::format(L"--export {} {}", LXSS_DISTRO_NAME_TEST_L, newDistroTar).c_str(),
1247 L"The operation completed successfully. \r\n",
1248 0);
1249
1250 auto deleteNewDistro = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1251 VERIFY_IS_TRUE(DeleteFileW(newDistroTar));
1252 LxsstuLaunchWsl(std::format(L"--unregister {}", newDistroName));
1253 });
1254
1255 validateOutput(
1256 std::format(L"--import {} . {} --version {}", newDistroName, newDistroTar, version).c_str(),
1257 L"The operation completed successfully. \r\n",
1258 0);
1259 validateOutput(std::format(L"-d {} -- ln -f -s /bin/bash /bin/sh", newDistroName).c_str(), L"", 0);
1260 validateOutput(
1261 std::format(L"--export {} {}", newDistroName, newDistroTar).c_str(), L"The operation completed successfully. \r\n", 0);
1262 validateOutput(std::format(L"--unregister {}", newDistroName).c_str(), L"The operation completed successfully. \r\n", 0);
1263 validateOutput(
1264 std::format(L"--import {} . {} --version {}", newDistroName, newDistroTar, version).c_str(),
1265 L"The operation completed successfully. \r\n",
1266 0);
1267 }
1268
1269 TEST_METHOD(ImportDistroInvalidTar)
1270 {
1271 const auto commandLine = std::format(
1272 L"--import dummy {} C:\\windows\\system32\\drivers\\etc\\hosts --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, LxsstuVmMode() ? 2 : 1);
1273
1274 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(commandLine.c_str(), -1);
1275
1276 VERIFY_ARE_EQUAL(
1277 out, FormatErrorMessage(L"Importing the distribution failed.", L"Wsl/Service/RegisterDistro/WSL_E_IMPORT_FAILED"));
1278
1279 constexpr auto expectedError = L"bsdtar: Error opening archive: Unrecognized archive format\n";
1280 if (LxsstuVmMode())
1281 {
1282 VERIFY_ARE_EQUAL(err, expectedError);
1283 }
1284 else
1285 {
1286 // lxcore.sys can close the stderr pipe before bsdtar has finished writing.
1287 VERIFY_IS_TRUE(std::wstring_view{expectedError}.starts_with(err));
1288 }
1289 }
1290
1291 TEST_METHOD(AppxDistroDeletion)
1292 {
1293 // Create a dummy distro registration
1294 const auto key = wsl::windows::common::registry::CreateKey(
1295 HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\{baa405ef-1822-4bbe-84e2-30e4c6330d41}");
1296
1297 wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"State", 1);
1298 wsl::windows::common::registry::WriteString(key.get(), nullptr, L"DistributionName", L"DistroToBeDeleted");
1299 wsl::windows::common::registry::WriteString(
1300 key.get(), nullptr, L"PackageFamilyName", L"Microsoft.AppThatIsntInstalledForSure.1.0.0.0_8wekyb3d8bbwe");
1301 wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"Version", 2);
1302
1303 const auto vhdDir = std::filesystem::current_path();
1304 wsl::windows::common::registry::WriteString(key.get(), nullptr, L"BasePath", vhdDir.c_str());
1305 wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"DefaultUid", 0);
1306 wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
1307
1308 // Create a dummy vhd
1309 const auto vhdPath = vhdDir.string() + "\\ext4.vhdx";
1310
1311 wil::unique_handle vhdHandle(CreateFileA(vhdPath.c_str(), GENERIC_READ, 0, nullptr, CREATE_ALWAYS, 0, nullptr));
1312 VERIFY_IS_TRUE(vhdHandle.is_valid());
1313 vhdHandle.reset();
1314
1315 wsl::windows::common::SvcComm service;
1316 auto isDistroListed = [&]() {
1317 auto distros = service.EnumerateDistributions();
1318
1319 return std::find_if(distros.begin(), distros.end(), [&](const auto& e) {
1320 return wsl::shared::string::IsEqual(e.DistroName, L"DistroToBeDeleted", false);
1321 }) != distros.end();
1322 };
1323
1324 // The distro should still be there, because the vhd exists.
1325 VERIFY_IS_TRUE(isDistroListed());
1326
1327 // Delete the VHD
1328 VERIFY_IS_TRUE(DeleteFileA(vhdPath.c_str()));
1329
1330 // Now the distro should be deleted.
1331 VERIFY_IS_FALSE(isDistroListed());
1332 }
1333
1334 // Validate that the default distribution is correctly displayed
1335 TEST_METHOD(DefaultDistro)
1336 {
1337 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--list");
1338
1339 VERIFY_IS_TRUE(out.find(std::format(L"{} (Default)", LXSS_DISTRO_NAME_TEST_L)) != std::wstring::npos);
1340 VERIFY_ARE_EQUAL(err, L"");
1341 }
1342
1343 // TODO: Add test coverage for the Linux => Windows code paths of $WSLENV
1344 TEST_METHOD(WslEnv)
1345 {
1346 auto validateEnv = [&](const std::map<std::wstring, std::wstring>& inputVariables,
1347 const std::map<std::wstring, std::wstring>& expectedOutput) {
1348 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1349 for (const auto& e : inputVariables)
1350 {
1351 THROW_LAST_ERROR_IF(!SetEnvironmentVariable(e.first.c_str(), nullptr));
1352 }
1353 });
1354
1355 for (const auto& e : inputVariables)
1356 {
1357 THROW_LAST_ERROR_IF(!SetEnvironmentVariable(e.first.c_str(), e.second.c_str()));
1358 }
1359
1360 for (const auto& e : expectedOutput)
1361 {
1362 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"echo -n $" + e.first);
1363
1364 VERIFY_ARE_EQUAL(e.second, output);
1365 }
1366 };
1367
1368 validateEnv({{L"a", L"b"}, {L"c", L"d"}, {L"WSLENV", L"a/u:c/u"}}, {{L"a", L"b"}, {L"c", L"d"}});
1369 validateEnv(
1370 {{L"a", L"C:\\Users"}, {L"b", L"C:\\Users"}, {L"WSLENV", L"a/l:b/p"}},
1371 {{L"a", L"/mnt/c/Users"}, {L"b", L"/mnt/c/Users"}});
1372
1373 validateEnv(
1374 {{L"a", L"C:\\Users;C:\\Windows"},
1375 {L"b", L"C:\\Users;C:\\Windows"},
1376 {L"c", L"C:\\Users;C:\\Windows"},
1377 {L"d", L"C:\\Users;C:\\Windows"},
1378 {L"WSLENV", L"a/l:b/p:c/pl:d/lp"}},
1379 {{L"a", L"/mnt/c/Users:/mnt/c/Windows"},
1380 {L"b", L"/mnt/c/Users:/mnt/c/Windows"},
1381 {L"c", L"/mnt/c/Users:/mnt/c/Windows"},
1382 {L"d", L"/mnt/c/Users:/mnt/c/Windows"}});
1383
1384 validateEnv(
1385 {{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"b", L"C:\\Users;C:\\Windows"}, {L"WSLENV", L"a/l:b/l:a/l"}},
1386 {{L"a", L"/mnt/c/Users:/mnt/c/Windows/System32"}, {L"b", L"/mnt/c/Users:/mnt/c/Windows"}});
1387
1388 validateEnv(
1389 {{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"b", L"C:\\Users;C:\\Windows"}, {L"WSLENV", L"a/u:b/u:a/u"}},
1390 {{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"b", L"C:\\Users;C:\\Windows"}});
1391
1392 validateEnv({{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"WSLENV", L"a/w"}}, {{L"a", L""}});
1393
1394 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
1395 THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"Empty", nullptr));
1396 THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"WSLENV", nullptr));
1397 });
1398
1399 THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"Empty", L""));
1400 THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"WSLENV", L"Empty/u"));
1401 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"[ -z ${Empty+x} ]"), (DWORD)1);
1402 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"[ -z ${SanityCheck+x} ]"), (DWORD)0);
1403 }
1404
1405 static void ValidateErrorMessage(
1406 const std::wstring& Cmd,
1407 const std::wstring& Message,
1408 const std::wstring& Code,
1409 const std::optional<std::wstring>& ExtraConfig = {},
1410 LPCWSTR EntryPoint = WSL_BINARY_NAME,
1411 bool ignoreCasing = false)
1412 {
1413 std::optional<std::wstring> previousConfig;
1414
1415 if (ExtraConfig.has_value())
1416 {
1417 previousConfig = LxssWriteWslConfig(L"[wsl2]\n" + ExtraConfig.value());
1418 RestartWslService();
1419 }
1420
1421 auto revertConfig = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1422 if (previousConfig.has_value())
1423 {
1424 LxssWriteWslConfig(previousConfig.value());
1425 RestartWslService();
1426 };
1427 });
1428
1429 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(
1430 Cmd.c_str(), wcscmp(EntryPoint, L"bash.exe") == 0 ? 1 : -1, nullptr, nullptr, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, EntryPoint);
1431
1432 const auto expectedOutput = FormatErrorMessage(Message, Code);
1433
1434 if (!wsl::shared::string::IsEqual(output, expectedOutput, ignoreCasing))
1435 {
1436 LogError("Expected error message: '%ls', actual error message: '%ls'", expectedOutput.c_str(), output.c_str());
1437 VERIFY_FAIL();
1438 }
1439 }
1440
1441 static void VerifyOutput(const std::wstring& Cmd, const std::wstring& ExpectedOutput, int ExpectedExitCode = 0, LPCWSTR EntryPoint = WSL_BINARY_NAME)
1442 {
1443 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(
1444 Cmd.c_str(), ExpectedExitCode, nullptr, nullptr, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, EntryPoint);
1445
1446 VERIFY_ARE_EQUAL(output, ExpectedOutput);
1447 }
1448
1449 static std::wstring ExpectedUsageMessage()
1450 {
1451 std::wstring expectedUsageMessage;
1452 for (auto e : wsl::shared::Localization::MessageWslUsage())
1453 {
1454 if (e == L'\n')
1455 {
1456 expectedUsageMessage += L'\r';
1457 }
1458
1459 expectedUsageMessage += e;
1460 }
1461
1462 return expectedUsageMessage + L"\r\n";
1463 }
1464
1465 static void VerifyInvalidUsage(const std::wstring& Cmd)
1466 {
1467 auto [output, error] = LxsstuLaunchWslAndCaptureOutput(Cmd.c_str(), -1);
1468 VERIFY_ARE_EQUAL(ExpectedUsageMessage(), output);
1469 VERIFY_ARE_EQUAL(error, L"");
1470 }
1471
1472 TEST_METHOD(ErrorMessages)
1473 {
1474 if (LxsstuVmMode()) // wsl --mount and bridged networking only exist in WSL2.
1475 {
1476 if (!wsl::shared::Arm64 && wsl::windows::common::helpers::GetWindowsVersion().BuildNumber >= 27653)
1477 {
1478 ValidateErrorMessage(
1479 L"--mount DoesNotExist",
1480 L"Failed to attach disk 'DoesNotExist' to WSL2: The system cannot find the file specified. ",
1481 L"Wsl/Service/AttachDisk/MountDisk/HCS/ERROR_FILE_NOT_FOUND");
1482 }
1483
1484 ValidateErrorMessage(
1485 L"--unmount DoesNotExist",
1486 GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)),
1487 L"Wsl/Service/DetachDisk/ERROR_FILE_NOT_FOUND");
1488
1489 ValidateErrorMessage(
1490 WSL_MANAGE_ARG L" " LXSS_DISTRO_NAME_TEST L" " WSL_MANAGE_ARG_SET_SPARSE_OPTION_LONG L" false_",
1491 L"false_ is not a valid boolean, <true|false>",
1492 L"Wsl/E_INVALIDARG");
1493
1494 const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
1495 {
1496 // Create a distro registration pointing to a vhdx that doesn't exist and validate that the error message reports that correctly.
1497
1498 const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
1499 const auto distroKey =
1500 wsl::windows::common::registry::CreateKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
1501 auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
1502 wsl::windows::common::registry::DeleteKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
1503 });
1504
1505 wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"BasePath", L"C:\\DoesNotExit");
1506 wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"DistributionName", L"DummyBrokenDistro");
1507 wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"DefaultUid", 0);
1508 wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Version", LXSS_DISTRO_VERSION_2);
1509 wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"State", LxssDistributionStateInstalled);
1510 wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
1511
1512 ValidateErrorMessage(
1513 L"-d DummyBrokenDistro",
1514 L"Failed to attach disk 'C:\\DoesNotExit\\ext4.vhdx' to WSL2: The system cannot find the path "
1515 L"specified. ",
1516 L"Wsl/Service/CreateInstance/MountDisk/ERROR_PATH_NOT_FOUND");
1517
1518 // Purposefully set an incorrect value type to validate registry error handling.
1519 wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"Version", L"Broken");
1520
1521 const auto tokenInfo = wil::get_token_information<TOKEN_USER>();
1522 const auto Sid = std::wstring(wsl::windows::common::wslutil::SidToString(tokenInfo->User.Sid).get());
1523
1524 // N.B. casing is ignored because the 'Software' key is sometimes uppercase, sometimes not.
1525 ValidateErrorMessage(
1526 L"-d DummyBrokenDistro",
1527 L"An error occurred accessing the registry. Path: '\\REGISTRY\\USER\\" + Sid +
1528 L"\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\{baa405ef-1822-4bbe-84e2-30e4c6330d42}"
1529 L"\\Version'. Error: Data of this type is not supported. ",
1530 L"Wsl/Service/ReadDistroConfig/ERROR_UNSUPPORTED_TYPE",
1531 {},
1532 L"wsl.exe",
1533 true);
1534 }
1535
1536 ValidateErrorMessage(
1537 L"echo ok",
1538 std::format(L"Invalid mac address 'foo' for key 'wsl2.macAddress' in {}:2", wslConfigPath),
1539 L"Wsl/Service/CreateInstance/CreateVm/ParseConfig/E_INVALIDARG",
1540 L"macAddress=foo");
1541 }
1542 else
1543 {
1544 // wsl.exe --manage --resize requires WSL2.
1545 ValidateErrorMessage(
1546 L"--manage test_distro --resize 10GB",
1547 L"This operation is only supported by WSL2.",
1548 L"Wsl/Service/WSL_E_WSL2_NEEDED");
1549
1550 // wsl.exe --manage --compact requires WSL2.
1551 ValidateErrorMessage(
1552 L"--manage test_distro --compact", L"This operation is only supported by WSL2.", L"Wsl/Service/WSL_E_WSL2_NEEDED");
1553 }
1554
1555 ValidateErrorMessage(
1556 L"--import a b c", GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)), L"Wsl/ERROR_FILE_NOT_FOUND");
1557
1558 ValidateErrorMessage(
1559 L"-d DoesNotExist echo foo",
1560 L"There is no distribution with the supplied name.",
1561 L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND");
1562
1563 ValidateErrorMessage(
1564 L"--export DoesNotExist FileName",
1565 L"There is no distribution with the supplied name.",
1566 L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND");
1567
1568 ValidateErrorMessage(
1569 L"--import-in-place DoesNotExist FileName",
1570 GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)),
1571 L"Wsl/ERROR_FILE_NOT_FOUND");
1572
1573 ValidateErrorMessage(
1574 L"--set-default-version 3",
1575 GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_VERSION_PARSE_ERROR)),
1576 L"Wsl/ERROR_VERSION_PARSE_ERROR");
1577
1578 ValidateErrorMessage(
1579 L"--manage DoesNotExist --resize 10GB",
1580 L"There is no distribution with the supplied name.",
1581 L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND");
1582
1583 ValidateErrorMessage(L"--manage test_distro --resize foo", L"Invalid size: foo", L"Wsl/E_INVALIDARG");
1584
1585 ValidateErrorMessage(
1586 L"--install --distribution debian --no-distribution",
1587 L"Arguments --no-distribution and --distribution can't be specified at same time.",
1588 L"Wsl/E_INVALIDARG");
1589
1590 ValidateErrorMessage(
1591 L"--install debian --from-file foo --distribution foo",
1592 L"Arguments --from-file and --distribution can't be specified at same time.",
1593 L"Wsl/E_INVALIDARG");
1594
1595 ValidateErrorMessage(
1596 L"--install foo --fixed-vhd", L"Argument --fixed-vhd requires the --vhd-size argument.", L"Wsl/E_INVALIDARG");
1597
1598 {
1599 UniqueWebServer server(c_testDistributionEndpoint, c_testDistributionJson);
1600 RegistryKeyChange<std::wstring> keyChange(
1601 HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::distribution::c_distroUrlRegistryValue, c_testDistributionEndpoint);
1602 ValidateErrorMessage(
1603 L"--install -d DoesNotExist",
1604 L"Invalid distribution name: 'DoesNotExist'.\r\nTo get a list of valid distributions, use 'wsl.exe --list "
1605 L"--online'.",
1606 L"Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND");
1607 }
1608
1609 {
1610 const auto lxssKey = wsl::windows::common::registry::OpenLxssMachineKey(KEY_READ | KEY_SET_VALUE);
1611 std::optional<std::wstring> revertValue;
1612
1613 try
1614 {
1615 revertValue = wsl::windows::common::registry::ReadString(
1616 lxssKey.get(), nullptr, wsl::windows::common::distribution::c_distroUrlRegistryValue);
1617 }
1618 catch (...)
1619 {
1620 // Expected if the value isn't set
1621 }
1622
1623 auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1624 if (revertValue.has_value())
1625 {
1626 wsl::windows::common::registry::WriteString(
1627 lxssKey.get(), nullptr, wsl::windows::common::distribution::c_distroUrlRegistryValue, revertValue->c_str());
1628 }
1629 else
1630 {
1631 wsl::windows::common::registry::DeleteValue(lxssKey.get(), wsl::windows::common::distribution::c_distroUrlRegistryValue);
1632 }
1633 });
1634
1635 wsl::windows::common::registry::WriteString(
1636 lxssKey.get(), nullptr, wsl::windows::common::distribution::c_distroUrlRegistryValue, L"http://127.0.0.1:6666");
1637
1638 ValidateErrorMessage(
1639 L"--install -d ubuntu",
1640 L"Failed to fetch the distribution list from 'http://127.0.0.1:6666'. " +
1641 GetSystemErrorString(HRESULT_FROM_WIN32(WININET_E_CANNOT_CONNECT)),
1642 L"Wsl/InstallDistro/WININET_E_CANNOT_CONNECT");
1643
1644 ValidateErrorMessage(
1645 L"--list --online",
1646 L"Failed to fetch the distribution list from 'http://127.0.0.1:6666'. " +
1647 GetSystemErrorString(HRESULT_FROM_WIN32(WININET_E_CANNOT_CONNECT)),
1648 L"Wsl/WININET_E_CANNOT_CONNECT");
1649 }
1650
1651 ValidateErrorMessage(
1652 L"/u foo",
1653 L"There is no distribution with the supplied name.",
1654 L"WslConfig/Service/WSL_E_DISTRO_NOT_FOUND",
1655 {},
1656 L"wslconfig.exe");
1657
1658 ValidateErrorMessage(
1659 L"e7bef681-c148-4687-8a0f-8c8be93bac93", // GUID for a distro that's not installed.
1660 L"There is no distribution with the supplied name.",
1661 L"Bash/Service/CreateInstance/ReadDistroConfig/WSL_E_DISTRO_NOT_FOUND",
1662 {},
1663 L"bash.exe");
1664
1665 VerifyOutput(L"--install --no-distribution", L"The operation completed successfully. \r\n");
1666
1667 VerifyInvalidUsage(L"--manage --move .");
1668 }
1669
1670 TEST_METHOD(CommandLineParsing)
1671 {
1672 VerifyOutput(L"echo -n \\\"", L"\"");
1673 VerifyOutput(L"echo -n \\\'", L"\'");
1674 VerifyOutput(L"echo -n \" \"", L" ");
1675 VerifyOutput(L"echo -n $USER", L"root");
1676 VerifyOutput(L"echo -n \"$USER\"", L"root");
1677 VerifyOutput(L"echo -n '\"$USER\"'", L"\"$USER\"");
1678 VerifyOutput(L"echo -n '\\\"$USER\\\"'", L"\\\"$USER\\\"");
1679 VerifyOutput(L"echo -n '$USER'", L"$USER");
1680 VerifyOutput(L"echo -n a \" \" b", L"a b");
1681 VerifyOutput(L"echo -n a \"\" b", L"a b");
1682 VerifyOutput(L"echo -n a b \"\"", L"a b ");
1683 VerifyOutput(L"echo -n \"a\"\"b\"", L"ab");
1684
1685 VerifyOutput(L"--exec echo -n \"a\"", L"a");
1686 VerifyOutput(L"--exec echo -n $USER", L"$USER");
1687 VerifyOutput(L"--exec echo -n \\\"a\\\"", L"\"a\"");
1688 VerifyOutput(L"--exec echo -n \\\"a\\\"", L"\"a\"");
1689 VerifyOutput(L"--exec echo -n \"a\"\"b\"", L"a\"b");
1690 VerifyOutput(L"--exec echo -n \\\"", L"\"");
1691 }
1692
1693 TEST_METHOD(ManageInvalidUsage)
1694 {
1695 VerifyInvalidUsage(L"--manage " LXSS_DISTRO_NAME_TEST_L L" --compact --resize 10GB");
1696 }
1697
1698 // This test validates that the help messages for wsl.exe and wsl.config are correctly displayed.
1699 // Notes:
1700 // - This test will fail if the help messages are changed. If that's the case, simply update the below strings
1701 // - This test assumes that English is the configured language.
1702 TEST_METHOD(UsageMessages)
1703 {
1704 const std::wstring WslHelpMessage =
1705 LR"""(Copyright (c) Microsoft Corporation. All rights reserved.
1706 For privacy information about this product please visit https://aka.ms/privacy.
1707
1708 Usage: wsl.exe [Argument] [Options...] [CommandLine]
1709
1710 Arguments for running Linux binaries:
1711
1712 If no command line is provided, wsl.exe launches the default shell.
1713
1714 --exec, -e <CommandLine>
1715 Execute the specified command without using the default Linux shell.
1716
1717 --shell-type <standard|login|none>
1718 Execute the specified command with the provided shell type.
1719
1720 --
1721 Pass the remaining command line as-is.
1722
1723 Options:
1724 --cd <Directory>
1725 Sets the specified directory as the current working directory.
1726 If ~ is used the Linux user's home path will be used. If the path begins
1727 with a / character, it will be interpreted as an absolute Linux path.
1728 Otherwise, the value must be an absolute Windows path.
1729
1730 --distribution, -d <DistroName>
1731 Run the specified distribution.
1732
1733 --distribution-id <DistroGuid>
1734 Run the specified distribution ID.
1735
1736 --user, -u <UserName>
1737 Run as the specified user.
1738
1739 --system
1740 Launches a shell for the system distribution.
1741
1742 Arguments for managing Windows Subsystem for Linux:
1743
1744 --help
1745 Display usage information.
1746
1747 --debug-shell
1748 Open a WSL2 debug shell for diagnostics purposes.
1749
1750 --install [Distro] [Options...]
1751 Install a Windows Subsystem for Linux distribution.
1752 For a list of valid distributions, use 'wsl.exe --list --online'.
1753
1754 Options:
1755 --enable-wsl1
1756 Enable WSL1 support.
1757
1758 --fixed-vhd
1759 Create a fixed-size disk to store the distribution.
1760
1761 --from-file <Path>
1762 Install a distribution from a local file.
1763
1764 --legacy
1765 Use the legacy distribution manifest.
1766
1767 --location <Location>
1768 Set the install path for the distribution.
1769
1770 --name <Name>
1771 Set the name of the distribution.
1772
1773 --no-distribution
1774 Only install the required optional components, does not install a distribution.
1775
1776 --no-launch, -n
1777 Do not launch the distribution after install.
1778
1779 --version <Version>
1780 Specifies the version to use for the new distribution.
1781
1782 --vhd-size <MemoryString>
1783 Specifies the size of the disk to store the distribution.
1784
1785 --web-download
1786 Download the distribution from the internet instead of the Microsoft Store.
1787
1788 --manage <Distro> <Options...>
1789 Changes distro specific options.
1790
1791 Options:
1792 --move <Location>
1793 Move the distribution to a new location.
1794
1795 --set-sparse, -s <true|false>
1796 Set the VHD of distro to be sparse, allowing disk space to be automatically reclaimed.
1797
1798 --set-default-user <Username>
1799 Set the default user of the distribution.
1800
1801 --resize <MemoryString>
1802 Resize the disk of the distribution to the specified size.
1803
1804 --compact
1805 Compact the VHDX file of a WSL 2 distribution.
1806
1807 --mount <Disk>
1808 Attaches and mounts a physical or virtual disk in all WSL 2 distributions.
1809
1810 Options:
1811 --vhd
1812 Specifies that <Disk> refers to a virtual hard disk.
1813
1814 --bare
1815 Attach the disk to WSL2, but don't mount it.
1816
1817 --name <Name>
1818 Mount the disk using a custom name for the mountpoint.
1819
1820 --type <Type>
1821 Filesystem to use when mounting a disk, if not specified defaults to ext4.
1822
1823 --options <Options>
1824 Additional mount options.
1825
1826 --partition <Index>
1827 Index of the partition to mount, if not specified defaults to the whole disk.
1828
1829 --set-default-version <Version>
1830 Changes the default install version for new distributions.
1831
1832 --shutdown
1833 Immediately terminates all running distributions and the WSL 2
1834 lightweight utility virtual machine.
1835
1836 Options:
1837 --force
1838 Terminate the WSL 2 virtual machine even if an operation is in progress. Can cause data loss.
1839
1840 --status
1841 Show the status of Windows Subsystem for Linux.
1842
1843 --unmount [Disk]
1844 Unmounts and detaches a disk from all WSL2 distributions.
1845 Unmounts and detaches all disks if called without argument.
1846
1847 --uninstall
1848 Uninstalls the Windows Subsystem for Linux package from this machine.
1849
1850 --update
1851 Update the Windows Subsystem for Linux package.
1852
1853 Options:
1854 --pre-release
1855 Download a pre-release version if available.
1856
1857 --version, -v
1858 Display version information.
1859
1860 Arguments for managing distributions in Windows Subsystem for Linux:
1861
1862 --export <Distro> <FileName> [Options]
1863 Exports the distribution to a tar file.
1864 The filename can be - for stdout.
1865
1866 Options:
1867 --format <Format>
1868 Specifies the export format. Supported values: tar, tar.gz, tar.xz, vhd.
1869
1870 --import <Distro> <InstallLocation> <FileName> [Options]
1871 Imports the specified tar file as a new distribution.
1872 The filename can be - for stdin.
1873
1874 Options:
1875 --version <Version>
1876 Specifies the version to use for the new distribution.
1877
1878 --vhd
1879 Specifies that the provided file is a .vhd or .vhdx file, not a tar file.
1880 This operation makes a copy of the VHD file at the specified install location.
1881
1882 --import-in-place <Distro> <FileName>
1883 Imports the specified VHD file as a new distribution.
1884 This virtual hard disk must be formatted with the ext4 filesystem type.
1885
1886 --list, -l [Options]
1887 Lists distributions.
1888
1889 Options:
1890 --all
1891 List all distributions, including distributions that are
1892 currently being installed or uninstalled.
1893
1894 --running
1895 List only distributions that are currently running.
1896
1897 --quiet, -q
1898 Only show distribution names.
1899
1900 --verbose, -v
1901 Show detailed information about all distributions.
1902
1903 --online, -o
1904 Displays a list of available distributions for install with 'wsl.exe --install'.
1905
1906 --set-default, -s <Distro>
1907 Sets the distribution as the default.
1908
1909 --set-version <Distro> <Version>
1910 Changes the version of the specified distribution.
1911
1912 --terminate, -t <Distro>
1913 Terminates the specified distribution.
1914
1915 --unregister <Distro>
1916 Unregisters the distribution and deletes the root filesystem.
1917 )""";
1918
1919 const std::wstring WslConfigHelpMessage =
1920 LR"""(Performs administrative operations on Windows Subsystem for Linux
1921
1922 Usage:
1923 /l, /list [Option]
1924 Lists registered distributions.
1925 /all - Optionally list all distributions, including distributions that
1926 are currently being installed or uninstalled.
1927
1928 /running - List only distributions that are currently running.
1929
1930 /s, /setdefault <DistributionName>
1931 Sets the distribution as the default.
1932
1933 /t, /terminate <DistributionName>
1934 Terminates the distribution.
1935
1936 /u, /unregister <DistributionName>
1937 Unregisters the distribution and deletes the root filesystem.
1938 )""";
1939
1940 auto AddCrlf = [](const std::wstring& Input) {
1941 std::wstring MessageWithCrlf;
1942
1943 for (const auto e : Input)
1944 {
1945 if (e == '\n')
1946 {
1947 MessageWithCrlf += '\r';
1948 }
1949 MessageWithCrlf += e;
1950 }
1951
1952 return MessageWithCrlf;
1953 };
1954
1955 // Note: There is no easy way to validate wslg's help message, since it displays a blocking
1956 // message box before exiting.
1957
1958 VerifyOutput(L"--help", AddCrlf(WslHelpMessage), -1);
1959 VerifyOutput(L"--help", AddCrlf(WslConfigHelpMessage), -1, L"wslconfig.exe");
1960
1961 UniqueWebServer server(c_testDistributionEndpoint, c_testDistributionJson);
1962 RegistryKeyChange<std::wstring> keyChange(
1963 HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::distribution::c_distroUrlRegistryValue, c_testDistributionEndpoint);
1964
1965 VerifyOutput(
1966 L"--install foo",
1967 FormatErrorMessage(
1968 L"Invalid distribution name: 'foo'.\r\nTo get a list of valid distributions, use 'wsl.exe --list --online'.",
1969 L"Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND"),
1970 -1);
1971 }
1972
1973 WSL2_TEST_METHOD(TestExistingSwapVhd)
1974 {
1975 // Create a 100MB swap vhdx.
1976 auto swapVhd = wil::GetCurrentDirectoryW<std::wstring>() + L"\\TestSwap.vhdx";
1977
1978 VIRTUAL_STORAGE_TYPE storageType{};
1979 storageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
1980 storageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT;
1981
1982 CREATE_VIRTUAL_DISK_PARAMETERS createVhdParameters{};
1983 createVhdParameters.Version = CREATE_VIRTUAL_DISK_VERSION_2;
1984 createVhdParameters.Version2.BlockSizeInBytes = 1024 * 1024;
1985 createVhdParameters.Version2.MaximumSize = 100 * 1024 * 1024;
1986
1987 wil::unique_hfile vhd{};
1988 VERIFY_ARE_EQUAL(
1989 ::CreateVirtualDisk(
1990 &storageType, swapVhd.c_str(), VIRTUAL_DISK_ACCESS_NONE, nullptr, CREATE_VIRTUAL_DISK_FLAG_SUPPORT_COMPRESSED_VOLUMES, 0, &createVhdParameters, nullptr, &vhd),
1991 0l);
1992
1993 vhd.reset();
1994
1995 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1996 WslShutdown();
1997 DeleteFile(swapVhd.c_str());
1998 });
1999
2000 // Update .wslconfig. Update the swapVhd path to replace single backslash
2001 // with double backslashes so as to be compatible with .wslconfig parsing.
2002 // The following regex replacement only works as intended if the path contains
2003 // single backslashes. Negative lookahead can be used to handle paths with double
2004 // backslashes but then the negative lookbehind case should also be used but the
2005 // latter is not supported in std::regex.
2006 swapVhd = std::regex_replace(swapVhd, std::wregex(L"\\\\"), L"\\\\");
2007 WslConfigChange configChange(LxssGenerateTestConfig() + L"\nswap=256MB\nswapFile=" + swapVhd);
2008
2009 auto validateSwapSize = [](LPCWSTR Expected) {
2010 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"swapon | awk 'END {print $3}'");
2011
2012 VERIFY_ARE_EQUAL(Expected + std::wstring(L"\n"), output);
2013 };
2014
2015 validateSwapSize(L"256M");
2016
2017 // Validate that the vhdx is resized correctly if the swap size changes
2018 configChange.Update(LxssGenerateTestConfig() + L"\nswap=200MB\nswapFile=" + swapVhd);
2019 validateSwapSize(L"200M");
2020 }
2021
2022 TEST_METHOD(InitDoesntBlockSignals)
2023 {
2024 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"grep -iF SigBlk < /proc/1/status");
2025 VERIFY_ARE_EQUAL(L"SigBlk:\t0000000000000000\n", output);
2026 }
2027
2028 WSL2_TEST_METHOD(InitReadonly)
2029 {
2030 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" grep '^rootfs /init rootfs ro,' /proc/self/mounts", nullptr, nullptr, nullptr, nullptr), 0u);
2031 }
2032
2033 WSL2_TEST_METHOD(GpuMounts)
2034 {
2035 auto ValidateGpuMounts = [](HANDLE Token) {
2036 VERIFY_ARE_EQUAL(
2037 LxsstuLaunchWsl(
2038 L"mount | grep -iF 'none on /usr/lib/wsl/lib type overlay (rw,nosuid,nodev,noatime,lowerdir=/gpu_" TEXT(LXSS_GPU_PACKAGED_LIB_SHARE) L":/gpu_" TEXT(
2039 LXSS_GPU_INBOX_LIB_SHARE) L",upperdir=/gpu_lib/rw/upper,workdir=/gpu_lib/rw/work,uuid=on)'",
2040 nullptr,
2041 nullptr,
2042 nullptr,
2043 Token),
2044 0u);
2045
2046 // Ensure the lib directory is writable.
2047 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" touch /usr/lib/wsl/lib/foo && rm /usr/lib/wsl/lib/foo", nullptr, nullptr, nullptr, Token), 0u);
2048
2049 VERIFY_ARE_EQUAL(
2050 LxsstuLaunchWsl(
2051 L"mount | grep -iF '" TEXT(
2052 LXSS_GPU_DRIVERS_SHARE) L" on /usr/lib/wsl/drivers type 9p (ro,nosuid,nodev,noatime,aname=" TEXT(LXSS_GPU_DRIVERS_SHARE) L";fmask=222;dmask=222,cache=0x5,access=client,msize=65536,trans=fd,rfd=8,wfd=8)'",
2053 nullptr,
2054 nullptr,
2055 nullptr,
2056 Token),
2057 0u);
2058 };
2059
2060 auto cleanUp = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
2061
2062 // Validate that GPU mounts are present in both namespaces.
2063 const auto nonElevatedToken = GetNonElevatedToken();
2064 WslShutdown();
2065 ValidateGpuMounts(nullptr);
2066 ValidateGpuMounts(nonElevatedToken.get());
2067
2068 // Create a new instance with a non-elevated token as the creator.
2069 WslShutdown();
2070 ValidateGpuMounts(nonElevatedToken.get());
2071 ValidateGpuMounts(nullptr);
2072 }
2073
2074 TEST_METHOD(InteropCornerCases)
2075 {
2076 auto validateInterop = [](const std::wstring& binaryName) {
2077 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(L"rm /tmp/'" + binaryName + L"'"); });
2078
2079 // The "|| echo fail" part is needed because bash will exec instead of forking() of only one non-builtin command is passed.
2080 // If bash exec's then this test is useless since the binfmt interpreter would not be a child of a process with a weird name.
2081
2082 const std::wstring commandLine =
2083 L"cp /bin/bash /tmp/'" + binaryName + L"' && '/tmp/" + binaryName +
2084 L"' -c 'export WSL_INTEROP=\"\" && echo -n $WSL_INTEROP && cmd.exe /c \"echo ok\" || echo fail'";
2085 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(commandLine);
2086
2087 VERIFY_ARE_EQUAL(output, L"ok\r\n");
2088 };
2089
2090 validateInterop(L"bash with spaces");
2091 validateInterop(L"bash )");
2092 validateInterop(L"bash (");
2093 validateInterop(L"(bash)");
2094 validateInterop(L"(bash(");
2095 validateInterop(L"()");
2096 validateInterop(L"(");
2097 validateInterop(L")");
2098 }
2099
2100 TEST_METHOD(InteropPid1)
2101 {
2102 // Validate that interop works as pid 1.
2103 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"unshare -pf --wd $(dirname $(which cmd.exe)) cmd.exe /c echo ok");
2104 VERIFY_ARE_EQUAL(output, L"ok\r\n");
2105 }
2106
2107 TEST_METHOD(Hostname)
2108 {
2109 auto cleanup = wil::scope_exit([] {
2110 LxsstuLaunchWsl(LXSST_REMOVE_DISTRO_CONF_COMMAND_LINE);
2111
2112 TerminateDistribution();
2113 });
2114
2115 auto validate = [](const std::string& input, const std::wstring& expectedOutput) {
2116 LxssWriteWslDistroConfig("[network]\nhostname=" + input);
2117 TerminateDistribution();
2118
2119 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"hostname");
2120 VERIFY_ARE_EQUAL(output, expectedOutput + L"\n");
2121
2122 output = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/hostname").first;
2123 VERIFY_ARE_EQUAL(output, expectedOutput + L"\n");
2124 };
2125
2126 validate("SimpleHostname", L"SimpleHostname");
2127 validate("Simple-Hostname", L"Simple-Hostname");
2128 validate("Simple_Hostname", L"SimpleHostname");
2129 validate("-hostname", L"hostname");
2130 validate("--hostname", L"hostname");
2131 validate("hostname.-", L"hostname");
2132 validate(".hostname", L"hostname");
2133 validate("hostname.", L"hostname");
2134 validate("host.name.", L"host.name");
2135 validate("host..name", L"host.name");
2136 validate("host|name", L"hostname");
2137 validate(".a-", L"a");
2138 validate(".a-b", L"a-b");
2139 validate(".", L"localhost");
2140 validate("-", L"localhost");
2141 validate("-.-", L"localhost");
2142 // Validate hostname is limited to 64 characters.
2143 const std::string longHostName("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz");
2144 validate(longHostName, wsl::shared::string::MultiByteToWide(longHostName.substr(0, 64)));
2145 }
2146
2147 WSL2_TEST_METHOD(WslConfWarnings)
2148 {
2149 DistroFileChange configChange(L"/etc/wsl.conf", false);
2150
2151 auto validateWarnings = [&configChange](const std::wstring& config, const std::wstring& expectedWarnings) {
2152 configChange.SetContent(config.c_str());
2153
2154 TerminateDistribution();
2155
2156 // This loop is here because of a race condition when starting WSL to get the warnings.
2157 // If a p9rdr distribution startup notification arrives just before wsl.exe calls CreateInstance(),
2158 // the warnings will be 'consumed' before wsl.exe can read them.
2159 // To work around that, loop for up to 2 minutes while we don't get any warnings
2160
2161 const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
2162
2163 while (std::chrono::steady_clock::now() < deadline)
2164 {
2165 auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"-u root echo ok");
2166 VERIFY_ARE_EQUAL(L"ok\n", output);
2167
2168 if (!warnings.empty() || expectedWarnings.empty())
2169 {
2170 VERIFY_ARE_EQUAL(expectedWarnings, warnings);
2171 return;
2172 }
2173
2174 LogInfo("Received empty warnings, trying again");
2175 WslShutdown();
2176 }
2177
2178 LogError("Timed out waiting for warnings. Expected warnings: %ls", expectedWarnings.c_str());
2179 VERIFY_FAIL();
2180 };
2181
2182 validateWarnings(L"[foo]\na=b", L"wsl: Unknown key 'foo.a' in /etc/wsl.conf:2\r\n");
2183 validateWarnings(L"a=a\\m", L"wsl: Invalid escaped character: 'm' in /etc/wsl.conf:1\r\n");
2184 validateWarnings(L"[=b", L"wsl: Invalid section name in /etc/wsl.conf:1\r\n");
2185 validateWarnings(L"\r\n\r\n[foo]\r\na=b", L"wsl: Unknown key 'foo.a' in /etc/wsl.conf:5\r\n");
2186
2187 // Validate that CRLF is correctly handled
2188 {
2189 configChange.SetContent(L"[network]\r\nhostname=foo\r\n");
2190 TerminateDistribution();
2191
2192 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"hostname");
2193 VERIFY_ARE_EQUAL(out, L"foo\n");
2194 VERIFY_ARE_EQUAL(err, L"");
2195 }
2196 }
2197
2198 WSL2_TEST_METHOD(Warnings)
2199 {
2200 WslConfigChange configChange(LxssGenerateTestConfig());
2201
2202 auto validateWarnings = [&configChange](
2203 const std::wstring& config,
2204 const std::wstring& expectedWarnings,
2205 const std::wstring& prefix = LxssGenerateTestConfig(),
2206 bool fnmatch = false) {
2207 WEX::Logging::Log::Comment(config.c_str());
2208 WEX::Logging::Log::Comment(expectedWarnings.c_str());
2209 configChange.Update(prefix + config);
2210
2211 // This loop is here because of a race condition when starting WSL to get the warnings.
2212 // If a p9rdr distribution startup notification arrives just before wsl.exe calls CreateInstance(),
2213 // the warnings will be 'consumed' before wsl.exe can read them.
2214 // To work around that, loop for up to 2 minutes while we don't get any warnings
2215
2216 const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
2217
2218 while (std::chrono::steady_clock::now() < deadline)
2219 {
2220 auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
2221 VERIFY_ARE_EQUAL(L"ok\n", output);
2222
2223 if (!warnings.empty() || expectedWarnings.empty())
2224 {
2225 if (fnmatch)
2226 {
2227 if (!PathMatchSpec(warnings.c_str(), expectedWarnings.c_str()))
2228 {
2229 LogError("Warning '%ls' didn't match pattern '%ls'", warnings.c_str(), expectedWarnings.c_str());
2230 VERIFY_FAIL();
2231 }
2232 }
2233 else
2234 {
2235 VERIFY_ARE_EQUAL(expectedWarnings, warnings);
2236 }
2237 return;
2238 }
2239
2240 LogInfo("Received empty warnings, trying again");
2241 WslShutdown();
2242 }
2243
2244 LogError("Timed out waiting for warnings. Expected warnings: %ls", expectedWarnings.c_str());
2245 VERIFY_FAIL();
2246 };
2247
2248 const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
2249
2250 validateWarnings(L"a=b", std::format(L"wsl: Unknown key 'wsl2.a' in {}:21\r\n", wslConfigPath));
2251 validateWarnings(L"[=b", std::format(L"wsl: Invalid section name in {}:21\r\n", wslConfigPath));
2252
2253 validateWarnings(
2254 L"dhcpTimeout=NotANumber",
2255 std::format(L"wsl: Invalid integer value 'NotANumber' for key 'wsl2.dhcpTimeout' in {}:21\r\n", wslConfigPath));
2256
2257 validateWarnings(L"ipv6=NotABoolean", std::format(L"wsl: Invalid boolean value 'NotABoolean' for key 'wsl2.ipv6' in {}:21\r\n", wslConfigPath));
2258
2259 validateWarnings(L"[sectionNotComplete", std::format(L"wsl: Expected ']' in {}:21\r\n", wslConfigPath));
2260 validateWarnings(L"NoEqual", std::format(L"wsl: Expected '=' in {}:21\r\n", wslConfigPath));
2261 validateWarnings(
2262 L"networkingMode=InvalidMode",
2263 std::format(L"wsl: Invalid value 'InvalidMode' for config key 'wsl2.networkingMode' in {}:2 (Valid values: Bridged, Consomme, Mirrored, Nat, None, VirtioProxy)\r\n", wslConfigPath),
2264 L"[wsl2]\n");
2265 validateWarnings(
2266 L"networkingMode=a\\m", std::format(L"wsl: Invalid escaped character: 'm' in {}:2\r\n", wslConfigPath), L"[wsl2]\n");
2267
2268 validateWarnings(
2269 L"\nswap=200MB\nswapFile=C:\\\\DoesNotExist\\\\swap.vhdx",
2270 L"wsl: Failed to create the swap disk in 'C:\\DoesNotExist\\swap.vhdx': The system cannot find the path "
2271 L"specified. \r\n");
2272
2273 validateWarnings(L"\nswap=/", std::format(L"wsl: Invalid memory string '/' for .wslconfig entry 'wsl2.swap' in {}:22\r\n", wslConfigPath));
2274 validateWarnings(L"\nswap=0GB", L"");
2275 validateWarnings(L"\nswap=0foo", std::format(L"wsl: Invalid memory string '0foo' for .wslconfig entry 'wsl2.swap' in {}:22\r\n", wslConfigPath));
2276 validateWarnings(L"safeMode=true", L"wsl: SAFE MODE ENABLED - many features will be disabled\r\n", L"[wsl2]\n");
2277 validateWarnings(L"processors=", std::format(L"wsl: Invalid integer value '' for key 'wsl2.processors' in {}:21\r\n", wslConfigPath));
2278 validateWarnings(L"memory=", std::format(L"wsl: Invalid memory string '' for .wslconfig entry 'wsl2.memory' in {}:21\r\n", wslConfigPath));
2279 validateWarnings(L"debugConsole=", std::format(L"wsl: Invalid boolean value '' for key 'wsl2.debugConsole' in {}:21\r\n", wslConfigPath));
2280 validateWarnings(
2281 L"networkingMode=",
2282 std::format(L"wsl: Invalid value '' for config key 'wsl2.networkingMode' in {}:21 (Valid values: Bridged, Consomme, Mirrored, Nat, None, VirtioProxy)\r\n", wslConfigPath));
2283
2284 validateWarnings(
2285 L"ipv6=true\nipv6=false",
2286 std::format(L"wsl: Duplicated config key 'wsl2.ipv6' in {}:22 (Conflicting key: 'wsl2.ipv6' in {}:21)\r\n", wslConfigPath, wslConfigPath));
2287
2288 validateWarnings(
2289 L"networkingMode=NAT\n[experimental]\nnetworkingMode=Mirrored",
2290 std::format(L"wsl: Duplicated config key 'experimental.networkingMode' in {}:4 (Conflicting key: 'wsl2.networkingMode' in {}:2)\r\n", wslConfigPath, wslConfigPath),
2291 L"[wsl2]\n");
2292
2293 validateWarnings(
2294 L"networkingMode=bridged",
2295 L"wsl: " +
2296 FormatErrorMessage(
2297 L"Bridged networking requires wsl2.vmSwitch to be set.",
2298 L"CreateInstance/CreateVm/ConfigureNetworking/WSL_E_VMSWITCH_NOT_SET") +
2299 L"wsl: Failed to configure network (networkingMode Bridged), falling back to networkingMode None.\r\n",
2300 L"[wsl2]\n");
2301
2302 validateWarnings(
2303 L"networkingMode=bridged\nvmSwitch=DoesNotExist",
2304 L"wsl: " +
2305 FormatErrorMessage(
2306 L"The VmSwitch 'DoesNotExist' was not found. Available switches:*",
2307 L"CreateInstance/CreateVm/ConfigureNetworking/WSL_E_VMSWITCH_NOT_FOUND") +
2308 L"wsl: Failed to configure network (networkingMode Bridged), falling back to networkingMode None.\r\n",
2309 L"[wsl2]\n",
2310 true);
2311
2312 if (!AreExperimentalNetworkingFeaturesSupported())
2313 {
2314 validateWarnings(
2315 L"[experimental]\nnetworkingMode=mirrored",
2316 L"wsl: Experimental networking features are not supported, falling back to default settings\r\n",
2317 L"[wsl2]\n");
2318
2319 validateWarnings(
2320 L"[experimental]\ndnsTunneling=true",
2321 L"wsl: Experimental networking features are not supported, falling back to default settings\r\n",
2322 L"[wsl2]\n");
2323
2324 validateWarnings(
2325 L"[experimental]\nfirewall=true",
2326 L"wsl: Experimental networking features are not supported, falling back to default settings\r\n",
2327 L"[wsl2]\n");
2328 }
2329 else
2330 {
2331 if (TryLoadDnsResolverMethods())
2332 {
2333 // Verify DNS tunneling settings are parsed correctly
2334 validateWarnings(L"[experimental]\ndnsTunneling=true\nbestEffortDnsParsing=true", L"");
2335 validateWarnings(L"[experimental]\ndnsTunneling=true\ndnsTunnelingIpAddress=10.255.255.1", L"");
2336
2337 validateWarnings(
2338 L"[experimental]\ndnsTunneling=true\ndnsTunnelingIpAddress=1.2.3",
2339 std::format(L"wsl: Invalid IP value '1.2.3' for key 'experimental.dnsTunnelingIpAddress' in {}:23\r\n", wslConfigPath));
2340 }
2341 }
2342
2343 validateWarnings(
2344 L"[experimental]\nignoredPorts=NotANumber",
2345 std::format(L"wsl: Invalid integer value 'NotANumber' for key 'experimental.ignoredPorts' in {}:22\r\n", wslConfigPath));
2346
2347 validateWarnings(
2348 L"[experimental]\nignoredPorts=65536",
2349 std::format(L"wsl: Invalid integer value '65536' for key 'experimental.ignoredPorts' in {}:22\r\n", wslConfigPath));
2350
2351 // Verify experimental.swiotlb parsing and validation.
2352 //
2353 // With wsl2.virtio enabled (the default), a valid swiotlb value is accepted silently.
2354
2355 validateWarnings(L"[experimental]\nswiotlb=64M", L"");
2356
2357 constexpr auto expectedWarning =
2358 wsl::shared::Arm64 ? L"wsl: The running kernel is missing a patch that significantly improves virtio device "
2359 L"performance. Update to a more recent WSL kernel to enable this optimization.\r\n"
2360 : L"";
2361
2362 validateWarnings(L"[experimental]\nswiotlb=4096K", expectedWarning);
2363
2364 // Malformed values are rejected by the parser; only the parser warning is reported.
2365 validateWarnings(
2366 L"[experimental]\nswiotlb=garbage",
2367 std::format(L"wsl: Invalid memory string 'garbage' for .wslconfig entry 'experimental.swiotlb' in {}:22\r\n", wslConfigPath));
2368
2369 // Verify that the vhdSize setting is parsed correctly.
2370 validateWarnings(L"[wsl2]\ndefaultVhdSize=64GB\n", L"");
2371
2372 auto maxProcessorCount = wsl::windows::common::wslutil::GetLogicalProcessorCount();
2373 validateWarnings(
2374 std::format(L"processors={}", maxProcessorCount + 1).c_str(),
2375 std::format(L"wsl: wsl2.processors cannot exceed the number of logical processors on the system ({} > {})\r\n", maxProcessorCount + 1, maxProcessorCount));
2376
2377 // Exclusively open .wslconfig to make it unreadable
2378 const wil::unique_handle wslConfig{
2379 CreateFile(wslConfigPath.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2380 VERIFY_IS_NOT_NULL(wslConfig);
2381
2382 WslShutdown();
2383 auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
2384 VERIFY_ARE_EQUAL(L"ok\n", output);
2385
2386 VERIFY_ARE_EQUAL(
2387 std::format(L"wsl: Failed to open config file {}, The process cannot access the file because it is being used by another process. \r\n", wslConfigPath),
2388 warnings);
2389
2390 {
2391 DistroFileChange fstab(L"/etc/fstab");
2392 fstab.SetContent(L"invalid fs tab content");
2393 TerminateDistribution();
2394
2395 std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
2396 VERIFY_ARE_EQUAL(L"ok\n", output);
2397 VERIFY_ARE_EQUAL(L"wsl: Processing /etc/fstab with mount -a failed.\n", warnings);
2398 }
2399
2400 // Validate that WSL_DISABLE_WARNINGS silence the stderr output
2401 ScopedEnvVariable disableWarnings(L"WSL_DISABLE_WARNINGS", L"1");
2402 WslShutdown();
2403
2404 std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
2405 VERIFY_ARE_EQUAL(L"ok\n", output);
2406 VERIFY_ARE_EQUAL(L"", warnings);
2407 }
2408
2409 WSL2_TEST_METHOD(Processors)
2410 {
2411 WslConfigChange configChange(LxssGenerateTestConfig() + L"\nprocessors=1");
2412
2413 auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"nproc --all");
2414 VERIFY_ARE_EQUAL(L"1\n", output);
2415 VERIFY_ARE_EQUAL(L"", warnings);
2416 }
2417
2418 WSL2_TEST_METHOD(DmesgCollection)
2419 {
2420 const auto dmesgLogFile = std::filesystem::current_path() / L"test-dmesg.txt";
2421 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteFile(dmesgLogFile.c_str()); });
2422 WslConfigChange config(LxssGenerateTestConfig({}));
2423
2424 auto readDmesgLog = [&](uint64_t offset) -> std::string {
2425 wil::unique_hfile file(CreateFileW(
2426 dmesgLogFile.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
2427 if (!file)
2428 {
2429 return {};
2430 }
2431
2432 LARGE_INTEGER fileOffset{};
2433 fileOffset.QuadPart = static_cast<LONGLONG>(offset);
2434 THROW_LAST_ERROR_IF(!SetFilePointerEx(file.get(), fileOffset, nullptr, FILE_BEGIN));
2435
2436 return ReadToString(file.get());
2437 };
2438
2439 auto fileSize = [&]() -> uint64_t {
2440 WIN32_FILE_ATTRIBUTE_DATA attributes{};
2441 if (!GetFileAttributesExW(dmesgLogFile.c_str(), GetFileExInfoStandard, &attributes))
2442 {
2443 return 0;
2444 }
2445
2446 return (static_cast<uint64_t>(attributes.nFileSizeHigh) << 32) | attributes.nFileSizeLow;
2447 };
2448
2449 auto expectInDmesg = [&](bool earlyBootLogging, const std::string_view& expectedLine) -> std::string {
2450 config.Update(LxssGenerateTestConfig({.earlyBootLogging = earlyBootLogging, .debugConsoleLogFile = dmesgLogFile}));
2451
2452 const auto offset = fileSize();
2453
2454 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0L);
2455
2456 return wsl::shared::retry::RetryWithTimeout<std::string>(
2457 [&]() {
2458 auto content = readDmesgLog(offset);
2459 THROW_HR_IF_MSG(E_FAIL, content.find(expectedLine) == std::string::npos, "%hs", content.c_str());
2460
2461 return content;
2462 },
2463 std::chrono::milliseconds(100),
2464 std::chrono::seconds(120));
2465 };
2466
2467 // 'Linux version' is printed during early boot. 'brd: module loaded' is printed after transitioning to the virtio console.
2468 {
2469 // N.B. brd is only loaded on X64.
2470 auto dmesg = expectInDmesg(true, wsl::shared::Arm64 ? "Linux version" : "brd: module loaded");
2471 VERIFY_ARE_NOT_EQUAL(dmesg.find("Linux version"), std::string::npos);
2472 }
2473
2474 // N.B. Early boot logging is always enabled on ARM64.
2475 if constexpr (!wsl::shared::Arm64)
2476 {
2477 auto dmesg = expectInDmesg(false, "brd: module loaded");
2478 VERIFY_ARE_EQUAL(dmesg.find("Linux version"), std::string::npos);
2479 }
2480 }
2481
2482 WSL2_TEST_METHOD(GuiApplications)
2483 {
2484 auto validateEnvironment = [&](bool systemdEnabled) {
2485 WslConfigChange configChange(LxssGenerateTestConfig({.guiApplications = true}));
2486
2487 // Validate that running the system distro works.
2488 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system true"), 0L);
2489
2490 // Validate that $DISPLAY and $WAYLAND_DISPLAY are set
2491 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"env | grep DISPLAY="), 0L);
2492 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"env | grep WAYLAND_DISPLAY="), 0L);
2493
2494 // Validate the X11 socket is in the expected location and that we can connect to it.
2495 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d /tmp/.X11-unix"), 0L);
2496 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"socat - UNIX-CONNECT:/tmp/.X11-unix/X0 < /dev/null"), 0L);
2497
2498 // Validate that distro-provided tmpfiles rules cannot modify the read-only X11 mount.
2499 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -f /run/tmpfiles.d/x11.conf"), 0L);
2500 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemd-tmpfiles --create --boot x11.conf"), 0L);
2501
2502 // Validate the runtime dir exists and the wayland-0 socket is in the expected location.
2503 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"env | grep XDG_RUNTIME_DIR="), 0L);
2504 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d $XDG_RUNTIME_DIR"), 0L);
2505 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -S $XDG_RUNTIME_DIR/wayland-0"), 0L);
2506 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/wayland-0 < /dev/null"), 0L);
2507
2508 // Validate that WSLg can be disabled.
2509 configChange.Update(LxssGenerateTestConfig({.guiApplications = false}));
2510
2511 // Validate that WSL starts successfully
2512 auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
2513 VERIFY_ARE_EQUAL(L"ok\n", output);
2514 VERIFY_ARE_EQUAL(L"", warnings);
2515 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test ! -e /run/tmpfiles.d/x11.conf"), 0L);
2516
2517 // Validate that WSLg-related environment variables are not present.
2518 //
2519 // N.B. XDG_RUNTIME_DIR is set when systemd is enabled even if GUI apps are disabled.
2520 std::vector<std::wstring> variables = {L"$DISPLAY", L"$WAYLAND_DISPLAY"};
2521 if (!systemdEnabled)
2522 {
2523 variables.emplace_back(L"$XDG_RUNTIME_DIR");
2524 }
2525
2526 for (const auto& variable : variables)
2527 {
2528 std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"echo -n " + variable);
2529 VERIFY_ARE_EQUAL(L"", output);
2530 VERIFY_ARE_EQUAL(L"", warnings);
2531 }
2532
2533 // Validate that wsl --system does not start
2534 std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"--system echo not ok", -1);
2535
2536 const std::wstring configPath = wsl::windows::common::helpers::GetWslConfigPath();
2537 const auto expectedOutput = FormatErrorMessage(
2538 L"GUI application support is disabled via " + configPath + L" or /etc/wsl.conf.",
2539 L"Wsl/Service/CreateInstance/WSL_E_GUI_APPLICATIONS_DISABLED");
2540
2541 VERIFY_ARE_EQUAL(output, expectedOutput);
2542 VERIFY_ARE_EQUAL(L"", warnings);
2543 };
2544
2545 LogInfo("Validate WSLg state with systemd disabled.");
2546 validateEnvironment(false);
2547
2548 LogInfo("Validate WSLg state with systemd enabled.");
2549 auto revert = EnableSystemd();
2550 VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
2551 validateEnvironment(true);
2552 }
2553
2554 WSL2_TEST_METHOD(GuiApplicationsSystemd)
2555 {
2556 DistroFileChange wslConf(L"/etc/wsl.conf", false);
2557 wslConf.SetContent(L"[boot]\nsystemd=true\n");
2558 WslConfigChange config{LxssGenerateTestConfig({.guiApplications = true})};
2559
2560 auto validateSocketExists = [](bool exists) {
2561 LxsstuLaunchWsl(L"ls -a /tmp/.X11-unix/");
2562 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /tmp/.X11-unix/X0"), exists ? 0L : 1L);
2563 };
2564
2565 // Validate that wslg.service restores the socket if it's deleted.
2566 {
2567 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -f /run/systemd/generator/wslg.service"), 0L);
2568 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /run/systemd/generator/default.target.wants/wslg.service"), 0L);
2569
2570 validateSocketExists(true);
2571
2572 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"umount /tmp/.X11-unix"), 0L);
2573
2574 validateSocketExists(false);
2575 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl restart wslg.service"), 0L);
2576 validateSocketExists(true);
2577 }
2578
2579 // Validate that the unit isn't create when GUI apps are disabled
2580 {
2581 config.Update(LxssGenerateTestConfig({.guiApplications = false}));
2582 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /run/systemd/generator/wslg.service"), 1L);
2583 }
2584
2585 // Validate that the unit isn't create when GUI apps are disabled inside the distro.
2586 {
2587 wslConf.SetContent(L"[boot]\nsystemd=true\n[general]\nguiApplications=false");
2588 TerminateDistribution();
2589
2590 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /run/systemd/generator/wslg.service"), 1L);
2591 }
2592 }
2593
2594 TEST_METHOD(RegistryKeys)
2595 {
2596 auto openKey = [&](LPCWSTR keyName) {
2597 LogInfo("OpenKey(HKEY_LOCAL_MACHINE, %ls, KEY_READ)", keyName);
2598 return wsl::windows::common::registry::OpenKey(HKEY_LOCAL_MACHINE, keyName, KEY_READ);
2599 };
2600
2601 // Keys that are created by the optional component and the service.
2602 const std::vector<LPCWSTR> inboxKeys{
2603 L"SOFTWARE\\Classes\\CLSID\\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}",
2604 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\{B2B4A4D1-2754-4140-A2EB-"
2605 L"9A76D9D7CDC6}",
2606 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\IdListAliasTranslations\\WSL",
2607 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\IdListAliasTranslations\\WSLLegacy",
2608 L"SOFTWARE\\Classes\\Directory\\shell\\WSL",
2609 L"SOFTWARE\\Classes\\Directory\\Background\\shell\\WSL",
2610 L"SOFTWARE\\Classes\\Drive\\shell\\WSL"};
2611
2612 for (const auto* keyName : inboxKeys)
2613 {
2614 auto key = openKey(keyName);
2615 VERIFY_IS_TRUE(!!key);
2616 }
2617
2618 // Keys that are only created by the MSI.
2619 const std::vector<LPCWSTR> serviceKeys{
2620 L"SOFTWARE\\Microsoft\\Terminal Server Client\\Default\\OptionalAddIns\\WSLDVC_PACKAGE",
2621 L"SOFTWARE\\Classes\\AppID\\{17696EAC-9568-4CF5-BB8C-82515AAD6C09}",
2622 L"SOFTWARE\\Classes\\CLSID\\{2C3E9A41-7B5D-4F18-93D6-A8C2E4F7B1D9}\\InProcServer32",
2623 L"SOFTWARE\\Classes\\CLSID\\{E3146082-A0DA-43A7-813B-A89EEE8C7628}\\InProcServer32",
2624 L"SOFTWARE\\Classes\\CLSID\\{4F9C8B23-D6E1-4A85-BF2A-E7C5D8F931A6}\\InProcServer32",
2625 L"SOFTWARE\\Classes\\CLSID\\{9C9C7131-D756-48FA-BD49-734E75AF37C0}\\InProcServer32",
2626 L"SOFTWARE\\Classes\\CLSID\\{6D32A4B7-9E1F-4C82-A573-F8B1C4D29E60}\\InProcServer32",
2627 L"SOFTWARE\\Classes\\Interface\\{27394DCF-6383-4E4E-BB0A-C13D4E5F6071}\\ProxyStubClsid32",
2628 L"SOFTWARE\\Classes\\Interface\\{D2F47B8A-1E3C-4D9F-A6B5-7C8E9F0A1B2C}\\ProxyStubClsid32",
2629 L"SOFTWARE\\Classes\\Interface\\{E3F58C9B-2F4D-4E0A-B7C6-8D9F0A1B2C3D}\\ProxyStubClsid32",
2630 L"SOFTWARE\\Classes\\Interface\\{F406DACB-3050-4F1B-A8D7-9E0A1B2C3D4E}\\ProxyStubClsid32",
2631 L"SOFTWARE\\Classes\\Interface\\{05172EBD-4161-4C2C-99E8-AF1B2C3D4E5F}\\ProxyStubClsid32",
2632 L"SOFTWARE\\Classes\\Interface\\{16283FCE-5272-4D3D-AAF9-B02C3D4E5F60}\\ProxyStubClsid32"};
2633
2634 for (const auto* keyName : serviceKeys)
2635 {
2636 auto key = openKey(keyName);
2637 VERIFY_IS_TRUE(!!key);
2638 }
2639 }
2640
2641 TEST_METHOD(BinariesAreSigned)
2642 {
2643 if (!wsl::shared::OfficialBuild)
2644 {
2645 LogSkipped("Build is not signed, skipping test");
2646 return;
2647 }
2648
2649 auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
2650 VERIFY_IS_TRUE(installPath.has_value());
2651
2652 size_t signedFiles = 0;
2653
2654 for (const auto& e : std::filesystem::recursive_directory_iterator(installPath.value()))
2655 {
2656 if (wsl::windows::common::string::IsPathComponentEqual(e.path().extension().native(), L".dll") ||
2657 wsl::windows::common::string::IsPathComponentEqual(e.path().extension().native(), L".exe"))
2658 {
2659 LogInfo("Validating signature for: %ls", e.path().c_str());
2660
2661 wsl::windows::common::install::ValidateFileSignature(e.path().c_str());
2662 signedFiles++;
2663 }
2664 }
2665
2666 // Sanity check
2667 VERIFY_ARE_NOT_EQUAL(signedFiles, 0);
2668 }
2669
2670 WSL2_TEST_METHOD(CorruptedVhd)
2671 {
2672 // Create a 100MB vhd without a filesystem.
2673 auto distroPath = wsl::windows::common::filesystem::GetCanonicalPath(wil::GetCurrentDirectoryW<std::wstring>());
2674 auto vhdPath = distroPath / L"CorruptedTest.vhdx";
2675
2676 VIRTUAL_STORAGE_TYPE storageType{};
2677 storageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
2678 storageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT;
2679
2680 CREATE_VIRTUAL_DISK_PARAMETERS createVhdParameters{};
2681 createVhdParameters.Version = CREATE_VIRTUAL_DISK_VERSION_2;
2682 createVhdParameters.Version2.BlockSizeInBytes = 1024 * 1024;
2683 createVhdParameters.Version2.MaximumSize = 100 * 1024 * 1024;
2684
2685 wil::unique_hfile vhd{};
2686 VERIFY_ARE_EQUAL(
2687 ::CreateVirtualDisk(
2688 &storageType, vhdPath.c_str(), VIRTUAL_DISK_ACCESS_NONE, nullptr, CREATE_VIRTUAL_DISK_FLAG_SUPPORT_COMPRESSED_VOLUMES, 0, &createVhdParameters, nullptr, &vhd),
2689 0l);
2690
2691 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2692 vhd.reset();
2693 DeleteFileW(vhdPath.c_str());
2694 });
2695
2696 auto validateOutput = [&](const std::wstring& command, const std::wstring& expectedOutput) {
2697 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(command.c_str(), -1);
2698 VERIFY_ARE_EQUAL(output, expectedOutput);
2699 };
2700
2701 // Attempt to import a vhd with an open handle.
2702 validateOutput(
2703 std::format(L"--import-in-place test-distro-corrupted \"{}\"", vhdPath.wstring()),
2704 FormatErrorMessage(
2705 std::format(
2706 L"Failed to attach disk '\\\\?\\{}' to WSL2: The process cannot access the file because it is being used by "
2707 L"another process. ",
2708 vhdPath.wstring()),
2709 L"Wsl/Service/RegisterDistro/MountDisk/HCS/ERROR_SHARING_VIOLATION"));
2710
2711 vhd.reset();
2712
2713 // Create a broken distribution registration
2714 {
2715 const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
2716 const auto distroKey =
2717 wsl::windows::common::registry::CreateKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
2718
2719 auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
2720 wsl::windows::common::registry::DeleteKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
2721 });
2722
2723 wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"BasePath", distroPath.c_str());
2724 wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"VhdFileName", L"CorruptedTest.vhdx");
2725 wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"DistributionName", L"BrokenDistro");
2726 wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"DefaultUid", 0);
2727 wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Version", LXSS_DISTRO_VERSION_2);
2728 wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"State", LxssDistributionStateInstalled);
2729 wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
2730
2731 // Validate that starting the distribution fails with the correct error code.
2732 validateOutput(
2733 L"-d BrokenDistro echo ok",
2734 FormatErrorMessage(
2735 L"The distribution failed to start because its virtual disk is corrupted.",
2736 L"Wsl/Service/CreateInstance/WSL_E_DISK_CORRUPTED"));
2737
2738 // Validate that trying to export the distribution fails with the correct error code.
2739 validateOutput(
2740 L"--export BrokenDistro dummy.tar",
2741 FormatErrorMessage(
2742 L"The distribution failed to start because its virtual disk is corrupted.",
2743 L"Wsl/Service/WSL_E_DISK_CORRUPTED"));
2744
2745 // Shutdown WSL to force the disk to detach.
2746 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown"), 0L);
2747 }
2748
2749 // Import a corrupted vhd.
2750 validateOutput(
2751 std::format(L"--import-in-place test-distro-corrupted \"{}\"", vhdPath.wstring()),
2752 FormatErrorMessage(
2753 L"The distribution failed to start because its virtual disk is corrupted.",
2754 L"Wsl/Service/RegisterDistro/WSL_E_DISK_CORRUPTED"));
2755
2756 // Ensure the VHD can be deleted to make sure it was properly ejected from the VM.
2757 VERIFY_ARE_EQUAL(DeleteFileW(vhdPath.c_str()), TRUE);
2758 }
2759
2760 static void ValidateDistributionShortcut(LPCWSTR DistroName, HANDLE ExpectedIcon)
2761 {
2762 auto distroKey = OpenDistributionKey(DistroName);
2763 auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
2764 auto shellLink = wil::CoCreateInstance<IShellLink>(CLSID_ShellLink);
2765 auto startMenu = wsl::windows::common::filesystem::GetKnownFolderPath(FOLDERID_StartMenu, KF_FLAG_CREATE);
2766
2767 // Validate that the shortcut is actually in the start menu
2768 VERIFY_IS_TRUE(shortcutPath.find(startMenu) != std::string::npos);
2769
2770 auto storage = shellLink.query<IPersistFile>();
2771
2772 VERIFY_SUCCEEDED(storage->Load(shortcutPath.c_str(), 0));
2773
2774 std::wstring target(MAX_PATH, '\0');
2775
2776 WIN32_FIND_DATA findData{};
2777 VERIFY_SUCCEEDED(shellLink->GetPath(target.data(), static_cast<int>(target.size()), &findData, SLGP_RAWPATH));
2778 target.resize(wcslen(target.c_str()));
2779
2780 static auto wslExePath = wsl::windows::common::wslutil::GetMsiPackagePath().value() + L"wsl.exe";
2781 VERIFY_ARE_EQUAL(target, wslExePath);
2782
2783 std::wstring arguments(MAX_PATH, '\0');
2784 VERIFY_SUCCEEDED(shellLink->GetArguments(arguments.data(), static_cast<int>(arguments.size())));
2785 arguments.resize(wcslen(arguments.c_str()));
2786
2787 auto distroId = GetDistributionId(DistroName);
2788 VERIFY_IS_TRUE(distroId.has_value());
2789
2790 VERIFY_ARE_EQUAL(
2791 std::format(L"{} {} {} {}", WSL_DISTRIBUTION_ID_ARG, wsl::shared::string::GuidToString<wchar_t>(distroId.value()), WSL_CHANGE_DIRECTORY_ARG, WSL_CWD_HOME),
2792 arguments);
2793
2794 std::wstring iconLocation(MAX_PATH, '\0');
2795 int id{};
2796 THROW_IF_FAILED(shellLink->GetIconLocation(iconLocation.data(), static_cast<int>(iconLocation.size()), &id));
2797 iconLocation.resize(wcslen(iconLocation.c_str()));
2798
2799 if (ExpectedIcon == nullptr)
2800 {
2801 VERIFY_ARE_EQUAL(iconLocation, wslExePath);
2802 }
2803 else
2804 {
2805 auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
2806
2807 // Validate that the icon is under the distribution folder.
2808 VERIFY_IS_TRUE(iconLocation.find(basePath) != std::string::npos);
2809
2810 // Validate that the icon has the content we expect.
2811 wil::unique_handle distroIcon{CreateFile(iconLocation.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
2812 VERIFY_ARE_EQUAL(GetFileSize(ExpectedIcon, nullptr), GetFileSize(distroIcon.get(), nullptr));
2813 }
2814 }
2815
2816 static std::pair<nlohmann::json, std::wstring> ValidateDistributionTerminalProfile(const std::wstring& DistroName, bool defaultIcon)
2817 {
2818 using namespace wsl::windows::common::wslutil;
2819 using namespace wsl::windows::common::string;
2820
2821 auto distroKey = OpenDistributionKey(DistroName.c_str());
2822 auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
2823
2824 auto distroId = GetDistributionId(DistroName.c_str());
2825 VERIFY_IS_TRUE(distroId.has_value());
2826
2827 auto distroIdString = wsl::shared::string::GuidToString<wchar_t>(distroId.value());
2828 auto distributionProfileId =
2829 wsl::shared::string::GuidToString<wchar_t>(CreateV5Uuid(WslTerminalNamespace, std::as_bytes(std::span{distroIdString})));
2830
2831 auto profilePath = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"Microsoft" / L"Windows Terminal" /
2832 L"Fragments" / L"Microsoft.WSL" / (distributionProfileId + L".json");
2833
2834 std::ifstream file{profilePath};
2835 VERIFY_IS_TRUE(file.good());
2836
2837 nlohmann::json json;
2838 VERIFY_IS_TRUE((file >> json).good());
2839
2840 VERIFY_IS_TRUE(json.is_object());
2841
2842 auto profiles = json.find("profiles");
2843 VERIFY_ARE_NOT_EQUAL(profiles, json.end());
2844 VERIFY_IS_TRUE(profiles->is_array());
2845
2846 VERIFY_IS_TRUE(profiles->size() >= 2);
2847 const auto profileHide = profiles->at(0);
2848
2849 auto expectedHideGuid = wsl::shared::string::GuidToString<wchar_t>(
2850 CreateV5Uuid(GeneratedProfilesTerminalNamespace, std::as_bytes(std::span{DistroName})));
2851 VERIFY_ARE_EQUAL(profileHide["updates"], wsl::shared::string::WideToMultiByte(expectedHideGuid));
2852 VERIFY_ARE_EQUAL(profileHide["hidden"], true);
2853
2854 const auto launchProfile = profiles->at(1);
2855
2856 auto expectedId =
2857 wsl::shared::string::GuidToString<wchar_t>(CreateV5Uuid(WslTerminalNamespace, std::as_bytes(std::span{distroIdString})));
2858 VERIFY_ARE_EQUAL(launchProfile["guid"].get<std::string>(), wsl::shared::string::WideToMultiByte(expectedId));
2859 VERIFY_ARE_EQUAL(launchProfile["name"].get<std::string>(), wsl::shared::string::WideToMultiByte(DistroName));
2860 VERIFY_ARE_EQUAL(launchProfile["pathTranslationStyle"].get<std::string>(), "wsl");
2861
2862 std::wstring systemDir;
2863 wil::GetSystemDirectoryW(systemDir);
2864
2865 VERIFY_ARE_EQUAL(
2866 std::format("{}\\{} {} {}", systemDir, WSL_BINARY_NAME, WSL_DISTRIBUTION_ID_ARG, distroIdString),
2867 launchProfile["commandline"].get<std::string>());
2868
2869 // Verify that startingDirectory is set to home directory
2870 VERIFY_ARE_EQUAL(launchProfile["startingDirectory"].get<std::string>(), "~");
2871
2872 auto iconLocation = wsl::shared::string::MultiByteToWide(launchProfile["icon"].get<std::string>());
2873 if (defaultIcon)
2874 {
2875 static auto wslExePath = wsl::windows::common::wslutil::GetMsiPackagePath().value() + L"wsl.exe";
2876 VERIFY_ARE_EQUAL(iconLocation, wslExePath);
2877 }
2878 else
2879 {
2880 auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
2881
2882 // Validate that the icon is under the distribution folder.
2883 VERIFY_IS_TRUE(iconLocation.find(basePath) == 0);
2884 }
2885
2886 return std::make_pair(json, profilePath);
2887 }
2888
2889 TEST_METHOD(ConvertDistro)
2890 {
2891 std::wstring originalVersion;
2892 std::wstring targetVersion;
2893 if (LxsstuVmMode())
2894 {
2895 originalVersion = L"2";
2896 targetVersion = L"1";
2897 }
2898 else
2899 {
2900 originalVersion = L"1";
2901 targetVersion = L"2";
2902 }
2903
2904 auto cleanup =
2905 wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(L"--set-version test_distro " + originalVersion); });
2906
2907 // Convert the test distribution to the target version and back to the original.
2908 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--set-version test_distro " + targetVersion), 0u);
2909 ValidateDistributionShortcut(LXSS_DISTRO_NAME_TEST_L, nullptr);
2910 ValidateDistributionTerminalProfile(LXSS_DISTRO_NAME_TEST_L, true);
2911
2912 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--set-version test_distro " + originalVersion), 0u);
2913 ValidateDistributionShortcut(LXSS_DISTRO_NAME_TEST_L, nullptr);
2914 ValidateDistributionTerminalProfile(LXSS_DISTRO_NAME_TEST_L, true);
2915
2916 // Do not convert the test distribution if it is already in the original version.
2917 cleanup.release();
2918 }
2919
2920 WSL2_TEST_METHOD(ManualDistroShutdown)
2921 {
2922 // Terminate a distribution from within WSL. This command should be terminated by the VM terminating
2923 LxsstuLaunchWsl(L"echo foo > /dev/shm/bar ; reboot -f ; sleep 1d");
2924
2925 // Wait for distribution to be terminated to avoid running the next command as it shuts down
2926 auto pred = []() {
2927 const auto commandLine = LxssGenerateWslCommandLine(L"--list --running");
2928 wsl::windows::common::SubProcess process(nullptr, commandLine.c_str());
2929
2930 // Don't check the exit code since that command returns -1 when no distros are running.
2931 const auto output = process.RunAndCaptureOutput();
2932 THROW_HR_IF(E_ABORT, output.Stdout.find(LXSS_DISTRO_NAME_TEST_L) != std::string::npos);
2933 };
2934
2935 wsl::shared::retry::RetryWithTimeout<void>(pred, std::chrono::seconds(1), std::chrono::minutes(2));
2936
2937 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"test -f /dev/shm/bar2 || echo -n ok");
2938 VERIFY_ARE_EQUAL(out, L"ok");
2939 }
2940
2941 WSL2_TEST_METHOD(KernelModules)
2942 {
2943 // Get the kernel version and strip off everything after the first dash.
2944 std::wstring kernelVersion{TEXT(KERNEL_VERSION)};
2945 auto position = kernelVersion.find_first_of(L"-");
2946 if (position != kernelVersion.npos)
2947 {
2948 kernelVersion = kernelVersion.substr(0, position);
2949 }
2950
2951 kernelVersion += L"-microsoft-standard-WSL2";
2952
2953 // Ensure the kernel modules folder is mounted correctly.
2954 std::wstring command = std::format(
2955 L"mount | grep -iF 'none on /usr/lib/modules/{} type overlay "
2956 L"(rw,nosuid,nodev,noatime,lowerdir=/modules/{}/modules,upperdir=/lib/modules/{}/rw/upper,workdir=/lib/modules/{}/rw/"
2957 L"work,uuid=on)'",
2958 kernelVersion,
2959 kernelVersion,
2960 kernelVersion,
2961 kernelVersion);
2962
2963 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command.c_str(), nullptr, nullptr, nullptr, nullptr), 0u);
2964
2965 // Update .wslconfig and ensure an error is displayed if nonexistent kernel or modules is specified.
2966 const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
2967 const std::wstring nonExistentFile = L"DoesNotExist";
2968 WslConfigChange configChange(LxssGenerateTestConfig({.kernel = nonExistentFile.c_str()}));
2969 ValidateOutput(
2970 L"echo ok",
2971 FormatErrorMessage(
2972 wsl::shared::Localization::MessageCustomKernelNotFound(wslConfigPath, nonExistentFile),
2973 L"Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND"),
2974 L"");
2975
2976 configChange.Update(LxssGenerateTestConfig({.kernelModules = nonExistentFile.c_str()}));
2977 ValidateOutput(
2978 L"echo ok",
2979 FormatErrorMessage(
2980 wsl::shared::Localization::MessageCustomKernelModulesNotFound(wslConfigPath, nonExistentFile),
2981 L"Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND"),
2982 L"");
2983
2984 #ifdef WSL_DEV_INSTALL_PATH
2985
2986 std::wstring kernelPath = WSL_DEV_INSTALL_PATH L"/kernel";
2987 std::wstring kernelModulesPath = WSL_DEV_INSTALL_PATH L"/artifacts.vhd";
2988
2989 #else
2990
2991 auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
2992 VERIFY_IS_TRUE(installPath.has_value());
2993
2994 std::filesystem::path wslInstallPath(installPath.value());
2995
2996 std::wstring kernelPath = wslInstallPath / "tools" / "kernel";
2997 std::wstring kernelModulesPath = wslInstallPath / "tools" / "artifacts.vhd";
2998
2999 #endif
3000
3001 // Verify that no modules are mounted for a custom kernel with no modules specified.
3002 kernelPath = std::regex_replace(kernelPath, std::wregex(L"\\\\"), L"\\\\");
3003 configChange.Update(LxssGenerateTestConfig({.kernel = kernelPath.c_str()}));
3004 ValidateOutput(command.c_str(), L"", L"", 1);
3005
3006 // Verify the error message if custom kernel modules are used with the default kernel.
3007 kernelModulesPath = std::regex_replace(kernelModulesPath, std::wregex(L"\\\\"), L"\\\\");
3008 configChange.Update(LxssGenerateTestConfig({.kernelModules = kernelModulesPath.c_str()}));
3009 ValidateOutput(
3010 L"echo ok",
3011 FormatErrorMessage(
3012 wsl::shared::Localization::MessageMismatchedKernelModulesError(),
3013 L"Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND"),
3014 L"");
3015
3016 configChange.Update(LxssGenerateTestConfig());
3017
3018 // Validate that tun is loaded by default.
3019 ValidateOutput(L"grep -i '^tun' /proc/modules | wc -l", L"1\n", L"", 0);
3020
3021 // Validate a VM can boot with no extra additional kernel modules.
3022 configChange.Update(LxssGenerateTestConfig({.loadDefaultKernelModules = false}));
3023 ValidateOutput(L"grep -i '^tun' /proc/modules | wc -l", L"0\n", L"", 0);
3024
3025 // Validate that the user can pass additional modules to load at boot.
3026 ValidateOutput(L"grep -iE '^(usb_storage|dm_crypt)' /proc/modules | wc -l", L"0\n", L"", 0);
3027
3028 configChange.Update(LxssGenerateTestConfig({.loadKernelModules = L"usb_storage,dm_crypt"}));
3029 ValidateOutput(L"grep -iE '^(usb_storage|dm_crypt)' /proc/modules | wc -l", L"2\n", L"", 0);
3030
3031 // Validate that failing to load a module shows a warning in dmesg.
3032 configChange.Update(LxssGenerateTestConfig({.loadKernelModules = L"not-found"}));
3033 ValidateOutput(L"dmesg | grep -iF \"failed to load module 'not-found'\" | wc -l", L"1\n", L"", 0);
3034 }
3035
3036 WSL2_TEST_METHOD(CrashCollection)
3037 {
3038 const auto folder = std::filesystem::absolute(L"test-crash-dumps");
3039
3040 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
3041 std::error_code error;
3042 std::filesystem::remove_all(folder, error);
3043 });
3044
3045 auto countCrashes = [&]() {
3046 std::error_code error;
3047 return std::distance(std::filesystem::directory_iterator{folder, error}, std::filesystem::directory_iterator{});
3048 };
3049
3050 auto waitForCrashes = [&](int expected) {
3051 wsl::shared::retry::RetryWithTimeout<void>(
3052 [&]() { THROW_HR_IF(E_UNEXPECTED, countCrashes() < expected); }, std::chrono::seconds(1), std::chrono::minutes(2));
3053
3054 VERIFY_ARE_EQUAL(countCrashes(), expected);
3055 };
3056
3057 auto crash = []() { LxsstuLaunchWsl(L"kill -SEGV $$"); };
3058
3059 WslConfigChange change(LxssGenerateTestConfig({.crashDumpCount = 2, .CrashDumpFolder = folder.wstring()}));
3060
3061 VERIFY_ARE_EQUAL(countCrashes(), 0);
3062
3063 crash();
3064 waitForCrashes(1);
3065
3066 crash();
3067 waitForCrashes(2);
3068
3069 crash();
3070 waitForCrashes(2);
3071
3072 // Create a dummy file and validate that the file limit logic doesn't remove it.
3073 std::filesystem::remove_all(folder);
3074 std::filesystem::create_directory(folder);
3075 std::ofstream(folder / "dummy").close();
3076
3077 crash();
3078 waitForCrashes(2);
3079
3080 crash();
3081 waitForCrashes(3);
3082
3083 crash();
3084 waitForCrashes(3);
3085
3086 VERIFY_IS_TRUE(std::filesystem::exists(folder / "dummy"));
3087 }
3088
3089 // UnitTests Private Methods
3090
3091 static VOID VerifyCaseSensitiveDirectory(_In_ PCWSTR RelativePath)
3092 {
3093
3094 const std::wstring Path = LxsstuGetLxssDirectory() + L"\\" + RelativePath;
3095 const wil::unique_hfile Directory{CreateFileW(
3096 Path.c_str(),
3097 FILE_READ_ATTRIBUTES,
3098 (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE),
3099 nullptr,
3100 OPEN_EXISTING,
3101 (FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT),
3102 nullptr)};
3103
3104 THROW_LAST_ERROR_IF(!Directory);
3105 IO_STATUS_BLOCK IoStatus;
3106 FILE_CASE_SENSITIVE_INFORMATION CaseInfo;
3107 THROW_IF_NTSTATUS_FAILED(NtQueryInformationFile(Directory.get(), &IoStatus, &CaseInfo, sizeof(CaseInfo), FileCaseSensitiveInformation));
3108
3109 VERIFY_ARE_EQUAL(CaseInfo.Flags, (ULONG)FILE_CS_FLAG_CASE_SENSITIVE_DIR);
3110 }
3111
3112 TEST_METHOD(Move)
3113 {
3114 constexpr auto name = L"move-test-distro";
3115 constexpr auto testFolder = L"move-test-test-folder";
3116
3117 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
3118
3119 auto cleanupName = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() {
3120 LxsstuLaunchWsl(std::format(L"--unregister {}", name));
3121 std::filesystem::remove_all(testFolder);
3122 });
3123
3124 auto validateDistro = []() {
3125 auto [cmdOutput, _] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
3126 VERIFY_ARE_EQUAL(cmdOutput, L"ok\n");
3127 };
3128
3129 // Move the distro to a different folder (relative path)
3130 {
3131 WslShutdown();
3132 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, testFolder)), 0L);
3133
3134 // Validate that the distribution still starts
3135 validateDistro();
3136 VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", testFolder)));
3137 }
3138
3139 auto absolutePath = wsl::windows::common::filesystem::GetCanonicalPath(".").wstring();
3140
3141 // Move the distro to a different folder (absolute path)
3142 {
3143 WslShutdown();
3144 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, absolutePath)), 0L);
3145
3146 // Validate that the distribution still starts
3147 validateDistro();
3148 VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", absolutePath)));
3149 }
3150
3151 // Try to move the distribution to a folder that's already in use
3152 {
3153 WslShutdown();
3154
3155 wil::unique_cotaskmem_string path;
3156 THROW_IF_FAILED(::SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path));
3157 auto targetPath = std::format(L"{}\\lxss", path.get());
3158 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --move {}", name, targetPath), -1);
3159
3160 VERIFY_ARE_EQUAL(
3161 out,
3162 FormatErrorMessage(
3163 L"The supplied install location is already in use.", L"Wsl/Service/MoveDistro/ERROR_FILE_EXISTS"));
3164 // Validate that the distribution still starts and that the vhd hasn't moved.
3165 validateDistro();
3166 VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", absolutePath)));
3167 }
3168
3169 // Try to move the distribution to an invalid path
3170 {
3171 WslShutdown();
3172
3173 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --move :", name), -1);
3174
3175 VERIFY_ARE_EQUAL(
3176 out,
3177 FormatErrorMessage(
3178 L"The filename, directory name, or volume label syntax is incorrect. ",
3179 L"Wsl/Service/MoveDistro/ERROR_INVALID_NAME"));
3180 // Validate that the distribution still starts and that the vhd hasn't moved.
3181 validateDistro();
3182 VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", absolutePath)));
3183 }
3184 }
3185
3186 WSL2_TEST_METHOD(MoveVhdOwnership)
3187 {
3188 constexpr auto name = L"move-owner-test-distro";
3189 constexpr auto moveElevatedFolder = L"move-owner-elevated";
3190 constexpr auto moveNonElevatedFolder = L"move-owner-non-elevated";
3191
3192 // Import a WSL2 distro.
3193 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
3194
3195 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() {
3196 LxsstuLaunchWsl(std::format(L"--unregister {}", name));
3197 std::filesystem::remove_all(moveElevatedFolder);
3198 std::filesystem::remove_all(moveNonElevatedFolder);
3199 });
3200
3201 auto verifyVhdOwner = [](const std::wstring& path) {
3202 PSID ownerSid = nullptr;
3203 wil::unique_hlocal descriptor;
3204 THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
3205 path.c_str(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &ownerSid, nullptr, nullptr, nullptr, &descriptor));
3206
3207 auto userToken = wil::open_current_access_token(TOKEN_QUERY);
3208 auto tokenUser = wil::get_token_information<TOKEN_USER>(userToken.get());
3209
3210 VERIFY_IS_TRUE(EqualSid(ownerSid, tokenUser->User.Sid));
3211 };
3212
3213 const auto nonElevatedToken = GetNonElevatedToken();
3214
3215 // Move as elevated, launch as non-elevated.
3216 // This is the primary bug scenario: MoveFileEx sets owner to BUILTIN\Administrators,
3217 // then HcsGrantVmAccess fails with E_ACCESSDENIED when impersonating the non-elevated user.
3218 {
3219 WslShutdown();
3220 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, moveElevatedFolder)), 0L);
3221
3222 auto vhdPath = std::format(L"{}\\ext4.vhdx", moveElevatedFolder);
3223 VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
3224 verifyVhdOwner(vhdPath);
3225
3226 WslShutdown();
3227 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name), 0, nullptr, nonElevatedToken.get());
3228 VERIFY_ARE_EQUAL(out, L"ok\n");
3229 }
3230
3231 // Move as non-elevated, launch as elevated.
3232 {
3233 WslShutdown();
3234 VERIFY_ARE_EQUAL(
3235 LxsstuLaunchWsl(
3236 std::format(L"--manage {} --move {}", name, moveNonElevatedFolder),
3237 nullptr,
3238 nullptr,
3239 nullptr,
3240 nonElevatedToken.get()),
3241 0L);
3242
3243 auto vhdPath = std::format(L"{}\\ext4.vhdx", moveNonElevatedFolder);
3244 VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
3245 verifyVhdOwner(vhdPath);
3246
3247 WslShutdown();
3248 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name));
3249 VERIFY_ARE_EQUAL(out, L"ok\n");
3250 }
3251
3252 // Also launch as non-elevated after the non-elevated move.
3253 {
3254 WslShutdown();
3255 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name), 0, nullptr, nonElevatedToken.get());
3256 VERIFY_ARE_EQUAL(out, L"ok\n");
3257 }
3258 }
3259
3260 WSL2_TEST_METHOD(MoveVhdWithAdminOwner)
3261 {
3262 // Regression test for #40716: a same-volume move must succeed when the VHD
3263 // is already owned by BUILTIN\Administrators.
3264 constexpr auto name = L"move-admin-owner-test-distro";
3265 constexpr auto firstFolder = L"move-admin-owner-first";
3266 constexpr auto secondFolder = L"move-admin-owner-second";
3267
3268 // Import a WSL2 distro.
3269 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
3270
3271 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() {
3272 LxsstuLaunchWsl(std::format(L"--unregister {}", name));
3273 std::filesystem::remove_all(firstFolder);
3274 std::filesystem::remove_all(secondFolder);
3275 });
3276
3277 // Move to first folder so we know where the VHD is.
3278 WslShutdown();
3279 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, firstFolder)), 0L);
3280
3281 auto vhdPath = std::format(L"{}\\ext4.vhdx", firstFolder);
3282 VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
3283
3284 // Simulate cross-volume MoveFileEx side-effect: change VHD owner to BUILTIN\Administrators.
3285 {
3286 BYTE adminsSidBuffer[SECURITY_MAX_SID_SIZE];
3287 DWORD sidSize = sizeof(adminsSidBuffer);
3288 THROW_IF_WIN32_BOOL_FALSE(CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, adminsSidBuffer, &sidSize));
3289
3290 THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(
3291 const_cast<LPWSTR>(vhdPath.c_str()), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, adminsSidBuffer, nullptr, nullptr, nullptr));
3292
3293 // Verify it took effect.
3294 PSID ownerSid = nullptr;
3295 wil::unique_hlocal descriptor;
3296 THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
3297 vhdPath.c_str(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &ownerSid, nullptr, nullptr, nullptr, &descriptor));
3298 VERIFY_IS_TRUE(EqualSid(ownerSid, adminsSidBuffer));
3299 }
3300
3301 // Now move again as non-elevated. Before the fix, this would fail with E_ACCESSDENIED
3302 // because CreateFileW(WRITE_OWNER) was called under user impersonation.
3303 const auto nonElevatedToken = GetNonElevatedToken();
3304 WslShutdown();
3305 VERIFY_ARE_EQUAL(
3306 LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, secondFolder), nullptr, nullptr, nullptr, nonElevatedToken.get()), 0L);
3307
3308 auto newVhdPath = std::format(L"{}\\ext4.vhdx", secondFolder);
3309 VERIFY_IS_TRUE(std::filesystem::exists(newVhdPath));
3310
3311 // A same-volume move preserves the VHD owner.
3312 {
3313 PSID ownerSid = nullptr;
3314 wil::unique_hlocal descriptor;
3315 THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
3316 newVhdPath.c_str(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &ownerSid, nullptr, nullptr, nullptr, &descriptor));
3317
3318 BYTE adminsSidCheck[SECURITY_MAX_SID_SIZE] = {};
3319 DWORD sidSize = sizeof(adminsSidCheck);
3320 THROW_IF_WIN32_BOOL_FALSE(CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, adminsSidCheck, &sidSize));
3321 VERIFY_IS_TRUE(EqualSid(ownerSid, adminsSidCheck));
3322 }
3323
3324 // Validate distro still works.
3325 WslShutdown();
3326 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name), 0, nullptr, nonElevatedToken.get());
3327 VERIFY_ARE_EQUAL(out, L"ok\n");
3328 }
3329
3330 WSL2_TEST_METHOD(Resize)
3331 {
3332 constexpr auto name = L"resize-test-distro";
3333
3334 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
3335 WslShutdown();
3336
3337 auto cleanupName =
3338 wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
3339
3340 auto validateDistro = [name](LPCWSTR size, LPCWSTR expectedSize, const std::wstring& expectedError = {}) {
3341 auto [out, _] =
3342 LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --resize {}", name, size), expectedError.empty() ? 0 : -1);
3343 if (!expectedError.empty())
3344 {
3345 VERIFY_ARE_EQUAL(expectedError, out);
3346 return;
3347 }
3348
3349 std::tie(out, _) = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} df -h / --output=size | sed 1d", name));
3350 VERIFY_ARE_EQUAL(std::format(L" {}\n", expectedSize), out);
3351 WslShutdown();
3352 };
3353
3354 validateDistro(L"1500G", L"1.5T");
3355 validateDistro(L"500G", L"492G");
3356 validateDistro(L"1M", nullptr, FormatErrorMessage(L"Failed to resize disk.", L"Wsl/Service/E_FAIL"));
3357
3358 {
3359 WslKeepAlive keepAlive;
3360 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage test_distro --resize 1500GB", -1);
3361 VERIFY_ARE_EQUAL(
3362 FormatErrorMessage(
3363 L"The operation could not be completed because the VHD is currently in use. To force WSL "
3364 L"to stop use: wsl.exe --shutdown",
3365 L"Wsl/Service/WSL_E_DISTRO_NOT_STOPPED"),
3366 out);
3367 }
3368 }
3369
3370 // Verifies that VHD-mutating manage operations (--resize, --set-sparse, --move) are rejected while a
3371 // long-running conversion/export holds the distribution lock, rather than racing with it on the VHD.
3372 WSL2_TEST_METHOD(ManageRejectedWhileLocked)
3373 {
3374 constexpr auto name = L"manage-locked-test-distro";
3375
3376 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
3377 auto cleanupName =
3378 wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
3379 WslShutdown();
3380
3381 // Start an export to a pipe we deliberately don't drain. Use a tiny buffer so the export blocks
3382 // as soon as it writes any data, regardless of the test distro's size, deterministically holding
3383 // the distribution in the "Exporting" locked state.
3384 auto [readPipe, writePipe] = CreateSubprocessPipe(false, true, 1);
3385
3386 std::thread exportThread([&]() { LxsstuLaunchWsl(std::format(L"--export {} -", name), nullptr, writePipe.get()); });
3387
3388 auto joinExport = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
3389 // Close the read end so the blocked export fails with a broken pipe and returns, then join.
3390 readPipe.reset();
3391 if (exportThread.joinable())
3392 {
3393 exportThread.join();
3394 }
3395 });
3396
3397 // Wait until the service reports the distribution as Exporting (i.e. the lock is held), retrying for up
3398 // to two minutes so a slow machine doesn't flake before the export acquires the lock.
3399 wsl::shared::retry::RetryWithTimeout<void>(
3400 [&]() {
3401 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--list --verbose");
3402 bool locked = false;
3403 std::wistringstream stream(out);
3404 for (std::wstring line; std::getline(stream, line);)
3405 {
3406 if (line.find(name) != std::wstring::npos && line.find(L"Exporting") != std::wstring::npos)
3407 {
3408 locked = true;
3409 break;
3410 }
3411 }
3412
3413 THROW_HR_IF(E_ABORT, !locked);
3414 },
3415 std::chrono::milliseconds(100),
3416 std::chrono::minutes(2),
3417 [] { return wil::ResultFromCaughtException() == E_ABORT; });
3418
3419 // Each VHD-mutating manage operation must be rejected with E_ILLEGAL_STATE_CHANGE while the lock is held.
3420 auto verifyRejected = [&](const std::wstring& command) {
3421 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(command, -1);
3422 VERIFY_IS_TRUE(out.find(L"E_ILLEGAL_STATE_CHANGE") != std::wstring::npos);
3423 };
3424
3425 verifyRejected(std::format(L"--manage {} --resize 2GB", name));
3426 verifyRejected(std::format(L"--manage {} --set-sparse false", name));
3427
3428 const auto moveTarget = std::filesystem::absolute(L"manage-locked-move-target").wstring();
3429 verifyRejected(std::format(L"--manage {} --move \"{}\"", name, moveTarget));
3430 }
3431
3432 WSL2_TEST_METHOD(Compact)
3433 {
3434 constexpr auto name = L"compact-test-distro";
3435
3436 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
3437 WslShutdown();
3438
3439 auto cleanupName =
3440 wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
3441
3442 const auto distroKey = OpenDistributionKey(name);
3443 VERIFY_IS_NOT_NULL(distroKey.get());
3444
3445 const auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
3446 const auto vhdFileName =
3447 wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"VhdFileName", L"ext4.vhdx");
3448 const auto vhdPath = std::filesystem::path(basePath) / vhdFileName;
3449 VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
3450
3451 auto getVhdSizeOnDisk = [](const std::filesystem::path& path) {
3452 DWORD highPart{};
3453 SetLastError(NO_ERROR);
3454 const auto lowPart = GetCompressedFileSizeW(path.c_str(), &highPart);
3455 THROW_LAST_ERROR_IF(lowPart == INVALID_FILE_SIZE && GetLastError() != NO_ERROR);
3456
3457 ULARGE_INTEGER size{};
3458 size.LowPart = lowPart;
3459 size.HighPart = highPart;
3460 return size.QuadPart;
3461 };
3462
3463 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name));
3464 VERIFY_ARE_EQUAL(err, L"");
3465
3466 constexpr auto minimumCompactionDelta = 32ull * 1024 * 1024;
3467 const auto sizeBeforeWrite = getVhdSizeOnDisk(vhdPath);
3468
3469 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(
3470 L"-d {} -u root -- sh -c 'mkdir -p /root/vhdx-compact-test && "
3471 L"dd if=/dev/zero bs=1M count=128 2>/dev/null | base64 -w 0 > /root/vhdx-compact-test/nonzero.bin && sync'",
3472 name));
3473 VERIFY_ARE_EQUAL(err, L"");
3474 WslShutdown();
3475
3476 const auto sizeAfterWrite = getVhdSizeOnDisk(vhdPath);
3477 VERIFY_IS_TRUE(sizeAfterWrite >= sizeBeforeWrite + minimumCompactionDelta);
3478
3479 // Delete the file but do NOT trim from inside the guest: reclaiming the freed blocks now
3480 // depends on the trim that '--compact' performs on the host before compacting the VHD.
3481 std::tie(out, err) =
3482 LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} -u root -- sh -c 'rm /root/vhdx-compact-test/nonzero.bin && sync'", name));
3483 VERIFY_ARE_EQUAL(err, L"");
3484 WslShutdown();
3485
3486 const auto sizeBeforeCompact = getVhdSizeOnDisk(vhdPath);
3487
3488 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name));
3489 VERIFY_ARE_EQUAL(err, L"");
3490
3491 const auto sizeAfterCompact = getVhdSizeOnDisk(vhdPath);
3492 LogInfo(
3493 "Compact test VHD size on disk: before write=%llu, after write=%llu, before compact=%llu, after compact=%llu",
3494 static_cast<unsigned long long>(sizeBeforeWrite),
3495 static_cast<unsigned long long>(sizeAfterWrite),
3496 static_cast<unsigned long long>(sizeBeforeCompact),
3497 static_cast<unsigned long long>(sizeAfterCompact));
3498
3499 VERIFY_IS_TRUE(sizeBeforeCompact >= sizeAfterCompact);
3500 VERIFY_IS_TRUE(sizeAfterWrite >= sizeAfterCompact + minimumCompactionDelta);
3501
3502 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name));
3503 VERIFY_ARE_EQUAL(err, L"");
3504
3505 std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name));
3506 VERIFY_ARE_EQUAL(out, L"ok\n");
3507 VERIFY_ARE_EQUAL(err, L"");
3508 }
3509
3510 WSL2_TEST_METHOD(FileOffsets)
3511 {
3512 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(L"output.txt"); });
3513
3514 std::ofstream file("output.txt");
3515 VERIFY_IS_TRUE(file.good() && file << "previous content\n");
3516 file.close();
3517
3518 std::wstring cmd(L"C:\\windows\\system32\\cmd.exe /c \"wsl.exe echo ok >> output.txt && type output.txt\"");
3519 auto [output, _] = LxsstuLaunchCommandAndCaptureOutput(cmd.data());
3520
3521 VERIFY_ARE_EQUAL(output, L"previous content\r\nok\n");
3522 }
3523
3524 TEST_METHOD(GlobalFlagsOverride)
3525 {
3526 auto isDriveMountingEnabled = []() { return LxsstuLaunchWsl(L"test -d /mnt/c/Windows") == 0; };
3527
3528 VERIFY_IS_TRUE(isDriveMountingEnabled());
3529
3530 {
3531 RegistryKeyChange<DWORD> key(HKEY_LOCAL_MACHINE, LXSS_SERVICE_REGISTRY_PATH, L"DistributionFlags", ~LXSS_DISTRO_FLAGS_ENABLE_DRIVE_MOUNTING);
3532
3533 TerminateDistribution();
3534 VERIFY_IS_FALSE(isDriveMountingEnabled());
3535 }
3536
3537 TerminateDistribution();
3538 VERIFY_IS_TRUE(isDriveMountingEnabled());
3539 }
3540
3541 WSL2_TEST_METHOD(WriteWslConfig)
3542 {
3543 WSL_SETTINGS_TEST();
3544
3545 auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
3546 VERIFY_IS_TRUE(installPath.has_value());
3547
3548 std::filesystem::path wslInstallPath(installPath.value());
3549 std::filesystem::path libWslDllPath = wslInstallPath / "libwsl.dll";
3550 VERIFY_IS_TRUE(std::filesystem::exists(libWslDllPath));
3551
3552 LxssDynamicFunction<decltype(GetWslConfigFilePath)> getWslConfigFilePath(libWslDllPath.c_str(), "GetWslConfigFilePath");
3553 LxssDynamicFunction<decltype(CreateWslConfig)> createWslConfig(libWslDllPath.c_str(), "CreateWslConfig");
3554 LxssDynamicFunction<decltype(FreeWslConfig)> freeWslConfig(libWslDllPath.c_str(), "FreeWslConfig");
3555 LxssDynamicFunction<decltype(GetWslConfigSetting)> getWslConfigSetting(libWslDllPath.c_str(), "GetWslConfigSetting");
3556 LxssDynamicFunction<decltype(SetWslConfigSetting)> setWslConfigSetting(libWslDllPath.c_str(), "SetWslConfigSetting");
3557
3558 // Reset the test config file. The original has already been saved as part of module setup.
3559 auto wslConfigFilePath = getenv("userprofile") + std::string("\\.wslconfig");
3560 WslConfigChange config{L""};
3561
3562 auto apiWslConfigFilePath = getWslConfigFilePath();
3563 VERIFY_IS_TRUE(std::filesystem::path(wslConfigFilePath) == std::filesystem::path(apiWslConfigFilePath));
3564
3565 auto wslConfigDefaults = createWslConfig(nullptr);
3566 VERIFY_IS_NOT_NULL(wslConfigDefaults);
3567 auto wslConfig = createWslConfig(apiWslConfigFilePath);
3568 VERIFY_IS_NOT_NULL(wslConfig);
3569
3570 freeWslConfig(wslConfigDefaults);
3571 freeWslConfig(wslConfig);
3572
3573 WslConfigSetting wslConfigSettingWriteOut;
3574 WslConfigSetting wslConfigSettingReadIn;
3575
3576 auto testLoop = [&](auto& testPlan, auto& updateWslConfigSettingWriteOutValue, auto& verifyWslConfigSettingValueReadEqual) {
3577 wslConfigSettingWriteOut = wslConfigSettingReadIn = WslConfigSetting{};
3578 for (const auto testEntry : testPlan)
3579 {
3580 wslConfigSettingWriteOut = testEntry.first;
3581 for (const auto& test : testEntry.second)
3582 {
3583 const auto& writeValue = test.first;
3584 const auto& expectedValue = test.second;
3585 {
3586 // This scenario tests writing a value to the config file and reading it back. If the write succeeded,
3587 // the written value will be cached in the WslConfig object. The read will then return the cached value.
3588 wslConfig = createWslConfig(apiWslConfigFilePath);
3589 VERIFY_IS_NOT_NULL(wslConfig);
3590 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
3591
3592 updateWslConfigSettingWriteOutValue(wslConfigSettingWriteOut, writeValue);
3593
3594 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
3595 wslConfigSettingReadIn = getWslConfigSetting(wslConfig, wslConfigSettingWriteOut.ConfigEntry);
3596 VERIFY_ARE_EQUAL(wslConfigSettingReadIn.ConfigEntry, wslConfigSettingWriteOut.ConfigEntry);
3597 verifyWslConfigSettingValueReadEqual(wslConfigSettingReadIn, expectedValue);
3598 }
3599 {
3600 // This scenario tests reading a value from the config file. Specifically, it will parse in the
3601 // written value to the wsl config file from the previous scenario. This validates parsing the value
3602 // from the file (e.g. that it was written correctly and then parsed as expected).
3603 wslConfig = createWslConfig(apiWslConfigFilePath);
3604 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
3605 wslConfigSettingReadIn = getWslConfigSetting(wslConfig, wslConfigSettingWriteOut.ConfigEntry);
3606 VERIFY_ARE_EQUAL(wslConfigSettingReadIn.ConfigEntry, wslConfigSettingWriteOut.ConfigEntry);
3607 verifyWslConfigSettingValueReadEqual(wslConfigSettingReadIn, expectedValue);
3608 }
3609 }
3610 }
3611 };
3612
3613 {
3614 // Enable NetworkingMode::Mirrored for IgnoredPorts to be set correctly upon parsing.
3615 WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
3616
3617 // std::pair[0] = Written value, std::pair[1] = Actual/Expected value
3618 static const std::vector<std::pair<PCWSTR, PCWSTR>> filePathsToTest{
3619 {L"C:\\DoesNotExit\\ext4.vhdx", L"C:\\DoesNotExit\\ext4.vhdx"},
3620 {L"\\DoesNotExit\\ext4.vhdx", L"\\DoesNotExit\\ext4.vhdx"},
3621 {L"", L""},
3622 };
3623
3624 // tuple: WslConfigSetting, expectedValue, actualValue
3625 std::vector<std::pair<WslConfigSetting, std::vector<std::pair<PCWSTR, PCWSTR>>>> wslConfigSettingStringTestPlan{
3626 {
3627 {.ConfigEntry = WslConfigEntry::SwapFilePath},
3628 filePathsToTest,
3629 },
3630 {
3631 {.ConfigEntry = WslConfigEntry::IgnoredPorts},
3632 {
3633 {L"1,2,300,4455,65535", L"1,2,300,4455,65535"},
3634 {L"10,20,-100,p", L"10,20"},
3635 {L"100,200,notaport", L"100,200"},
3636 {L"1000,2000;3.4", L"1000,2000"},
3637 {L"10000, 20000, 30000,40000 ,50000", L"10000,20000,30000,40000,50000"},
3638 {L"", L""},
3639 {L"notaport", L""},
3640 {L"-5555", L""},
3641 {L"C:\\DoesNotExit\\ext4.vhdx", L""},
3642 },
3643 },
3644 {
3645 {.ConfigEntry = WslConfigEntry::KernelPath},
3646 filePathsToTest,
3647 },
3648 {
3649 {.ConfigEntry = WslConfigEntry::SystemDistroPath},
3650 filePathsToTest,
3651 },
3652 };
3653
3654 auto updateWslConfigSettingWriteOutStringValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
3655 wslConfigSettingWriteOut.StringValue = writeValue;
3656 };
3657
3658 auto verifyWslConfigSettingReadStringValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
3659 VERIFY_ARE_EQUAL(std::wstring_view(wslConfigSettingReadIn.StringValue), std::wstring_view(expectedValue));
3660 };
3661
3662 testLoop(wslConfigSettingStringTestPlan, updateWslConfigSettingWriteOutStringValue, verifyWslConfigSettingReadStringValueEqual);
3663 }
3664
3665 {
3666 wslConfigSettingWriteOut = wslConfigSettingReadIn = WslConfigSetting{};
3667 wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::NoEntry;
3668
3669 wslConfig = createWslConfig(apiWslConfigFilePath);
3670 VERIFY_IS_NOT_NULL(wslConfig);
3671 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
3672
3673 wslConfigSettingReadIn = getWslConfigSetting(wslConfig, wslConfigSettingWriteOut.ConfigEntry);
3674 VERIFY_ARE_EQUAL(wslConfigSettingReadIn.ConfigEntry, wslConfigSettingWriteOut.ConfigEntry);
3675 }
3676
3677 SYSTEM_INFO systemInfo{};
3678 GetSystemInfo(&systemInfo);
3679 {
3680 // std::pair[0] = Written value, std::pair[1] = Actual/Expected value
3681 static const std::vector<std::pair<int, int>> timeoutValuesToTest{
3682 {-132445, -132445},
3683 {0, 0},
3684 {1, 1},
3685 {13456, 13456},
3686 {100000000, 100000000},
3687 };
3688
3689 // tuple: WslConfigSetting, expectedValue, actualValue
3690 std::vector<std::pair<WslConfigSetting, std::vector<std::pair<int, int>>>> wslConfigSettingInt32TestPlan{
3691 {
3692 {.ConfigEntry = WslConfigEntry::ProcessorCount},
3693 {
3694 {-123443, systemInfo.dwNumberOfProcessors},
3695 {-1, systemInfo.dwNumberOfProcessors},
3696 {1, 1},
3697 {2, std::min(2, static_cast<int>(systemInfo.dwNumberOfProcessors))},
3698 {systemInfo.dwNumberOfProcessors, systemInfo.dwNumberOfProcessors},
3699 {1234, systemInfo.dwNumberOfProcessors},
3700 },
3701 },
3702 {
3703 {.ConfigEntry = WslConfigEntry::InitialAutoProxyTimeout},
3704 timeoutValuesToTest,
3705 },
3706 {
3707 {.ConfigEntry = WslConfigEntry::VMIdleTimeout},
3708 timeoutValuesToTest,
3709 },
3710 };
3711
3712 auto updateWslConfigSettingWriteOutInt32Value = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
3713 wslConfigSettingWriteOut.Int32Value = writeValue;
3714 };
3715
3716 auto verifyWslConfigSettingReadInt32ValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
3717 VERIFY_ARE_EQUAL(wslConfigSettingReadIn.Int32Value, expectedValue);
3718 };
3719
3720 testLoop(wslConfigSettingInt32TestPlan, updateWslConfigSettingWriteOutInt32Value, verifyWslConfigSettingReadInt32ValueEqual);
3721 }
3722
3723 {
3724 MEMORYSTATUSEX memInfo{sizeof(MEMORYSTATUSEX)};
3725 THROW_IF_WIN32_BOOL_FALSE(GlobalMemoryStatusEx(&memInfo));
3726 const auto minimumMemorySizeBytes = 256 * _1MB;
3727 const auto maximumMemorySizeBytes = memInfo.ullTotalPhys;
3728
3729 // std::pair[0] = Written value, std::pair[1] = Actual/Expected value
3730 static const std::vector<std::pair<unsigned long long, unsigned long long>> fileSizesBytesToTest{
3731 {0, 0}, {1, 1}, {13456, 13456}, {100000000, 100000000}, {9223372036854775807, 9223372036854775807}};
3732
3733 // tuple: WslConfigSetting, expectedValue, actualValue
3734 std::vector<std::pair<WslConfigSetting, std::vector<std::pair<unsigned long long, unsigned long long>>>> wslConfigSettingUInt64TestPlan{
3735 {
3736 {.ConfigEntry = WslConfigEntry::MemorySizeBytes},
3737 {
3738 {0, maximumMemorySizeBytes / 2},
3739 {minimumMemorySizeBytes / 2, minimumMemorySizeBytes},
3740 {minimumMemorySizeBytes, minimumMemorySizeBytes},
3741 {maximumMemorySizeBytes / 2, maximumMemorySizeBytes / 2},
3742 {maximumMemorySizeBytes, maximumMemorySizeBytes},
3743 {maximumMemorySizeBytes * 2, maximumMemorySizeBytes},
3744 },
3745 },
3746 {
3747 {.ConfigEntry = WslConfigEntry::SwapSizeBytes},
3748 fileSizesBytesToTest,
3749 },
3750 {
3751 {.ConfigEntry = WslConfigEntry::VhdSizeBytes},
3752 fileSizesBytesToTest,
3753 },
3754 };
3755
3756 auto updateWslConfigSettingWriteOutUInt64Value = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
3757 wslConfigSettingWriteOut.UInt64Value = writeValue;
3758 };
3759
3760 auto verifyWslConfigSettingReadUInt64ValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
3761 VERIFY_ARE_EQUAL(wslConfigSettingReadIn.UInt64Value, expectedValue);
3762 };
3763
3764 testLoop(wslConfigSettingUInt64TestPlan, updateWslConfigSettingWriteOutUInt64Value, verifyWslConfigSettingReadUInt64ValueEqual);
3765 }
3766
3767 {
3768 // Enable NetworkingMode::Mirrored for IgnoredPorts to be set correctly upon parsing.
3769 WslConfigChange config(LxssGenerateTestConfig());
3770
3771 // std::pair[0] = Written value, std::pair[1] = Actual/Expected value
3772 static const std::vector<std::pair<bool, bool>> booleansToTest{{false, false}, {true, true}};
3773
3774 // tuple: WslConfigSetting, expectedValue, actualValue
3775 std::vector<std::pair<WslConfigSetting, std::vector<std::pair<bool, bool>>>> wslConfigSettingBooleanTestPlan{
3776 {
3777 {.ConfigEntry = WslConfigEntry::FirewallEnabled},
3778 booleansToTest,
3779 },
3780 {
3781 {.ConfigEntry = WslConfigEntry::LocalhostForwardingEnabled},
3782 booleansToTest,
3783 },
3784 {
3785 {.ConfigEntry = WslConfigEntry::HostAddressLoopbackEnabled},
3786 // This setting is only enabled when NetworkingMode != Mirrored.
3787 {{false, false}, {true, false}},
3788 },
3789 {
3790 {.ConfigEntry = WslConfigEntry::AutoProxyEnabled},
3791 booleansToTest,
3792 },
3793 {
3794 {.ConfigEntry = WslConfigEntry::DNSProxyEnabled},
3795 booleansToTest,
3796 },
3797 {
3798 {.ConfigEntry = WslConfigEntry::DNSTunnelingEnabled},
3799 // This setting is only enabled when NetworkingMode != Nat && NetworkingMode != Mirrored
3800 booleansToTest,
3801 },
3802 {
3803 {.ConfigEntry = WslConfigEntry::BestEffortDNSParsingEnabled},
3804 // This setting is only enabled when DNSTunnelingEnabled = true
3805 booleansToTest,
3806 },
3807 {
3808 {.ConfigEntry = WslConfigEntry::GUIApplicationsEnabled},
3809 booleansToTest,
3810 },
3811 {
3812 {.ConfigEntry = WslConfigEntry::NestedVirtualizationEnabled},
3813 booleansToTest,
3814 },
3815 {
3816 {.ConfigEntry = WslConfigEntry::SafeModeEnabled},
3817 booleansToTest,
3818 },
3819 {
3820 {.ConfigEntry = WslConfigEntry::SparseVHDEnabled},
3821 booleansToTest,
3822 },
3823 {
3824 {.ConfigEntry = WslConfigEntry::DebugConsoleEnabled},
3825 booleansToTest,
3826 },
3827 {
3828 {.ConfigEntry = WslConfigEntry::HardwarePerformanceCountersEnabled},
3829 // This setting is disabled when SafeModeEnabled = true.
3830 // Since testing SafeModeEnabled is tested earlier and left as
3831 // true (.wslconfig is re-used), this setting should be false.
3832 {{false, false}, {true, false}},
3833 },
3834 };
3835
3836 auto updateWslConfigSettingWriteOutBooleanValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
3837 wslConfigSettingWriteOut.BoolValue = writeValue;
3838 };
3839
3840 auto verifyWslConfigSettingReadBooleanValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
3841 VERIFY_ARE_EQUAL(wslConfigSettingReadIn.BoolValue, expectedValue);
3842 };
3843
3844 testLoop(wslConfigSettingBooleanTestPlan, updateWslConfigSettingWriteOutBooleanValue, verifyWslConfigSettingReadBooleanValueEqual);
3845 }
3846
3847 {
3848 // std::pair[0] = Written value, std::pair[1] = Actual/Expected value
3849 static const std::vector<std::pair<NetworkingConfiguration, NetworkingConfiguration>> networkingConfigurationsToTest{
3850 {NetworkingConfiguration::None, NetworkingConfiguration::None},
3851 {NetworkingConfiguration::Nat, NetworkingConfiguration::Nat},
3852 {NetworkingConfiguration::Bridged, NetworkingConfiguration::Bridged},
3853 {NetworkingConfiguration::Mirrored, NetworkingConfiguration::Mirrored},
3854 {NetworkingConfiguration::Consomme, NetworkingConfiguration::Consomme},
3855 };
3856
3857 // tuple: WslConfigSetting, expectedValue, actualValue
3858 std::vector<std::pair<WslConfigSetting, std::vector<std::pair<NetworkingConfiguration, NetworkingConfiguration>>>> wslConfigSettingNetworkingConfigurationTestPlan{
3859 {
3860 {.ConfigEntry = WslConfigEntry::Networking},
3861 networkingConfigurationsToTest,
3862 },
3863 };
3864
3865 auto updateWslConfigSettingWriteOutNetworkingConfigurationValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
3866 wslConfigSettingWriteOut.NetworkingConfigurationValue = writeValue;
3867 };
3868
3869 auto verifyWslConfigSettingReadNetworkingConfigurationValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
3870 VERIFY_ARE_EQUAL(expectedValue, wslConfigSettingReadIn.NetworkingConfigurationValue);
3871 };
3872
3873 testLoop(
3874 wslConfigSettingNetworkingConfigurationTestPlan,
3875 updateWslConfigSettingWriteOutNetworkingConfigurationValue,
3876 verifyWslConfigSettingReadNetworkingConfigurationValueEqual);
3877 }
3878
3879 {
3880 // std::pair[0] = Written value, std::pair[1] = Actual/Expected value
3881 static const std::vector<std::pair<MemoryReclaimConfiguration, MemoryReclaimConfiguration>> memoryReclaimModesToTest{
3882 {MemoryReclaimConfiguration::Disabled, MemoryReclaimConfiguration::Disabled},
3883 {MemoryReclaimConfiguration::Gradual, MemoryReclaimConfiguration::Gradual},
3884 {MemoryReclaimConfiguration::DropCache, MemoryReclaimConfiguration::DropCache},
3885 };
3886
3887 // tuple: WslConfigSetting, expectedValue, actualValue
3888 std::vector<std::pair<WslConfigSetting, std::vector<std::pair<MemoryReclaimConfiguration, MemoryReclaimConfiguration>>>> wslConfigSettingMemoryReclaimModeTestPlan{
3889 {
3890 {.ConfigEntry = WslConfigEntry::AutoMemoryReclaim},
3891 memoryReclaimModesToTest,
3892 },
3893 };
3894
3895 auto updateWslConfigSettingWriteOutMemoryReclaimModeValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
3896 wslConfigSettingWriteOut.MemoryReclaimModeValue = writeValue;
3897 };
3898
3899 auto verifyWslConfigSettingReadMemoryReclaimModeValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
3900 VERIFY_ARE_EQUAL(wslConfigSettingReadIn.MemoryReclaimModeValue, expectedValue);
3901 };
3902
3903 testLoop(wslConfigSettingMemoryReclaimModeTestPlan, updateWslConfigSettingWriteOutMemoryReclaimModeValue, verifyWslConfigSettingReadMemoryReclaimModeValueEqual);
3904 }
3905
3906 {
3907 std::wstring customWslConfigContentOut{
3908 LR"(
3909 [wsl2] # trailing section comment
3910 vmIdleTimeout=200 # property trailing comment
3911 vmIdleTimeout=20000 # property trailing comment
3912 vmIdleTimeout=20000 # property trailing comment
3913 mountDeviceTimeout=120\
3914 000
3915 kernelBootTimeout=120000
3916
3917 # property comment
3918 swapfile=E:\\wsl-b\
3919 uild\\src\\win\
3920 dows\\wslc\
3921 ore\\lib\\swap.vhdx # multi-line property with trailing comment
3922 telemetry=false
3923 safeMode=false
3924 guiApplications=true
3925 earlyBootLogging=false
3926 # comment 1
3927 # comment 2
3928 # \t \b
3929 virtio9p=true # property trailing comment, ensure new property is appended to the section while preserving this comment
3930
3931 # section comment
3932 [experimental]
3933 autoProxy=false
3934
3935 [wsl2]
3936
3937 # end comment
3938 )"};
3939
3940 WslConfigChange config(customWslConfigContentOut);
3941
3942 wslConfig = createWslConfig(apiWslConfigFilePath);
3943 VERIFY_IS_NOT_NULL(wslConfig);
3944 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
3945
3946 // The config contains multiple vmIdleTimeout entries. The first one should be updated/written.
3947 wslConfigSettingWriteOut = WslConfigSetting{};
3948 wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::VMIdleTimeout;
3949 wslConfigSettingWriteOut.Int32Value = 1234;
3950
3951 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
3952
3953 // Replace the swapfile path, which is a multi-line property with a trailing comment.
3954 // The multi-line value should be replaced with the new value and trailing comment preserved.
3955 wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::SwapFilePath;
3956 wslConfigSettingWriteOut.StringValue = LR"(C:\DoesNotExist\swap.vhdx)";
3957
3958 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
3959
3960 // Write out a new setting that doesn't exist in the original config but its section
3961 // does. The new setting should be appended to that section. There are two cases here::
3962 wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::HardwarePerformanceCountersEnabled;
3963 wslConfigSettingWriteOut.BoolValue = true;
3964
3965 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
3966
3967 wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::AutoMemoryReclaim;
3968 wslConfigSettingWriteOut.MemoryReclaimModeValue = MemoryReclaimConfiguration::Gradual;
3969
3970 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
3971
3972 std::wstring customWslConfigContentExpected{
3973 LR"(
3974 [wsl2] # trailing section comment
3975 vmIdleTimeout=1234 # property trailing comment
3976 vmIdleTimeout=20000 # property trailing comment
3977 vmIdleTimeout=20000 # property trailing comment
3978 mountDeviceTimeout=120\
3979 000
3980 kernelBootTimeout=120000
3981
3982 # property comment
3983 swapfile=C:\\DoesNotExist\\swap.vhdx # multi-line property with trailing comment
3984 telemetry=false
3985 safeMode=false
3986 guiApplications=true
3987 earlyBootLogging=false
3988 # comment 1
3989 # comment 2
3990 # \t \b
3991 virtio9p=true # property trailing comment, ensure new property is appended to the section while preserving this comment
3992
3993 # section comment
3994 [experimental]
3995 autoProxy=false
3996 autoMemoryReclaim=Gradual
3997
3998 [wsl2]
3999
4000 # end comment
4001 )"};
4002
4003 std::wifstream configRead(apiWslConfigFilePath);
4004 auto customWslConfigContentActual = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
4005 configRead.close();
4006 VERIFY_ARE_EQUAL(customWslConfigContentExpected, customWslConfigContentActual);
4007 }
4008
4009 {
4010 // This test contains an invalid line ('babyshark') in the wsl2 section.
4011 // The line should be preserved and no additional spacing/lines should be added.
4012 std::wstring customWslConfigContentOut{
4013 LR"(
4014 [wsl2]
4015 memory=32G
4016 processors=12
4017 hostAddressLoopback=false
4018 dnsTunneling=true
4019 defaultVhdSize=1099511627776
4020 babyshark
4021 localhostForwarding=true
4022 autoProxy=false
4023 )"};
4024
4025 WslConfigChange config(customWslConfigContentOut);
4026
4027 wslConfig = createWslConfig(apiWslConfigFilePath);
4028 VERIFY_IS_NOT_NULL(wslConfig);
4029 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
4030
4031 auto wslConfigSetting = getWslConfigSetting(wslConfig, WslConfigEntry::AutoProxyEnabled);
4032 const auto autoProxyEnabled = false;
4033 VERIFY_ARE_EQUAL(wslConfigSetting.BoolValue, autoProxyEnabled);
4034
4035 wslConfigSetting.BoolValue = !autoProxyEnabled;
4036 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSetting), ERROR_SUCCESS);
4037
4038 std::wstring customWslConfigContentExpected{
4039 LR"(
4040 [wsl2]
4041 memory=32G
4042 processors=12
4043 hostAddressLoopback=false
4044 dnsTunneling=true
4045 defaultVhdSize=1099511627776
4046 babyshark
4047 localhostForwarding=true
4048 )"};
4049
4050 std::wifstream configRead(apiWslConfigFilePath);
4051 auto customWslConfigContentActual = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
4052 configRead.close();
4053 VERIFY_ARE_EQUAL(customWslConfigContentActual, customWslConfigContentExpected);
4054 }
4055
4056 {
4057 // This test verifies removal of a setting from the .wslconfig when a default value for the particular setting is
4058 // set. This gives wsl control over the default value.
4059 std::wstring customWslConfigContentOut{
4060 LR"(
4061 [wsl2]
4062 memory=32G
4063 processors=12 # property trailing comment
4064 hostAddressLoopback=false
4065 dnsTunneling=true
4066 defaultVhdSize=1099511627776
4067 localhostForwarding=true
4068 autoProxy=false
4069 )"};
4070
4071 WslConfigChange config(customWslConfigContentOut);
4072
4073 wslConfig = createWslConfig(apiWslConfigFilePath);
4074 VERIFY_IS_NOT_NULL(wslConfig);
4075 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
4076
4077 wslConfigDefaults = createWslConfig(nullptr);
4078 VERIFY_IS_NOT_NULL(wslConfigDefaults);
4079 auto cleanupWslConfigDefaults = wil::scope_exit([&] { freeWslConfig(wslConfigDefaults); });
4080
4081 // This setting should be removed from the .wslconfig file.
4082 auto wslConfigDefaultSettingMemorySize = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::MemorySizeBytes);
4083 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingMemorySize), ERROR_SUCCESS);
4084
4085 // This setting should be removed from the .wslconfig file but trailing comment preserved.
4086 auto wslConfigDefaultSettingProcessorCount = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::ProcessorCount);
4087 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingProcessorCount), ERROR_SUCCESS);
4088
4089 // This setting should be preserved with an updated value in the .wslconfig file.
4090 auto wslConfigDefaultSettingVhdSize = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::VhdSizeBytes);
4091 wslConfigDefaultSettingVhdSize.UInt64Value -= 1;
4092 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingVhdSize), ERROR_SUCCESS);
4093
4094 // This setting should be removed from the .wslconfig file.
4095 auto wslConfigDefaultSettingAutoProxy = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::AutoProxyEnabled);
4096 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingAutoProxy), ERROR_SUCCESS);
4097
4098 // This setting should not be written to the .wslconfig file.
4099 auto wslConfigDefaultSettingGuiApplications = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::GUIApplicationsEnabled);
4100 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingGuiApplications), ERROR_SUCCESS);
4101
4102 std::wstring customWslConfigContentExpected{
4103 LR"(
4104 [wsl2]
4105 # property trailing comment
4106 hostAddressLoopback=false
4107 dnsTunneling=true
4108 defaultVhdSize=1099511627775
4109 localhostForwarding=true
4110 )"};
4111
4112 std::wifstream configRead(apiWslConfigFilePath);
4113 auto customWslConfigContentActual = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
4114 configRead.close();
4115 VERIFY_ARE_EQUAL(customWslConfigContentActual, customWslConfigContentExpected);
4116 }
4117
4118 // Regression test for GitHub issue #12671:
4119 // Ensure that section headers always appear BEFORE their key-value pairs.
4120 // Bug: WSL Settings GUI was writing keys before the section header, causing "Unknown key" errors.
4121 {
4122 std::wstring bugScenarioConfig =
4123 LR"([wsl2]
4124 [experimental]
4125 [wsl2]
4126 )";
4127 WslConfigChange config{bugScenarioConfig.c_str()};
4128
4129 wslConfig = createWslConfig(apiWslConfigFilePath);
4130 VERIFY_IS_NOT_NULL(wslConfig);
4131 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
4132
4133 // Write memory setting - this should NOT appear before the first [wsl2]
4134 WslConfigSetting memorySetting{};
4135 memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
4136 memorySetting.UInt64Value = 17825792000ULL; // Value from bug report
4137
4138 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
4139
4140 // Read and verify
4141 std::wifstream configRead(apiWslConfigFilePath);
4142 std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
4143 configRead.close();
4144
4145 // Find FIRST occurrence of [wsl2] and memory=
4146 auto firstWsl2Pos = fileContent.find(L"[wsl2]");
4147 auto memoryPos = fileContent.find(L"memory=");
4148
4149 VERIFY_ARE_NOT_EQUAL(firstWsl2Pos, std::wstring::npos);
4150 VERIFY_ARE_NOT_EQUAL(memoryPos, std::wstring::npos);
4151
4152 // The critical assertion: memory= must NOT appear before [wsl2]
4153 VERIFY_IS_TRUE(firstWsl2Pos < memoryPos);
4154
4155 // Additional check: memory should appear after the first [wsl2], not after line 1
4156 auto firstLineEnd = fileContent.find(L'\n');
4157 VERIFY_IS_TRUE(memoryPos > firstLineEnd);
4158 }
4159
4160 // Test: Empty file - should create proper [wsl2] section structure
4161 {
4162 std::wofstream emptyConfig(apiWslConfigFilePath, std::ios::trunc);
4163 emptyConfig.close();
4164
4165 wslConfig = createWslConfig(apiWslConfigFilePath);
4166 VERIFY_IS_NOT_NULL(wslConfig);
4167 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
4168
4169 WslConfigSetting memorySetting{};
4170 memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
4171 memorySetting.UInt64Value = 4294967296ULL; // 4GB
4172 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
4173
4174 std::wifstream configRead(apiWslConfigFilePath);
4175 std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
4176 configRead.close();
4177
4178 // Should create [wsl2] section and add memory key
4179 VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") != std::wstring::npos);
4180 VERIFY_IS_TRUE(fileContent.find(L"memory=") != std::wstring::npos);
4181 // Verify [wsl2] comes before memory=
4182 VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") < fileContent.find(L"memory="));
4183 }
4184
4185 // Test: Multiple same-section instances - should update first occurrence
4186 {
4187 std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
4188 configFile << L"[wsl2]\n";
4189 configFile << L"processors=4\n";
4190 configFile << L"\n";
4191 configFile << L"[experimental]\n";
4192 configFile << L"autoProxy=true\n";
4193 configFile << L"\n";
4194 configFile << L"[wsl2]\n"; // Second [wsl2] section
4195 configFile << L"swap=0\n";
4196 configFile.close();
4197
4198 wslConfig = createWslConfig(apiWslConfigFilePath);
4199 VERIFY_IS_NOT_NULL(wslConfig);
4200 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
4201
4202 WslConfigSetting memorySetting{};
4203 memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
4204 memorySetting.UInt64Value = 8589934592ULL; // 8GB
4205 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
4206
4207 std::wifstream configRead(apiWslConfigFilePath);
4208 std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
4209 configRead.close();
4210
4211 // Find first and second [wsl2]
4212 auto firstWsl2 = fileContent.find(L"[wsl2]");
4213 auto secondWsl2 = fileContent.find(L"[wsl2]", firstWsl2 + 1);
4214 auto memoryPos = fileContent.find(L"memory=");
4215
4216 VERIFY_ARE_NOT_EQUAL(firstWsl2, std::wstring::npos);
4217 VERIFY_ARE_NOT_EQUAL(secondWsl2, std::wstring::npos);
4218 VERIFY_ARE_NOT_EQUAL(memoryPos, std::wstring::npos);
4219
4220 // Memory should be added to FIRST [wsl2] section, not second
4221 VERIFY_IS_TRUE(memoryPos > firstWsl2);
4222 VERIFY_IS_TRUE(memoryPos < secondWsl2);
4223 }
4224
4225 // Test: EOF without trailing newline
4226 {
4227 std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
4228 configFile << L"[wsl2]\n";
4229 configFile << L"processors=2"; // No trailing newline
4230 configFile.close();
4231
4232 wslConfig = createWslConfig(apiWslConfigFilePath);
4233 VERIFY_IS_NOT_NULL(wslConfig);
4234 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
4235
4236 WslConfigSetting memorySetting{};
4237 memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
4238 memorySetting.UInt64Value = 3221225472ULL; // 3GB
4239 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
4240
4241 std::wifstream configRead(apiWslConfigFilePath);
4242 std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
4243 configRead.close();
4244
4245 // Should properly append memory key even without trailing newline on last line
4246 VERIFY_IS_TRUE(fileContent.find(L"processors=2") != std::wstring::npos);
4247 VERIFY_IS_TRUE(fileContent.find(L"memory=") != std::wstring::npos);
4248
4249 // Verify both keys are in the same section
4250 auto wsl2Pos = fileContent.find(L"[wsl2]");
4251 auto processorsPos = fileContent.find(L"processors=2");
4252 auto memoryPos = fileContent.find(L"memory=");
4253 VERIFY_IS_TRUE(wsl2Pos < processorsPos);
4254 VERIFY_IS_TRUE(wsl2Pos < memoryPos);
4255
4256 // Memory should come after processors in the same section
4257 VERIFY_IS_TRUE(processorsPos < memoryPos);
4258 }
4259
4260 // Test: Empty section followed by another section
4261 {
4262 std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
4263 configFile << L"[wsl2]\n";
4264 configFile << L"[experimental]\n";
4265 configFile << L"autoProxy=true\n";
4266 configFile.close();
4267
4268 wslConfig = createWslConfig(apiWslConfigFilePath);
4269 VERIFY_IS_NOT_NULL(wslConfig);
4270 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
4271
4272 WslConfigSetting memorySetting{};
4273 memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
4274 memorySetting.UInt64Value = 5368709120ULL; // 5GB
4275 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
4276
4277 std::wifstream configRead(apiWslConfigFilePath);
4278 std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
4279 configRead.close();
4280
4281 // Should insert memory into empty [wsl2] section before [experimental]
4282 auto wsl2Pos = fileContent.find(L"[wsl2]");
4283 auto memoryPos = fileContent.find(L"memory=");
4284 auto experimentalPos = fileContent.find(L"[experimental]");
4285
4286 VERIFY_ARE_NOT_EQUAL(wsl2Pos, std::wstring::npos);
4287 VERIFY_ARE_NOT_EQUAL(memoryPos, std::wstring::npos);
4288 VERIFY_ARE_NOT_EQUAL(experimentalPos, std::wstring::npos);
4289
4290 // Order should be: [wsl2], memory=, [experimental]
4291 VERIFY_IS_TRUE(wsl2Pos < memoryPos);
4292 VERIFY_IS_TRUE(memoryPos < experimentalPos);
4293 }
4294
4295 // Test: Section header at EOF with no content
4296 {
4297 std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
4298 configFile << L"[wsl2]"; // Section at EOF, no newline, no content
4299 configFile.close();
4300
4301 wslConfig = createWslConfig(apiWslConfigFilePath);
4302 VERIFY_IS_NOT_NULL(wslConfig);
4303 auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
4304
4305 WslConfigSetting memorySetting{};
4306 memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
4307 memorySetting.UInt64Value = 6442450944ULL; // 6GB
4308 VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
4309
4310 std::wifstream configRead(apiWslConfigFilePath);
4311 std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
4312 configRead.close();
4313
4314 // Should properly add key to section at EOF
4315 VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") != std::wstring::npos);
4316 VERIFY_IS_TRUE(fileContent.find(L"memory=") != std::wstring::npos);
4317 VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") < fileContent.find(L"memory="));
4318 }
4319 }
4320
4321 TEST_METHOD(LaunchWslSettingsFromProtocol)
4322 {
4323 WSL_SETTINGS_TEST();
4324
4325 SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
4326
4327 SHELLEXECUTEINFOW execInfo{};
4328 execInfo.cbSize = sizeof(execInfo);
4329 execInfo.fMask = SEE_MASK_CLASSNAME | SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS;
4330 execInfo.lpClass = L"wsl-settings";
4331 execInfo.lpFile = L"wsl-settings://";
4332 execInfo.nShow = SW_HIDE;
4333
4334 VERIFY_WIN32_BOOL_SUCCEEDED(ShellExecuteExW(&execInfo));
4335 const wil::unique_process_handle process{execInfo.hProcess};
4336 VERIFY_IS_NOT_NULL(process.get());
4337
4338 auto killProcess = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&process]() {
4339 if (process)
4340 {
4341 LOG_IF_WIN32_BOOL_FALSE(TerminateProcess(process.get(), 0));
4342 }
4343 });
4344
4345 const auto moduleFileName = wil::GetModuleFileNameExW<std::wstring>(process.get(), nullptr);
4346 const auto findExeName = moduleFileName.find(L"wslsettings.exe");
4347 VERIFY_ARE_NOT_EQUAL(findExeName, std::wstring::npos);
4348 }
4349
4350 TEST_METHOD(ManageDefaultUid)
4351 {
4352 const auto distroKey = OpenDistributionKey(LXSS_DISTRO_NAME_TEST_L);
4353
4354 auto assertDefaultUid = [&](ULONG ExpectedUid) {
4355 const auto uid = wsl::windows::common::registry::ReadDword(distroKey.get(), nullptr, L"DefaultUid", 0);
4356
4357 VERIFY_ARE_EQUAL(ExpectedUid, uid);
4358
4359 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"id -u");
4360 while (!out.empty() && (out.back() == '\n' || out.back() == '\r'))
4361 {
4362 out.pop_back();
4363 }
4364
4365 VERIFY_ARE_EQUAL(out, std::to_wstring(ExpectedUid));
4366 };
4367
4368 assertDefaultUid(0);
4369
4370 auto validateUidChange =
4371 [&](const std::wstring& User, ULONG expectedDefaultUid, LPCWSTR ExpectedOutput, const std::wstring& ExpectedError, int ExpectedExitCode) {
4372 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(
4373 std::format(L"--manage {} --set-default-user {}", LXSS_DISTRO_NAME_TEST_L, User), ExpectedExitCode);
4374
4375 VERIFY_ARE_EQUAL(out, ExpectedOutput);
4376 VERIFY_ARE_EQUAL(err, ExpectedError);
4377
4378 assertDefaultUid(expectedDefaultUid);
4379 };
4380
4381 validateUidChange(L"root", 0, L"The operation completed successfully. \r\n", L"", 0);
4382
4383 constexpr auto TestUser = L"testuser";
4384
4385 auto cleanup = wil::scope_exit_log(
4386 WI_DIAGNOSTICS_INFO, [TestUser]() { LxsstuLaunchWsl(std::format(L"-u root userdel -f {}", TestUser)); });
4387
4388 ULONG Uid{};
4389 ULONG Gid{};
4390 CreateUser(TestUser, &Uid, &Gid);
4391 VERIFY_ARE_NOT_EQUAL(Uid, 0);
4392
4393 validateUidChange(L"testuser", Uid, L"The operation completed successfully. \r\n", L"", 0);
4394 validateUidChange(L"root", 0, L"The operation completed successfully. \r\n", L"", 0);
4395
4396 const std::wstring invalidUser = L"Nonexistent";
4397 validateUidChange(invalidUser, 0, L"", L"id: \u2018" + invalidUser + L"\u2019: no such user\n", 1);
4398
4399 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage nonexistent --set-default-user root", -1);
4400
4401 VERIFY_ARE_EQUAL(
4402 out, FormatErrorMessage(L"There is no distribution with the supplied name.", L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND"));
4403
4404 constexpr auto injectionMarker = L"/tmp/wsl-manage-default-user-injection";
4405 LxsstuLaunchWsl(std::format(L"-u root -e /usr/bin/rm -f {}", injectionMarker));
4406 auto cleanupInjectionMarker = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [injectionMarker]() {
4407 LxsstuLaunchWsl(std::format(L"-u root -e /usr/bin/rm -f {}", injectionMarker));
4408 });
4409
4410 const auto injectionUsername = std::format(L"' || touch {} || '", injectionMarker);
4411 const std::array<std::wstring_view, 4> injectionArguments{
4412 WSL_MANAGE_ARG, LXSS_DISTRO_NAME_TEST_L, WSL_MANAGE_ARG_SET_DEFAULT_USER_OPTION_LONG, injectionUsername};
4413 const auto injectionCommand = wil::ArgvToCommandLine(injectionArguments, wil::ArgvToCommandLineFlags::FirstArgumentIsNotPath);
4414 auto injectionCommandLine = LxssGenerateWslCommandLine(injectionCommand.c_str());
4415 const auto injectionExitCode = LxsstuRunCommand(injectionCommandLine.data());
4416
4417 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-u root -e /usr/bin/test ! -e {}", injectionMarker)), 0L);
4418 VERIFY_ARE_EQUAL(injectionExitCode, 1L);
4419 }
4420
4421 TEST_METHOD(PostDistroRegistrationSettingsOOBE)
4422 {
4423 WSL_SETTINGS_TEST();
4424
4425 wsl::windows::common::SvcComm service;
4426 const auto distros = service.EnumerateDistributions();
4427 if (distros.size() != 1)
4428 {
4429 LogSkipped("Test distro as the only distro is required to run this test.");
4430 return;
4431 }
4432
4433 const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey();
4434 // Test setup should set OOBEComplete
4435 VERIFY_ARE_EQUAL(bool(wsl::windows::common::registry::ReadDword(lxssKey.get(), nullptr, LXSS_OOBE_COMPLETE_NAME, false)), true);
4436
4437 // Delete the OOBEComplete reg value to simulate OOBE not being complete
4438 wsl::windows::common::registry::DeleteValue(lxssKey.get(), LXSS_OOBE_COMPLETE_NAME);
4439
4440 // Restore the OOBEComplete reg value in case of failure
4441 auto restoreOOBEComplete = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
4442 wsl::windows::common::registry::WriteDword(lxssKey.get(), nullptr, LXSS_OOBE_COMPLETE_NAME, true);
4443 });
4444
4445 constexpr auto wslSettingsWindowName = L"Welcome to Windows Subsystem for Linux";
4446 VERIFY_ARE_EQUAL(FindWindowEx(nullptr, nullptr, nullptr, wslSettingsWindowName), nullptr);
4447
4448 auto testDistro = distros.front();
4449 VERIFY_IS_TRUE(wsl::shared::string::IsEqual(testDistro.DistroName, LXSS_DISTRO_NAME_TEST_L, false));
4450 // Get the original BasePath in order to restore the test distro as before.
4451 auto guidStringWithBraces = wsl::shared::string::GuidToString<wchar_t>(testDistro.DistroGuid);
4452 auto testDistroBasePath =
4453 wsl::windows::common::registry::ReadString(lxssKey.get(), guidStringWithBraces.c_str(), L"BasePath", L"");
4454 VERIFY_ARE_NOT_EQUAL(testDistroBasePath, L"");
4455
4456 if (LxsstuVmMode())
4457 {
4458 const auto testDistroVhdPath = std::filesystem::path(testDistroBasePath) / LXSS_VM_MODE_VHD_NAME;
4459 VERIFY_IS_TRUE(std::filesystem::exists(testDistroVhdPath));
4460 const auto testDistroVhdPathExported = std::filesystem::path(testDistroBasePath) / L"exported.vhdx";
4461
4462 WslShutdown();
4463 VERIFY_ARE_EQUAL(
4464 LxsstuLaunchWsl(std::format(L"--export {} \"{}\" --vhd", testDistro.DistroName, testDistroVhdPathExported.c_str())), 0u);
4465 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", testDistro.DistroName)), 0u);
4466 VERIFY_IS_FALSE(std::filesystem::exists(testDistroVhdPath));
4467 VERIFY_IS_TRUE(service.EnumerateDistributions().empty());
4468
4469 std::error_code ec{};
4470 std::filesystem::rename(testDistroVhdPathExported, testDistroVhdPath, ec);
4471
4472 VERIFY_ARE_EQUAL(
4473 LxsstuLaunchWsl(std::format(L"--import-in-place {} \"{}\"", testDistro.DistroName, testDistroVhdPath.c_str())), 0L);
4474 }
4475 else
4476 {
4477 const auto testDistroRootfsPath = std::filesystem::path(testDistroBasePath) / LXSS_ROOTFS_DIRECTORY;
4478 VERIFY_IS_TRUE(std::filesystem::exists(testDistroRootfsPath));
4479 const auto testDistroExported = std::filesystem::path(testDistroBasePath) / L"exported.tar";
4480 auto deleteTar = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteFile(testDistroExported.c_str()); });
4481
4482 WslShutdown();
4483 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export {} \"{}\"", testDistro.DistroName, testDistroExported.c_str())), 0u);
4484 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", testDistro.DistroName)), 0u);
4485 VERIFY_IS_FALSE(std::filesystem::exists(testDistroRootfsPath));
4486 VERIFY_IS_TRUE(service.EnumerateDistributions().empty());
4487 VERIFY_ARE_EQUAL(
4488 LxsstuLaunchWsl(std::format(
4489 L"--import {} \"{}\" \"{}\" --version 1", testDistro.DistroName, testDistroBasePath, testDistroExported.c_str())),
4490 0L);
4491 }
4492
4493 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-default {}", testDistro.DistroName)), 0);
4494
4495 VERIFY_ARE_EQUAL(service.EnumerateDistributions().size(), 1);
4496 HWND wslSettingsWindow{};
4497 const auto findWslSettingsWindowAttempts = 60;
4498 for (auto attempt = 0; attempt < findWslSettingsWindowAttempts; ++attempt)
4499 {
4500 wslSettingsWindow = FindWindowEx(nullptr, nullptr, nullptr, wslSettingsWindowName);
4501 if (wslSettingsWindow)
4502 {
4503 break;
4504 }
4505
4506 Sleep(500);
4507 }
4508
4509 VERIFY_ARE_NOT_EQUAL(wslSettingsWindow, nullptr);
4510 SendMessage(wslSettingsWindow, WM_CLOSE, 0, 0);
4511 VERIFY_ARE_EQUAL(bool(wsl::windows::common::registry::ReadDword(lxssKey.get(), nullptr, LXSS_OOBE_COMPLETE_NAME, false)), true);
4512 }
4513
4514 TEST_METHOD(VersionFlavorParsing)
4515 {
4516 DWORD currentVersion = LxsstuVmMode() ? 2 : 1;
4517 DWORD convertVersion = LxsstuVmMode() ? 1 : 2;
4518
4519 const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey();
4520
4521 auto validateFlavorVersion = [&](LPCWSTR Distro, LPCWSTR ExpectedFlavor, LPCWSTR ExpectedVersion) {
4522 const auto testDistroId = GetDistributionId(Distro);
4523 VERIFY_IS_TRUE(testDistroId.has_value());
4524
4525 const auto distroId = wsl::shared::string::GuidToString<wchar_t>(testDistroId.value());
4526
4527 TerminateDistribution(Distro);
4528 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} cat /etc/os-release || true", Distro).c_str()), 0L);
4529
4530 const auto flavor = wsl::windows::common::registry::ReadString(lxssKey.get(), distroId.c_str(), L"Flavor", L"");
4531 const auto version = wsl::windows::common::registry::ReadString(lxssKey.get(), distroId.c_str(), L"OsVersion", L"");
4532
4533 VERIFY_ARE_EQUAL(ExpectedFlavor, flavor);
4534 VERIFY_ARE_EQUAL(ExpectedVersion, version);
4535 };
4536
4537 validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"debian", L"13");
4538
4539 constexpr auto testTar = L"exported-distro.tar";
4540 constexpr auto tmpDistroName = L"tmpdistro";
4541
4542 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [tmpDistroName]() {
4543 DeleteFile(testTar);
4544 LxsstuLaunchWsl(std::format(L"--unregister {}", tmpDistroName));
4545 });
4546
4547 DistroFileChange osRelease(L"/etc/os-release");
4548
4549 {
4550 osRelease.SetContent(
4551 LR"(
4552 ID=Distro
4553 VERSION_ID=Version
4554 )");
4555
4556 validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro", L"Version");
4557 }
4558
4559 {
4560 osRelease.SetContent(
4561 LR"(
4562 DISTRO_I=Wrong
4563 ID="DistroWithQuotes"
4564 VERSION_ID="VersionWithQuotes"
4565 Something else
4566 )");
4567
4568 validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"DistroWithQuotes", L"VersionWithQuotes");
4569 }
4570
4571 {
4572 osRelease.SetContent(
4573 LR"(
4574 ID="InvalidFormat!"
4575 VERSION_ID="ValidFormat"
4576 )");
4577
4578 validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"DistroWithQuotes", L"ValidFormat");
4579 }
4580
4581 {
4582 osRelease.SetContent(
4583 LR"(
4584 ID="Distro-_.,"
4585 VERSION_ID="ValidFormat"
4586 )");
4587
4588 validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro-_.,", L"ValidFormat");
4589 }
4590
4591 {
4592 osRelease.SetContent(
4593 LR"(
4594 ID="Invalid|Format"
4595 VERSION_ID="Invalid|Format"
4596 )");
4597
4598 validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro-_.,", L"ValidFormat");
4599 }
4600
4601 {
4602 osRelease.Delete(); // Nothing should happen if the file is deleted, but the distro should still work.
4603 validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro-_.,", L"ValidFormat");
4604 }
4605
4606 // Validate that importing a distro without os-release works.
4607 {
4608 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export {} {}", LXSS_DISTRO_NAME_TEST_L, testTar).c_str()), 0L);
4609 VERIFY_ARE_EQUAL(
4610 LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, testTar, currentVersion).c_str()), 0L);
4611
4612 validateFlavorVersion(tmpDistroName, L"", L"");
4613
4614 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} echo -e 'VERSION_ID=v' > /etc/os-release", tmpDistroName).c_str()), 0L);
4615 validateFlavorVersion(tmpDistroName, L"", L"v");
4616 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", tmpDistroName).c_str()), 0L);
4617 }
4618
4619 // Validate that importing and then converting also behaves correctly when there's no os-release
4620 {
4621 VERIFY_ARE_EQUAL(
4622 LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, testTar, convertVersion).c_str()), 0L);
4623 validateFlavorVersion(tmpDistroName, L"", L"");
4624
4625 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-version {} {}", tmpDistroName, currentVersion).c_str()), 0L);
4626
4627 validateFlavorVersion(tmpDistroName, L"", L"");
4628
4629 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} echo -e 'VERSION_ID=v2' > /etc/os-release", tmpDistroName).c_str()), 0L);
4630 validateFlavorVersion(tmpDistroName, L"", L"v2");
4631 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", tmpDistroName).c_str()), 0L);
4632 }
4633
4634 // Verify that importing a distribution with an os-release as then converting works as well
4635 VERIFY_ARE_EQUAL(
4636 LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, g_testDistroPath, convertVersion).c_str()), 0L);
4637 validateFlavorVersion(tmpDistroName, L"debian", L"13");
4638
4639 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-version {} {}", tmpDistroName, currentVersion).c_str()), 0L);
4640 validateFlavorVersion(tmpDistroName, L"debian", L"13");
4641 }
4642
4643 TEST_METHOD(DistributionId)
4644 {
4645 using namespace wsl::windows::common::string;
4646 const auto testDistroId = GetDistributionId(LXSS_DISTRO_NAME_TEST_L);
4647 VERIFY_IS_TRUE(testDistroId.has_value());
4648
4649 auto validateOutput = [](const std::wstring& Cmd, const std::wstring& ExpectedOutput, int ExitCode = 0) {
4650 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(Cmd, ExitCode);
4651
4652 VERIFY_ARE_EQUAL(out, ExpectedOutput);
4653 };
4654
4655 validateOutput(
4656 std::format(
4657 L"--distribution-id {} echo -n OK",
4658 wsl::shared::string::GuidToString<wchar_t>(testDistroId.value(), wsl::shared::string::GuidToStringFlags::None)),
4659 L"OK");
4660
4661 validateOutput(
4662 std::format(
4663 L"--distribution-id {} echo -n OK",
4664 wsl::shared::string::GuidToString<wchar_t>(testDistroId.value(), wsl::shared::string::GuidToStringFlags::AddBraces)),
4665 L"OK");
4666
4667 validateOutput(
4668 std::format(
4669 L"--distribution-id {} echo -n OK",
4670 wsl::shared::string::GuidToString<wchar_t>(testDistroId.value(), wsl::shared::string::GuidToStringFlags::Uppercase)),
4671 L"OK");
4672
4673 validateOutput(L"--distribution-id InvalidGuid", FormatErrorMessage(L"The parameter is incorrect. ", L"Wsl/E_INVALIDARG"), -1);
4674 validateOutput(
4675 L"--distribution-id {C13B2B63-F9D5-4840-8105-F6ABECCF46CA}",
4676 FormatErrorMessage(
4677 L"There is no distribution with the supplied name.",
4678 L"Wsl/Service/CreateInstance/ReadDistroConfig/WSL_E_DISTRO_NOT_FOUND"),
4679 -1);
4680 }
4681
4682 TEST_METHOD(ModernOOBE)
4683 {
4684 const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey();
4685 const auto testDistroId = GetDistributionId(LXSS_DISTRO_NAME_TEST_L);
4686 VERIFY_IS_TRUE(testDistroId.has_value());
4687 const auto testDistroIdString = wsl::shared::string::GuidToString<wchar_t>(testDistroId.value());
4688
4689 DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
4690 distributionconf.SetContent(L"[oobe]\ncommand = /bin/bash -c 'echo OOBE'\n");
4691
4692 RegistryKeyChange<DWORD> runOOBE(lxssKey.get(), testDistroIdString.c_str(), L"RunOOBE", 1);
4693 const RegistryKeyChange<DWORD> defaultUid(lxssKey.get(), testDistroIdString.c_str(), L"DefaultUid", 0);
4694
4695 auto validateOutput = [](LPCWSTR Cmd, LPCWSTR ExpectedOutput, LPCWSTR ExpectedWarnings = L"", DWORD ExpectedExitCode = 0) {
4696 auto [read, write] = CreateSubprocessPipe(true, false);
4697 write.reset();
4698
4699 wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(Cmd).c_str());
4700 process.SetStdHandles(read.get(), nullptr, nullptr);
4701
4702 const auto output = process.RunAndCaptureOutput();
4703
4704 VERIFY_ARE_EQUAL(ExpectedExitCode, output.ExitCode);
4705
4706 VERIFY_ARE_EQUAL(ExpectedOutput, output.Stdout);
4707 VERIFY_ARE_EQUAL(ExpectedWarnings, output.Stderr);
4708 };
4709
4710 {
4711 TerminateDistribution();
4712
4713 // Non-interactive commands shouldn't trigger OOBE
4714 validateOutput(L"echo no oobe", L"no oobe\n");
4715 VERIFY_ARE_EQUAL(runOOBE.Get(), 1);
4716
4717 // Interactive shell should trigger OOBE
4718 validateOutput(nullptr, L"OOBE\n");
4719 VERIFY_ARE_EQUAL(runOOBE.Get(), 0);
4720
4721 // OOBE should only trigger once
4722 validateOutput(L"", L"");
4723 }
4724
4725 {
4726 runOOBE.Set(1);
4727 distributionconf.SetContent(L"[oobe]\ncommand = /bin/bash -c 'echo failed OOBE && exit 1'\n");
4728
4729 TerminateDistribution();
4730
4731 constexpr auto expectedStdErr = L"OOBE command \"/bin/bash -c 'echo failed OOBE && exit 1'\" failed, exiting\n";
4732
4733 validateOutput(nullptr, L"failed OOBE\n", expectedStdErr, 1);
4734 VERIFY_ARE_EQUAL(runOOBE.Get(), 1);
4735
4736 // Failed OOBE command should be retried
4737 TerminateDistribution();
4738 validateOutput(nullptr, L"failed OOBE\n", expectedStdErr, 1);
4739 VERIFY_ARE_EQUAL(runOOBE.Get(), 1);
4740 }
4741
4742 {
4743 runOOBE.Set(1);
4744 distributionconf.SetContent(
4745 L"[oobe]\ncommand = /bin/bash -c 'echo OOBE && useradd -u 1010 -m -s /bin/bash user'\n defaultUid = 1010\n");
4746
4747 TerminateDistribution();
4748
4749 validateOutput(nullptr, L"OOBE\n");
4750 VERIFY_ARE_EQUAL(runOOBE.Get(), 0);
4751
4752 // Validate that DefaultUid was set
4753 validateOutput(L"id -u", L"1010\n");
4754 VERIFY_ARE_EQUAL(defaultUid.Get(), 1010);
4755
4756 // New file should be created with the correct uid.
4757 const std::wstring testFilePathLinux = L"/tmp/oobe_file_test";
4758 const std::wstring testFilePathWindows = L"\\\\wsl.localhost\\" LXSS_DISTRO_NAME_TEST_L L"\\tmp\\oobe_file_test";
4759
4760 const wil::unique_hfile file(CreateFile(
4761 testFilePathWindows.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
4762 VERIFY_IS_TRUE(file.is_valid());
4763 validateOutput(std::format(L"stat -c %u {}", testFilePathLinux).c_str(), L"1010\n");
4764 }
4765
4766 // Verify that the default UID isn't changed if it's not present in wsl-distribution.conf.
4767 {
4768 runOOBE.Set(1);
4769
4770 distributionconf.SetContent(L"[oobe]\ncommand = /bin/bash -c 'echo OOBE'");
4771 TerminateDistribution();
4772
4773 validateOutput(nullptr, L"OOBE\n");
4774 VERIFY_ARE_EQUAL(defaultUid.Get(), 1010);
4775 }
4776
4777 // Verify that OOBE doesn't run if a distribution is installed via wsl --import
4778 {
4779 constexpr auto testDir = L"test-oobe-import";
4780 constexpr auto testDistroName = L"test-oobe-import";
4781
4782 std::filesystem::create_directory(testDir);
4783 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, testDistroName]() {
4784 LxsstuLaunchWsl(std::format(L"--unregister {}", testDistroName));
4785 std::error_code error;
4786 std::filesystem::remove_all(testDir, error);
4787 });
4788
4789 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export {} {}/exported.tar", LXSS_DISTRO_NAME_TEST_L, testDir)), 0L);
4790 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} {} {}/exported.tar", testDistroName, testDir, testDistroName)), 0L);
4791
4792 const auto distroKey = OpenDistributionKey(testDistroName);
4793
4794 VERIFY_ARE_EQUAL(wsl::windows::common::registry::ReadDword(distroKey.get(), nullptr, L"RunOOBE", 1), 0);
4795 validateOutput(nullptr, L"");
4796 }
4797
4798 // Make sure the defaultUid is reset for next test case.
4799 TerminateDistribution();
4800 }
4801
4802 static void ValidateDistributionStarts(LPCWSTR Name)
4803 {
4804 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo -n OK", Name));
4805 VERIFY_ARE_EQUAL(out, L"OK");
4806 }
4807
4808 TEST_METHOD(InstallWithBrokenDefault)
4809 {
4810 // This test case validates that a broken 'DefaultDistribution' value doesn't prevent installing new distributions.
4811
4812 // Create a broken default
4813 RegistryKeyChange defaultDistro(
4814 HKEY_CURRENT_USER,
4815 L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss",
4816 L"DefaultDistribution",
4817 std::wstring{L"{1DB260CB-912D-432A-B898-518DFD0F374E}"});
4818
4819 // Validate that installing a new distribution succeeds.
4820 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister test_new_default"); });
4821
4822 VERIFY_ARE_EQUAL(
4823 LxsstuLaunchWsl(std::format(L"--install --from-file \"{}\" --no-launch --name test_new_default", g_testDistroPath)), 0L);
4824
4825 auto [out, error] = LxsstuLaunchWslAndCaptureOutput(L"-d test_new_default echo OK");
4826 VERIFY_ARE_EQUAL(out, L"OK\n");
4827 VERIFY_ARE_EQUAL(error, L"");
4828
4829 // Verify that the default distribution is updated
4830 const auto key = wsl::windows::common::registry::OpenLxssUserKey();
4831
4832 const auto defaultValue = wsl::windows::common::registry::ReadString(key.get(), nullptr, L"DefaultDistribution");
4833
4834 VERIFY_ARE_EQUAL(GetDistributionId(L"test_new_default").value_or(GUID_NULL), wsl::shared::string::ToGuid(defaultValue));
4835 }
4836
4837 TEST_METHOD(ModernInstall)
4838 {
4839 using namespace wsl::windows::common::wslutil;
4840 using namespace wsl::windows::common::string;
4841 constexpr auto IconPath = L"test-icon.ico";
4842
4843 auto CreateTarFromManifest = [](LPCWSTR Manifest, LPCWSTR TarName) {
4844 DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
4845 distributionconf.SetContent(Manifest);
4846 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export test_distro {}", TarName)), 0L);
4847 };
4848
4849 auto InstallFromTar =
4850 [](LPCWSTR TarName, LPCWSTR ExtraArgs = L"", int ExpectedExitCode = 0, LPCWSTR ExpectedOutput = nullptr, LPCWSTR ExpectedWarnings = nullptr) {
4851 auto [out, err] = LxsstuLaunchWslAndCaptureOutput(
4852 std::format(L"--install --no-launch --from-file {} {}", TarName, ExtraArgs), ExpectedExitCode);
4853
4854 if (ExpectedOutput != nullptr)
4855 {
4856 VERIFY_ARE_EQUAL(ExpectedOutput, out);
4857 }
4858
4859 if (ExpectedWarnings != nullptr)
4860 {
4861 VERIFY_ARE_EQUAL(ExpectedWarnings, err);
4862 }
4863 };
4864
4865 auto installLocation = wsl::windows::common::wslutil::GetMsiPackagePath();
4866 VERIFY_IS_TRUE(installLocation.has_value());
4867
4868 auto wslExePath = installLocation.value() + L"wsl.exe";
4869
4870 wil::unique_hmodule wslExe{LoadLibrary(wslExePath.c_str())};
4871 VERIFY_IS_TRUE(!!wslExe);
4872
4873 auto resource = FindResource(wslExe.get(), MAKEINTRESOURCE(1), RT_ICON);
4874 VERIFY_IS_TRUE(resource != nullptr);
4875
4876 auto loadedResource = LoadResource(wslExe.get(), resource);
4877 const void* iconAddress = LockResource(loadedResource);
4878
4879 wil::unique_handle icon{CreateFile(IconPath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr)};
4880 VERIFY_IS_TRUE(!!icon);
4881
4882 DWORD bytes{};
4883 VERIFY_IS_TRUE(WriteFile(icon.get(), iconAddress, SizeofResource(wslExe.get(), resource), &bytes, nullptr));
4884 LogInfo("Created icon %ls (%lu bytes)", IconPath, bytes);
4885
4886 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"cp '{}' /icon.ico", IconPath)), 0L);
4887
4888 // Distribution with default name and icon
4889 {
4890 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
4891 LxsstuLaunchWsl(L"--unregister test-default-name");
4892 DeleteFile(L"distro-default-name-icon.tar");
4893 });
4894
4895 CreateTarFromManifest(
4896 L"[shortcut]\nicon = /icon.ico\n[oobe]\ndefaultName = test-default-name", L"distro-default-name-icon.tar");
4897
4898 //
4899 // Validate that the distribution icon path is also correct when installing via wsl --import.
4900 //
4901
4902 {
4903 constexpr auto distroName = L"TestCustomLocation";
4904
4905 auto currentDirectory = std::filesystem::absolute(std::filesystem::current_path()).wstring();
4906 for (const auto& location : {currentDirectory, std::wstring(L".")})
4907 {
4908 auto cleanup = wil::scope_exit_log(
4909 WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(std::format(L"--unregister {}", distroName)); });
4910
4911 VERIFY_ARE_EQUAL(
4912 LxsstuLaunchWsl(
4913 std::format(L"--import {} \"{}\" {}", distroName, location, "distro-default-name-icon.tar")),
4914 0L);
4915
4916 auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, false);
4917 VERIFY_ARE_EQUAL(
4918 json["profiles"][1]["icon"].get<std::string>(), (std::filesystem::absolute(".") / "shortcut.ico").string());
4919 }
4920 }
4921
4922 InstallFromTar(L"distro-default-name-icon.tar");
4923 ValidateDistributionStarts(L"test-default-name");
4924
4925 // Validate that the distribution was installed under the right name
4926 auto distroKey = OpenDistributionKey(L"test-default-name");
4927 VERIFY_IS_TRUE(!!distroKey);
4928
4929 auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
4930 auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
4931
4932 VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
4933 VERIFY_IS_TRUE(std::filesystem::exists(basePath));
4934
4935 ValidateDistributionShortcut(L"test-default-name", icon.get());
4936 auto [json, profile_path] = ValidateDistributionTerminalProfile(L"test-default-name", false);
4937
4938 VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
4939 cleanup.reset();
4940
4941 // Terminal profile should be removed when the distribution is unregistered.
4942 VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
4943
4944 // Validate that the base path is removed and that the shortcut is gone*
4945 VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
4946 VERIFY_IS_FALSE(std::filesystem::exists(basePath));
4947 }
4948
4949 // Distribution with default name and no icon
4950 {
4951 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
4952 LxsstuLaunchWsl(L"--unregister test-default-name");
4953 DeleteFile(L"distro-default-name-no-icon.tar");
4954 });
4955
4956 CreateTarFromManifest(L"\n[oobe]\ndefaultName = test-default-name", L"distro-default-name-no-icon.tar");
4957 InstallFromTar(L"distro-default-name-no-icon.tar");
4958 ValidateDistributionStarts(L"test-default-name");
4959
4960 // Validate that the distribution was installed under the right name and icon
4961 auto distroKey = OpenDistributionKey(L"test-default-name");
4962 VERIFY_IS_TRUE(!!distroKey);
4963
4964 auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
4965 auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
4966
4967 VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
4968 VERIFY_IS_TRUE(std::filesystem::exists(basePath));
4969 ValidateDistributionShortcut(L"test-default-name", nullptr);
4970
4971 cleanup.reset();
4972
4973 // Validate that the base path is removed and that the shortcut is gone*
4974 VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
4975 VERIFY_IS_FALSE(std::filesystem::exists(basePath));
4976 }
4977
4978 // Distribution with no default name
4979 {
4980 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
4981 LxsstuLaunchWsl(L"--unregister test-distro-no-default-name");
4982 DeleteFile(L"distro-no-default-name.tar");
4983 });
4984
4985 CreateTarFromManifest(L"", L"distro-no-default-name.tar");
4986
4987 // Import should fail without --name
4988 const auto expectedOutput = L"Installing: distro-no-default-name.tar\r\n" +
4989 FormatErrorMessage(
4990 L"This distribution doesn't contain a default name. Use --name to choose the "
4991 L"distribution name.",
4992 L"Wsl/Service/RegisterDistro/WSL_E_DISTRIBUTION_NAME_NEEDED");
4993
4994 InstallFromTar(L"distro-no-default-name.tar", L"", -1, expectedOutput.c_str());
4995
4996 // And succeed with --name
4997 InstallFromTar(L"distro-no-default-name.tar", L"--name test-distro-no-default-name");
4998 ValidateDistributionStarts(L"test-distro-no-default-name");
4999
5000 auto distroKey = OpenDistributionKey(L"test-distro-no-default-name");
Showing first 5,000 of 8,050 lines. View raw