13
--*/
14
15
#include "precomp.h"
16
+#include <fstream>
17
#include "Common.h"
18
#include "registry.hpp"
19
#include "wslpolicies.h"
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");
420
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'virtioproxy'"), 0u);
421
}
422
}
423
+
424
+ // Build the absolute path to the installed wslc.exe.
425
+ static std::wstring GetWslcExePath()
426
+ {
427
+ auto msiPath = wsl::windows::common::wslutil::GetMsiPackagePath();
428
+ THROW_HR_IF_MSG(E_UNEXPECTED, !msiPath.has_value(), "MSI install location not found in registry; is WSL installed?");
429
+ return (std::filesystem::path(*msiPath) / L"wslc.exe").wstring();
430
+ }
431
+
432
+ // Verifies AllowWSLContainer=0 gates the WSLCSessionManager COM factory itself, so that
433
+ // every method (including GetVersion) is unreachable when the policy disables containers.
434
+ WSLC_TEST_METHOD(WSLContainerDisabled)
435
+ {
436
+ auto revert = SetPolicy(c_allowWSLContainer, 0);
437
+
438
+ wil::com_ptr<IWSLCSessionManager> sessionManager;
439
+ HRESULT hr = CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager));
440
+ VERIFY_ARE_EQUAL(WSL_E_CONTAINER_DISABLED, hr);
441
+ VERIFY_IS_NULL(sessionManager.get());
442
+ }
443
+
444
+ // Verifies AllowWSLContainer=0 gates wslc.exe at startup with a friendly message.
445
+ WSLC_TEST_METHOD(WSLContainerDisabledCli)
446
+ {
447
+ auto revert = SetPolicy(c_allowWSLContainer, 0);
448
+
449
+ std::wstring cmd = L"\"" + GetWslcExePath() + L"\" container ls";
450
+ auto [stdoutText, stderrText, exitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(cmd.data(), nullptr, nullptr);
451
+
452
+ VERIFY_ARE_EQUAL(1, exitCode);
453
+ if (stderrText.find(L"WSL container is disabled by the computer policy") == std::wstring::npos)
454
+ {
455
+ LogError("Expected stderr to contain disabled message, got: '%ls'", stderrText.c_str());
456
+ VERIFY_FAIL();
457
+ }
458
+ }
459
+
460
+ // Verifies the WSLContainerRegistryAllowlist denies image pulls from registries not in the
461
+ // allowlist.
462
+ WSLC_TEST_METHOD(RegistryAllowlistDenies)
463
+ {
464
+ // Allowlist contains ONLY mcr.microsoft.com -- pulling docker.io must be denied.
465
+ auto revert = SetRegistryAllowlist({L"mcr.microsoft.com"});
466
+
467
+ std::wstring cmd = L"\"" + GetWslcExePath() + L"\" image pull alpine:latest";
468
+ auto [stdoutText, stderrText, exitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(cmd.data(), nullptr, nullptr);
469
+
470
+ VERIFY_ARE_NOT_EQUAL(0, exitCode);
471
+ const std::wstring combined = stdoutText + stderrText;
472
+ if (combined.find(L"docker.io") == std::wstring::npos || combined.find(L"blocked by the computer policy") == std::wstring::npos)
473
+ {
474
+ LogError(
475
+ "Expected blocked-by-policy for docker.io when allowlist is mcr.microsoft.com, got stdout: '%ls' stderr: '%ls'",
476
+ stdoutText.c_str(),
477
+ stderrText.c_str());
478
+ VERIFY_FAIL();
479
+ }
480
+ }
481
+
482
+ // Verifies that `wslc image build` is rejected outright when an allowlist is configured,
483
+ // since the in-VM docker daemon would fetch FROM base images directly and bypass the
484
+ // per-pull registry gate.
485
+ WSLC_TEST_METHOD(RegistryAllowlistRejectsImageBuild)
486
+ {
487
+ auto revert = SetRegistryAllowlist({L"mcr.microsoft.com"});
488
+
489
+ // Set up a minimal build context with a one-line Dockerfile in TEMP.
490
+ const auto contextDir = std::filesystem::temp_directory_path() / L"wsl-policy-build-test";
491
+ std::error_code ec;
492
+ std::filesystem::remove_all(contextDir, ec);
493
+ std::filesystem::create_directories(contextDir);
494
+ auto cleanup = wil::scope_exit([&] { std::filesystem::remove_all(contextDir, ec); });
495
+
496
+ {
497
+ std::ofstream df(contextDir / L"Dockerfile");
498
+ VERIFY_IS_TRUE(df.is_open());
499
+ df << "FROM scratch\n";
500
+ }
501
+
502
+ std::wstring cmd = L"\"" + GetWslcExePath() + L"\" image build \"" + contextDir.wstring() + L"\"";
503
+ auto [stdoutText, stderrText, exitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(cmd.data(), nullptr, nullptr);
504
+
505
+ VERIFY_ARE_NOT_EQUAL(0, exitCode);
506
+ const std::wstring combined = stdoutText + stderrText;
507
+ if (combined.find(L"Building container images is blocked") == std::wstring::npos ||
508
+ combined.find(L"computer policy") == std::wstring::npos)
509
+ {
510
+ LogError(
511
+ "Expected image-build to be blocked by policy, got stdout: '%ls' stderr: '%ls'", stdoutText.c_str(), stderrText.c_str());
512
+ VERIFY_FAIL();
513
+ }
514
+ }
515
+
516
+ // Pure-function tests for the registry-allowlist policy evaluator. These don't talk to the
517
+ // service, but do read/write the WSL policies registry key (created by TestClassSetup).
518
+ TEST_METHOD(IsRegistryAllowed_Logic)
519
+ {
520
+ // No policy key configured -> always allowed.
521
+ VERIFY_IS_TRUE(IsRegistryAllowed(nullptr, L"docker.io"));
522
+
523
+ const auto policiesKey = OpenPoliciesKey();
524
+ VERIFY_IS_TRUE(!!policiesKey);
525
+
526
+ // No allowlist sub-key configured -> allowed.
527
+ VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"docker.io"));
528
+
529
+ // Allowlist with multiple entries; matching is case-insensitive.
530
+ {
531
+ auto revert = SetRegistryAllowlist({L"mcr.microsoft.com", L"Docker.IO"});
532
+ VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"mcr.microsoft.com"));
533
+ VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"docker.io"));
534
+ VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"DOCKER.IO"));
535
+ VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"MCR.Microsoft.COM"));
536
+ VERIFY_IS_FALSE(IsRegistryAllowed(policiesKey.get(), L"ghcr.io"));
537
+ }
538
+
539
+ // Sub-key present with no entries -> no effective restriction, every server allowed.
540
+ {
541
+ auto revert = SetRegistryAllowlist({});
542
+ VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"docker.io"));
543
+ VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"mcr.microsoft.com"));
544
+ }
545
+
546
+ // Sub-key present but only contains empty entries -> treated as no restriction, not
547
+ // as a deny-all (defensive against stray GP editor list items).
548
+ {
549
+ auto revert = SetRegistryAllowlist({L"", L""});
550
+ VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"docker.io"));
551
+ VERIFY_IS_TRUE(IsRegistryAllowed(policiesKey.get(), L"mcr.microsoft.com"));
552
+ }
553
+ }
554
+
555
+ // Pure-function tests for HasRegistryAllowlist (used by `wslc image build` to decide whether
556
+ // to refuse outright when the operation cannot be attributed to a single registry).
557
+ TEST_METHOD(HasRegistryAllowlist_Logic)
558
+ {
559
+ VERIFY_IS_FALSE(HasRegistryAllowlist(nullptr));
560
+
561
+ const auto policiesKey = OpenPoliciesKey();
562
+ VERIFY_IS_TRUE(!!policiesKey);
563
+
564
+ // No sub-key -> not configured.
565
+ VERIFY_IS_FALSE(HasRegistryAllowlist(policiesKey.get()));
566
+
567
+ // Sub-key present with no entries -> not effectively configured.
568
+ {
569
+ auto revert = SetRegistryAllowlist({});
570
+ VERIFY_IS_FALSE(HasRegistryAllowlist(policiesKey.get()));
571
+ }
572
+
573
+ // Sub-key present with entries -> configured.
574
+ {
575
+ auto revert = SetRegistryAllowlist({L"mcr.microsoft.com"});
576
+ VERIFY_IS_TRUE(HasRegistryAllowlist(policiesKey.get()));
577
+ }
578
+ }
579
};
\ No newline at end of file