master
cpp 677 lines 26.7 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 PolicyTests.cpp
8
9 Abstract:
10
11 This file contains test cases for WSL policies.
12
13 --*/
14
15 #include "precomp.h"
16 #include <fstream>
17 #include "Common.h"
18 #include "registry.hpp"
19 #include "wslpolicies.h"
20
21 using namespace wsl::windows::policies;
22 using namespace wsl::windows::common::registry;
23
24 class PolicyTest
25 {
26 WSL_TEST_CLASS(PolicyTest)
27
28 bool m_initialized = false;
29
30 TEST_CLASS_SETUP(TestClassSetup)
31 {
32 const auto policies = OpenKey(HKEY_LOCAL_MACHINE, ROOT_POLICIES_KEY, KEY_CREATE_SUB_KEY, 0);
33 VERIFY_IS_TRUE(!!policies);
34
35 const auto wslPolicies = CreateKey(policies.get(), L"WSL");
36 VERIFY_IS_TRUE(!!wslPolicies);
37
38 VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE);
39 m_initialized = true;
40 return true;
41 }
42
43 TEST_CLASS_CLEANUP(TestClassCleanup)
44 {
45 if (m_initialized)
46 {
47 LxsstuUninitialize(FALSE);
48 }
49
50 return true;
51 }
52
53 static auto SetPolicy(LPCWSTR Name, DWORD Value)
54 {
55 return RegistryKeyChange(HKEY_LOCAL_MACHINE, c_registryKey, Name, Value);
56 }
57
58 // Writes the supplied entries under the WSLContainerRegistryAllowlist sub-key as REG_SZ
59 // values named "AllowedRegistry1", "AllowedRegistry2", ... (matching what the GP editor
60 // writes for the ADMX `<list valuePrefix="AllowedRegistry"/>` policy) and deletes the
61 // sub-key when the returned scope exits.
62 static auto SetRegistryAllowlist(std::initializer_list<std::wstring_view> entries)
63 {
64 const auto policies = OpenKey(HKEY_LOCAL_MACHINE, c_registryKey, KEY_ALL_ACCESS);
65
66 // Drop any pre-existing sub-key so stale `AllowedRegistryN` values from a previous
67 // (possibly interrupted) test run can't leak into this one.
68 DeleteKey(policies.get(), c_wslContainerRegistryAllowlist);
69
70 const auto subKey = CreateKey(policies.get(), c_wslContainerRegistryAllowlist);
71 DWORD index = 1;
72 for (const auto& entry : entries)
73 {
74 const auto name = std::format(L"AllowedRegistry{}", index++);
75 const std::wstring data{entry};
76 WriteString(subKey.get(), nullptr, name.c_str(), data.c_str());
77 }
78 return wil::scope_exit([] {
79 try
80 {
81 const auto policies = OpenKey(HKEY_LOCAL_MACHINE, c_registryKey, KEY_ALL_ACCESS);
82 DeleteKey(policies.get(), c_wslContainerRegistryAllowlist);
83 }
84 CATCH_LOG()
85 });
86 }
87
88 static void ValidateWarnings(const std::wstring& expectedWarnings, bool pattern = false)
89 {
90 auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
91 VERIFY_ARE_EQUAL(L"ok\n", output);
92
93 if (pattern)
94 {
95 if (!PathMatchSpec(warnings.c_str(), expectedWarnings.c_str()))
96 {
97 LogError("Warning '%ls' didn't match pattern '%ls'", warnings.c_str(), expectedWarnings.c_str());
98 VERIFY_FAIL();
99 }
100 }
101 else
102 {
103 VERIFY_ARE_EQUAL(expectedWarnings, warnings);
104 }
105 };
106
107 WSL2_TEST_METHOD(MountPolicyAllowed)
108 {
109 SKIP_TEST_ARM64();
110 auto revert = SetPolicy(c_allowDiskMount, 1);
111 ValidateOutput(
112 L"--mount DoesNotExist",
113 FormatErrorMessage(
114 L"Failed to attach disk 'DoesNotExist' to WSL2: The system cannot find the file specified. ",
115 L"Wsl/Service/AttachDisk/MountDisk/HCS/ERROR_FILE_NOT_FOUND"));
116 }
117
118 WSL2_TEST_METHOD(MountPolicyDisabled)
119 {
120 SKIP_TEST_ARM64();
121 auto revert = SetPolicy(c_allowDiskMount, 0);
122 ValidateOutput(
123 L"--mount DoesNotExist",
124 FormatErrorMessage(L"wsl.exe --mount is disabled by the computer policy.", L"Wsl/Service/WSL_E_DISK_MOUNT_DISABLED"));
125 }
126
127 void ValidatePolicy(LPCWSTR Name, LPCWSTR Config, LPCWSTR ExpectedWarnings, const std::function<void(DWORD)>& Validate = [](auto) {})
128 {
129 WslConfigChange config(LxssGenerateTestConfig() + Config);
130
131 // Validate behavior with policy allowed
132 {
133 auto revert = SetPolicy(Name, 1);
134 WslShutdown();
135
136 ValidateWarnings(L""); // Expect no warnings
137 Validate(1);
138 }
139
140 // Validate behavior with policy disabled
141 {
142 auto revert = SetPolicy(Name, 0);
143 WslShutdown();
144
145 ValidateWarnings(ExpectedWarnings);
146 Validate(0);
147 }
148
149 // Validate behavior with an invalid policy value
150 {
151 auto revert = SetPolicy(Name, 12);
152 WslShutdown();
153
154 ValidateWarnings(L"");
155 Validate(12);
156 }
157 }
158
159 WSL2_TEST_METHOD(KernelCommandLine)
160 {
161 auto validate = [](DWORD policyValue) {
162 auto [commandLine, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/cmdline");
163
164 if (policyValue == 0)
165 {
166 VERIFY_IS_FALSE(commandLine.find(L"dummy-cmd-arg") != std::wstring::npos);
167 }
168 else
169 {
170 VERIFY_IS_TRUE(commandLine.find(L"dummy-cmd-arg") != std::wstring::npos);
171 }
172 };
173
174 ValidatePolicy(
175 c_allowCustomKernelCommandLineUserSetting,
176 L"kernelCommandLine=dummy-cmd-arg",
177 L"wsl: The .wslconfig setting 'wsl2.kernelCommandLine' is disabled by the computer policy.\r\n",
178 validate);
179 }
180
181 WSL2_TEST_METHOD(NestedVirtualization)
182 {
183 SKIP_TEST_ARM64();
184 WINDOWS_11_TEST_ONLY();
185
186 ValidatePolicy(
187 c_allowNestedVirtualizationUserSetting,
188 L"nestedVirtualization=true",
189 L"wsl: The .wslconfig setting 'wsl2.nestedVirtualization' is disabled by the computer policy.\r\n");
190 }
191
192 WSL2_TEST_METHOD(KernelDebugging)
193 {
194 WINDOWS_11_TEST_ONLY();
195
196 ValidatePolicy(
197 c_allowKernelDebuggingUserSetting,
198 L"kernelDebugPort=1234",
199 L"wsl: The .wslconfig setting 'wsl2.kernelDebugPort' is disabled by the computer policy.\r\n");
200 }
201
202 WSL2_TEST_METHOD(CustomKernel)
203 {
204 const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
205 const std::wstring nonExistentFile = L"DoesNotExist";
206 WslConfigChange config(LxssGenerateTestConfig({.kernel = nonExistentFile.c_str(), .kernelModules = nonExistentFile.c_str()}));
207
208 {
209 auto revert = SetPolicy(c_allowCustomKernelUserSetting, 1);
210 WslShutdown();
211
212 ValidateOutput(
213 L"echo ok",
214 FormatErrorMessage(
215 wsl::shared::Localization::MessageCustomKernelNotFound(wslConfigPath, nonExistentFile),
216 L"Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND"));
217 }
218
219 // Disable the custom kernel policy and validate that the expected warnings are shown.
220 {
221 auto revert = SetPolicy(c_allowCustomKernelUserSetting, 0);
222 WslShutdown();
223
224 const auto kernelWarning =
225 std::format(L"wsl: {}\r\n", wsl::shared::Localization::MessageSettingOverriddenByPolicy(L"wsl2.kernel"));
226 const auto modulesWarning =
227 std::format(L"wsl: {}\r\n", wsl::shared::Localization::MessageSettingOverriddenByPolicy(L"wsl2.kernelModules"));
228
229 ValidateWarnings(std::format(L"{}{}", kernelWarning, modulesWarning));
230
231 config.Update(LxssGenerateTestConfig({.kernel = nonExistentFile.c_str()}));
232 ValidateWarnings(kernelWarning);
233
234 config.Update(LxssGenerateTestConfig({.kernelModules = nonExistentFile.c_str()}));
235 ValidateWarnings(modulesWarning);
236 }
237 }
238
239 WSL2_TEST_METHOD(CustomSystemDistro)
240 {
241 WslConfigChange config(LxssGenerateTestConfig() + L"systemDistro=DoesNotExist");
242 const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
243
244 {
245 auto revert = SetPolicy(c_allowCustomSystemDistroUserSetting, 1);
246 WslShutdown();
247
248 ValidateOutput(
249 L"echo ok",
250 FormatErrorMessage(
251 L"The custom system distribution specified in " + wslConfigPath + L" was not found or is not the correct format.",
252 L"Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_SYSTEM_DISTRO_ERROR"));
253 }
254
255 {
256 auto revert = SetPolicy(c_allowCustomSystemDistroUserSetting, 0);
257 WslShutdown();
258
259 ValidateWarnings(L"wsl: The .wslconfig setting 'wsl2.systemDistro' is disabled by the computer policy.\r\n");
260 }
261 }
262
263 WSL2_TEST_METHOD(CustomNetworkingMode)
264 {
265 WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Consomme}));
266
267 {
268 auto revert = SetPolicy(c_allowCustomNetworkingModeUserSetting, 1);
269 WslShutdown();
270
271 ValidateWarnings(L"");
272 }
273
274 {
275 auto revertCustomMode = SetPolicy(c_allowCustomNetworkingModeUserSetting, 0);
276 WslShutdown();
277
278 ValidateWarnings(L"wsl: The .wslconfig setting 'wsl2.networkingMode' is disabled by the computer policy.\r\n");
279
280 // Validate that no warnings are shown for NAT or None
281 config.Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Nat}));
282 ValidateWarnings(L"");
283
284 config.Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::None}));
285 ValidateWarnings(L"");
286
287 // Validate that no warnings are shown if the default networking mode is set to the same value as .wslconfig.
288 auto revertDefault = SetPolicy(c_defaultNetworkingMode, static_cast<DWORD>(wsl::core::NetworkingMode::Consomme));
289 config.Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Consomme}));
290 ValidateWarnings(L"");
291 }
292 }
293
294 WSL2_TEST_METHOD(DebugShell)
295 {
296 auto revert = SetPolicy(c_allowDebugShellUserSetting, 0);
297 WslShutdown();
298
299 // Only testing the negative case since the debug shell is difficult to programmatically exit.
300
301 WslKeepAlive keepAlive;
302 ValidateOutput(L"--debug-shell", L"The debug shell is disabled by the computer policy.\r\n", L"", 1);
303 }
304
305 TEST_METHOD(WSL1)
306 {
307 // Test policy registry key with allow key explicitly set.
308 {
309 auto revert = SetPolicy(c_allowWSL1, 1);
310 WslShutdown();
311
312 ValidateWarnings(L"");
313 }
314
315 // Disable WSL1.
316 {
317 auto revert = SetPolicy(c_allowWSL1, 0);
318 WslShutdown();
319
320 // If running as WSL2, attempt to convert the distro to WSL1. If running as WSL1, attempt to run a command.
321 if (LxsstuVmMode())
322 {
323 ValidateOutput(
324 L"--set-version " LXSS_DISTRO_NAME_TEST_L L" 1",
325 FormatErrorMessage(L"WSL1 is disabled by the computer policy.", L"Wsl/Service/WSL_E_WSL1_DISABLED"));
326 }
327 else
328 {
329 ValidateOutput(
330 L"echo ok",
331 FormatErrorMessage(
332 L"WSL1 is disabled by the computer policy.\r\nPlease run 'wsl.exe "
333 L"--set-version " LXSS_DISTRO_NAME_TEST_L L" 2' to upgrade to WSL2.",
334 L"Wsl/Service/CreateInstance/WSL_E_WSL1_DISABLED"));
335 }
336 }
337 }
338
339 TEST_METHOD(DisableWsl)
340 {
341 // N.B. Modifying one of the policy registry keys triggers a registry watcher in the service.
342 // Retry for up to 30 seconds to ensure the registry watcher has time to take effect.
343 auto createInstance = [&](HRESULT expectedResult) {
344 HRESULT result;
345 const auto stop = std::chrono::steady_clock::now() + std::chrono::seconds{30};
346 for (;;)
347 {
348 wil::com_ptr<ILxssUserSession> session;
349 result = CoCreateInstance(CLSID_LxssUserSession, nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&session));
350 if (result == expectedResult || std::chrono::steady_clock::now() > stop)
351 {
352 break;
353 }
354
355 std::this_thread::sleep_for(std::chrono::milliseconds{250});
356 }
357
358 VERIFY_ARE_EQUAL(expectedResult, result);
359 if (SUCCEEDED(result))
360 {
361 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0u);
362 }
363 else
364 {
365 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"/bin/true", -1);
366 VERIFY_ARE_EQUAL(
367 output,
368 FormatErrorMessage(
369 L"This program is blocked by group policy. For more information, contact your system administrator. ",
370 L"Wsl/ERROR_ACCESS_DISABLED_BY_POLICY"));
371 }
372 };
373
374 // Set the policy registry key and validate that user session creation returns the expected result,
375 // then delete the key and ensure user session can be created.
376 auto testPolicy = [&](LPCWSTR policy, HRESULT expectedResult, bool restartService) {
377 {
378 auto revert = SetPolicy(policy, 0);
379 if (restartService)
380 {
381 RestartWslService();
382 }
383
384 createInstance(expectedResult);
385 }
386
387 if (restartService)
388 {
389 RestartWslService();
390 }
391 createInstance(S_OK);
392 };
393
394 for (const auto restartService : {false, true})
395 {
396 // Ensure the top-level disable WSL policy works.
397 testPolicy(wsl::windows::policies::c_allowWSL, HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY), restartService);
398
399 // Verify the disable inbox WSL policy does not block lifted.
400 testPolicy(wsl::windows::policies::c_allowInboxWSL, S_OK, restartService);
401 }
402
403 // Delete and recreate the key without restarting the service to ensure the registry watcher continues to work.
404 wsl::windows::common::registry::DeleteKey(HKEY_LOCAL_MACHINE, wsl::windows::policies::c_registryKey);
405 auto key = wsl::windows::common::registry::CreateKey(HKEY_LOCAL_MACHINE, wsl::windows::policies::c_registryKey);
406 testPolicy(wsl::windows::policies::c_allowWSL, HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY), false);
407 }
408
409 WSL2_TEST_METHOD(DefaultNetworkingMode)
410 {
411 WslConfigChange config(LxssGenerateTestConfig());
412
413 {
414 auto revert = SetPolicy(c_defaultNetworkingMode, static_cast<DWORD>(wsl::core::NetworkingMode::None));
415 WslShutdown();
416
417 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'none'"), 0u);
418 }
419
420 {
421 auto revert = SetPolicy(c_defaultNetworkingMode, static_cast<DWORD>(wsl::core::NetworkingMode::Consomme));
422 WslShutdown();
423
424 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'consomme'"), 0u);
425 }
426 }
427
428 // Build the absolute path to the installed wslc.exe.
429 static std::wstring GetWslcExePath()
430 {
431 auto msiPath = wsl::windows::common::wslutil::GetMsiPackagePath();
432 THROW_HR_IF_MSG(E_UNEXPECTED, !msiPath.has_value(), "MSI install location not found in registry; is WSL installed?");
433 return (std::filesystem::path(*msiPath) / L"wslc.exe").wstring();
434 }
435
436 // Verifies AllowWSLContainer=0 gates the WSLCSessionManager COM factory itself, so that
437 // every method (including GetVersion) is unreachable when the policy disables containers.
438 WSLC_TEST_METHOD(WSLContainerDisabled)
439 {
440 auto revert = SetPolicy(c_allowWSLContainer, 0);
441
442 wil::com_ptr<IWSLCSessionManager> sessionManager;
443 HRESULT hr = CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager));
444 VERIFY_ARE_EQUAL(WSLC_E_CONTAINER_DISABLED, hr);
445 VERIFY_IS_NULL(sessionManager.get());
446 }
447
448 // Verifies AllowWSLContainer=0 gates wslc.exe at startup with a friendly message that is
449 // surfaced on stderr (and not stdout). Locks down both the exact rendered text and the
450 // handle the message is written to so future regressions show up here.
451 WSLC_TEST_METHOD(WSLContainerDisabledCli)
452 {
453 auto revert = SetPolicy(c_allowWSLContainer, 0);
454
455 std::wstring cmd = L"\"" + GetWslcExePath() + L"\" container ls";
456 auto [stdoutText, stderrText, exitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(cmd.data(), nullptr, nullptr);
457
458 VERIFY_ARE_EQUAL(1, exitCode);
459
460 // The disabled message must go to stderr only -- never to stdout.
461 VERIFY_ARE_EQUAL(L"", stdoutText);
462
463 // The wslc CLI renders failures via MessageErrorCode and
464 // PrintMessage adds a trailing newline; line endings are \r\n through console pipes.
465 const auto expected =
466 FormatErrorMessage(wsl::shared::Localization::MessageWSLContainerDisabled(), L"WSLC_E_CONTAINER_DISABLED");
467 VERIFY_ARE_EQUAL(expected, stderrText);
468 }
469
470 // Verifies the WSLContainerRegistryAllowlist denies image pulls from registries not in the
471 // allowlist.
472 WSLC_TEST_METHOD(RegistryAllowlistDenies)
473 {
474 // Allowlist contains ONLY mcr.microsoft.com -- pulling docker.io must be denied.
475 auto revert = SetRegistryAllowlist({L"mcr.microsoft.com"});
476
477 std::wstring cmd = L"\"" + GetWslcExePath() + L"\" image pull alpine:latest";
478 auto [stdoutText, stderrText, exitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(cmd.data(), nullptr, nullptr);
479
480 VERIFY_ARE_NOT_EQUAL(0, exitCode);
481 VERIFY_ARE_EQUAL(L"", stdoutText);
482
483 const auto expected = FormatErrorMessage(
484 wsl::shared::Localization::MessageRegistryBlockedByPolicy(L"docker.io"), L"WSLC_E_REGISTRY_BLOCKED_BY_POLICY");
485 VERIFY_ARE_EQUAL(expected, stderrText);
486 }
487
488 // Two variants: BuildKit echoes the caller's Dockerfile spelling in the "failed to solve" prefix.
489 static constexpr auto c_denialPatternExplicitAlpine =
490 "*failed to solve: docker.io/library/alpine:latest: could not resolve image due to policy: "
491 "source \"docker-image://docker.io/library/alpine:latest\" denied by policy: source denied by policy*";
492 static constexpr auto c_denialPatternImplicitAlpine =
493 "*failed to solve: alpine:latest: could not resolve image due to policy: "
494 "source \"docker-image://docker.io/library/alpine:latest\" denied by policy: source denied by policy*";
495
496 // Verifies WSLContainerRegistryAllowlist blocks `wslc image build` when the FROM base image
497 // isn't in the allowlist. Matches the `RegistryAllowlistDenies` pull test.
498 WSLC_TEST_METHOD(RegistryAllowlistBlocksImageBuild)
499 {
500 auto revert = SetRegistryAllowlist({L"mcr.microsoft.com"});
501
502 auto [exitCode, output] = RunImageBuild(L"FROM docker.io/library/alpine:latest\n", L"wsl-policy-build-blocked");
503
504 VERIFY_ARE_NOT_EQUAL(0, exitCode);
505 VerifyPatternMatch(wsl::shared::string::WideToMultiByte(output), c_denialPatternExplicitAlpine);
506 }
507
508 // Positive path: build must proceed when FROM is on the allowlist.
509 WSLC_TEST_METHOD(RegistryAllowlistAllowsImageBuild)
510 {
511 auto revert = SetRegistryAllowlist({L"mcr.microsoft.com"});
512
513 auto [exitCode, output] =
514 RunImageBuild(L"FROM mcr.microsoft.com/cbl-mariner/base/core:2.0\n", L"wsl-policy-build-allowed");
515
516 if (exitCode != 0)
517 {
518 LogError("Expected build against allowlisted registry to succeed, got exit=%d output: '%ls'", exitCode, output.c_str());
519 VERIFY_FAIL();
520 }
521 }
522
523 // Case regression: allowlist entries stored uppercase must still match lowercased FROM.
524 WSLC_TEST_METHOD(RegistryAllowlistImageBuildIsCaseInsensitive)
525 {
526 auto revert = SetRegistryAllowlist({L"MCR.MICROSOFT.COM"});
527
528 auto [exitCode, output] = RunImageBuild(L"FROM mcr.microsoft.com/cbl-mariner/base/core:2.0\n", L"wsl-policy-build-case");
529
530 if (exitCode != 0)
531 {
532 LogError("Expected uppercase allowlist entry to match lowercase FROM, got: '%ls'", output.c_str());
533 VERIFY_FAIL();
534 }
535 }
536
537 // Multi-stage regression: `COPY --from=<image>` must also be gated, not just top-level FROM.
538 WSLC_TEST_METHOD(RegistryAllowlistBlocksImageBuildCopyFrom)
539 {
540 auto revert = SetRegistryAllowlist({L"mcr.microsoft.com"});
541 const auto dockerfile =
542 L"FROM mcr.microsoft.com/cbl-mariner/base/core:2.0\n"
543 L"COPY --from=docker.io/library/alpine:latest /etc/os-release /tmp/os-release\n";
544
545 auto [exitCode, output] = RunImageBuild(dockerfile, L"wsl-policy-build-copyfrom");
546
547 VERIFY_ARE_NOT_EQUAL(0, exitCode);
548 VerifyPatternMatch(wsl::shared::string::WideToMultiByte(output), c_denialPatternExplicitAlpine);
549 }
550
551 WSLC_TEST_METHOD(RegistryAllowlistBlocksImageBuildImplicitDockerIo)
552 {
553 auto revert = SetRegistryAllowlist({L"mcr.microsoft.com"});
554
555 auto [exitCode, output] = RunImageBuild(L"FROM alpine:latest\n", L"wsl-policy-build-implicit");
556
557 VERIFY_ARE_NOT_EQUAL(0, exitCode);
558 VerifyPatternMatch(wsl::shared::string::WideToMultiByte(output), c_denialPatternImplicitAlpine);
559 }
560
561 // Runs `wslc image build` with the supplied Dockerfile content and returns the exit code
562 // plus combined stdout/stderr. Extracted to keep the allowlist matrix above readable.
563 static std::tuple<int, std::wstring> RunImageBuild(std::wstring_view dockerfile, std::wstring_view folder)
564 {
565 // Terminate any existing session so ConfigureBuildKitPolicy re-snapshots the registry.
566 {
567 std::wstring terminateCmd = L"\"" + GetWslcExePath() + L"\" system session terminate";
568 LxsstuLaunchCommandAndCaptureOutputWithResult(terminateCmd.data(), nullptr, nullptr);
569 }
570
571 const auto contextDir = std::filesystem::temp_directory_path() / folder;
572 std::error_code ec;
573 std::filesystem::remove_all(contextDir, ec);
574 std::filesystem::create_directories(contextDir);
575 auto cleanup = wil::scope_exit([&] { std::filesystem::remove_all(contextDir, ec); });
576 {
577 std::ofstream df(contextDir / L"Dockerfile");
578 VERIFY_IS_TRUE(df.is_open());
579 df << wsl::shared::string::WideToMultiByte(std::wstring{dockerfile});
580 }
581 std::wstring cmd = L"\"" + GetWslcExePath() + L"\" image build \"" + contextDir.wstring() + L"\"";
582 auto [stdoutText, stderrText, exitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(cmd.data(), nullptr, nullptr);
583 return {exitCode, stdoutText + stderrText};
584 }
585
586 // Pure-function tests for the registry-allowlist policy evaluator. These don't talk to the
587 // service, but do read/write the WSL policies registry key (created by TestClassSetup).
588 TEST_METHOD(IsRegistryAllowed_Logic)
589 {
590 // No policy key configured -> always allowed.
591 VERIFY_IS_TRUE(IsRegistryAllowed(nullptr, L"docker.io"));
592
593 const auto policiesKey = OpenPoliciesKey();
594 VERIFY_IS_TRUE(!!policiesKey);
595
596 // No allowlist sub-key configured -> allowed.
597 VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"docker.io"));
598
599 // Allowlist with multiple entries; matching is case-insensitive.
600 {
601 auto revert = SetRegistryAllowlist({L"mcr.microsoft.com", L"Docker.IO"});
602 VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"mcr.microsoft.com"));
603 VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"docker.io"));
604 VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"DOCKER.IO"));
605 VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"MCR.Microsoft.COM"));
606 VERIFY_IS_FALSE(IsRegistryAllowed(policiesKey.get(), L"ghcr.io"));
607 }
608
609 // Sub-key present with no entries -> no effective restriction, every server allowed.
610 {
611 auto revert = SetRegistryAllowlist({});
612 VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"docker.io"));
613 VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"mcr.microsoft.com"));
614 }
615
616 // Sub-key present but only contains empty entries -> treated as no restriction, not
617 // as a deny-all (defensive against stray GP editor list items).
618 {
619 auto revert = SetRegistryAllowlist({L"", L""});
620 VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"docker.io"));
621 VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"mcr.microsoft.com"));
622 }
623 }
624
625 // Pure-function tests for HasRegistryAllowlist (used by `wslc image build` to decide whether
626 // to refuse outright when the operation cannot be attributed to a single registry).
627 TEST_METHOD(HasRegistryAllowlist_Logic)
628 {
629 VERIFY_IS_FALSE(HasRegistryAllowlist(nullptr));
630
631 const auto policiesKey = OpenPoliciesKey();
632 VERIFY_IS_TRUE(!!policiesKey);
633
634 // No sub-key -> not configured.
635 VERIFY_IS_FALSE(HasRegistryAllowlist(policiesKey.get()));
636
637 // Sub-key present with no entries -> not effectively configured.
638 {
639 auto revert = SetRegistryAllowlist({});
640 VERIFY_IS_FALSE(HasRegistryAllowlist(policiesKey.get()));
641 }
642
643 // Sub-key present with entries -> configured.
644 {
645 auto revert = SetRegistryAllowlist({L"mcr.microsoft.com"});
646 VERIFY_IS_TRUE(HasRegistryAllowlist(policiesKey.get()));
647 }
648 }
649
650 // The (HKEY) overload is exercised transitively via FromPoliciesRoot.
651 TEST_METHOD(ReadRegistryAllowlistSnapshot_Logic)
652 {
653 // No sub-key -> NotConfigured.
654 {
655 const auto snapshot = ReadRegistryAllowlistSnapshotFromPoliciesRoot();
656 VERIFY_IS_TRUE(snapshot.State == RegistryAllowlistState::NotConfigured);
657 VERIFY_IS_TRUE(snapshot.Hosts.empty());
658 }
659
660 // Sub-key with only empty entries -> NotConfigured (defensive: stray blank GP list
661 // items must not silently deny every registry).
662 {
663 auto revert = SetRegistryAllowlist({L"", L""});
664 const auto snapshot = ReadRegistryAllowlistSnapshotFromPoliciesRoot();
665 VERIFY_IS_TRUE(snapshot.State == RegistryAllowlistState::NotConfigured);
666 VERIFY_IS_TRUE(snapshot.Hosts.empty());
667 }
668
669 // Sub-key with hosts -> Configured, hosts populated in order.
670 {
671 auto revert = SetRegistryAllowlist({L"mcr.microsoft.com", L"Docker.IO"});
672 const auto snapshot = ReadRegistryAllowlistSnapshotFromPoliciesRoot();
673 VERIFY_IS_TRUE(snapshot.State == RegistryAllowlistState::Configured);
674 VERIFY_ARE_EQUAL(size_t{2}, snapshot.Hosts.size());
675 }
676 }
677 };