master
cpp 877 lines 36.6 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 Plugin.cpp
8
9 Abstract:
10
11 This file contains a test plugin.
12
13 --*/
14
15 #include "precomp.h"
16 #include <atomic>
17 #include <thread>
18 #include "WslPluginApi.h"
19 #include "wslc_schema.h"
20
21 #include "PluginTests.h"
22
23 using namespace wsl::windows::common::registry;
24 using namespace wsl::windows::common::relay;
25 using namespace wsl::shared::string;
26 using namespace std::chrono_literals;
27
28 std::ofstream g_logfile;
29 std::optional<GUID> g_distroGuid;
30
31 const WSLPluginAPIV1* g_api = nullptr;
32 PluginTestType g_testType = PluginTestType::Invalid;
33
34 // Process deliberately left running across OnWslcVmStopping by the WslcVmStopCommitted test, to
35 // prove the announced teardown happens anyway. Never released: it dies with the VM.
36 //
37 // The exit event is fetched on the callback's own thread and cached here as a plain Win32 handle:
38 // the process itself is a COM proxy marshalled to that thread, so the stop-window thread below
39 // cannot call methods on it (RPC_E_WRONG_THREAD), but it can wait on the handle.
40 std::atomic<WSLCProcessHandle> g_leakedProcess = nullptr;
41 std::atomic<HANDLE> g_leakedProcessExitEvent = nullptr;
42
43 // Set by the WslcVmStopCommitted test: a call issued from a thread the plugin owns while
44 // OnWslcVmStopping is running. Like the callback itself it is served by the VM that is stopping, and
45 // must not block on the teardown. It deliberately logs nothing of its own -- its results are written
46 // when it is joined -- so g_logfile keeps a single writer and the expected output stays ordered.
47 std::thread g_stopWindowCaller;
48 HRESULT g_stopWindowCallerResult = E_PENDING;
49 std::atomic<bool> g_leakedProcessDied = false;
50
51 std::optional<uint32_t> g_previousInitPid;
52
53 std::vector<char> ReadFromSocket(SOCKET socket)
54 {
55 // Simplified error handling for the sake of the demo.
56 int result = 0;
57 int offset = 0;
58
59 std::vector<char> content(1024);
60 while ((result = recv(socket, content.data() + offset, 1024, 0)) > 0)
61 {
62 offset += result;
63 content.resize(offset + 1024);
64 }
65
66 content.resize(offset);
67 return content;
68 }
69
70 HRESULT OnVmStarted(const WSLSessionInformation* Session, const WSLVmCreationSettings* Settings)
71 {
72 g_logfile << "VM created (settings->CustomConfigurationFlags=" << Settings->CustomConfigurationFlags << ")" << std::endl;
73
74 if (g_testType == PluginTestType::FailToStartVm)
75 {
76 g_logfile << "OnVmStarted: E_UNEXPECTED" << std::endl;
77 return E_UNEXPECTED;
78 }
79 else if (g_testType == PluginTestType::FailToStartVmWithPluginErrorMessage)
80 {
81 g_logfile << "OnVmStarted: E_UNEXPECTED" << std::endl;
82 g_api->PluginError(L"Plugin error message");
83 return E_UNEXPECTED;
84 }
85 else if (WI_IsFlagSet(Settings->CustomConfigurationFlags, WSLUserConfigurationCustomKernel))
86 {
87 g_logfile << "OnVmStarted: E_ACCESSDENIED" << std::endl;
88 return E_ACCESSDENIED;
89 }
90 else if (g_testType == PluginTestType::Success)
91 {
92 // Get the current module's directory
93 std::filesystem::path modulePath = wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()).get();
94 auto mountSource = modulePath.parent_path().wstring();
95
96 // Mount the folder with the linux binary in the vm
97 RETURN_IF_FAILED(
98 g_api->MountFolder(Session->SessionId, mountSource.c_str(), L"/test-plugin/deep/folder", true, L"test-plugin-mount"));
99
100 g_logfile << "Folder mounted (" << wsl::shared::string::WideToMultiByte(mountSource) << " -> /test-plugin)" << std::endl;
101
102 // Create a file with dummy content
103 std::ofstream file(mountSource + L"\\test-file.txt");
104 if (!file || !(file << "OK"))
105 {
106 g_logfile << "Failed to open test-file.txt in: " << wsl::shared::string::WideToMultiByte(mountSource) << std::endl;
107 return E_ABORT;
108 }
109
110 file.close();
111
112 // Launch the process
113 std::vector<const char*> arguments = {"/bin/cat", "/test-plugin/deep/folder/test-file.txt", nullptr};
114 wil::unique_socket socket;
115 RETURN_IF_FAILED(g_api->ExecuteBinary(Session->SessionId, arguments[0], arguments.data(), &socket));
116 g_logfile << "Process created" << std::endl;
117
118 // Read the socket output
119 auto output = ReadFromSocket(socket.get());
120 if (output != std::vector<char>{'O', 'K'})
121 {
122 g_logfile << "Got unexpected output from bash" << std::endl;
123 return E_ABORT;
124 }
125 }
126 else if (g_testType == PluginTestType::ApiErrors)
127 {
128 auto result = g_api->MountFolder(Session->SessionId, L"C:\\DoesNotExit", L"/dummy", true, L"test-plugin-mount");
129 if (result != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
130 {
131 g_logfile << "Unexpected error for MountFolder(): " << result << std::endl;
132 return E_ABORT;
133 }
134
135 wil::unique_socket socket;
136 std::vector<const char*> arguments = {"/bin/does-no-exist", nullptr};
137 result = g_api->ExecuteBinary(Session->SessionId, arguments[0], arguments.data(), &socket);
138 if (result != E_FAIL)
139 {
140 g_logfile << "Unexpected error for ExecuteBinary(): " << result << std::endl;
141 return E_ABORT;
142 }
143
144 result = g_api->ExecuteBinary(0xcafe, arguments[0], arguments.data(), &socket);
145 if (result != RPC_E_DISCONNECTED)
146 {
147 g_logfile << "Unexpected error for ExecuteBinary(): " << result << std::endl;
148 return E_ABORT;
149 }
150
151 // Call PluginError asynchronously to verify that we handle this properly.
152
153 std::thread thread{[Session]() {
154 const auto result = g_api->PluginError(L"Dummy");
155
156 if (result != E_ILLEGAL_METHOD_CALL)
157 {
158 g_logfile << "Unexpected error for async PluginError(): " << result << std::endl;
159 }
160 }};
161
162 thread.join();
163
164 g_logfile << "API error tests passed" << std::endl;
165 }
166 else if (g_testType == PluginTestType::ErrorMessageStartVm)
167 {
168 auto result = g_api->PluginError(L"StartVm plugin error message");
169 if (FAILED(result))
170 {
171 g_logfile << "Unexpected error from PluginError(): " << result << std::endl;
172 }
173 g_logfile << "OnVmStarted: E_FAIL" << std::endl;
174 return E_FAIL;
175 }
176 else if (g_testType == PluginTestType::GetUsername)
177 {
178 try
179 {
180 auto info = wil::get_token_information<TOKEN_USER>(Session->UserToken);
181
182 DWORD size{};
183 DWORD domainSize{};
184 SID_NAME_USE use{};
185 LookupAccountSid(nullptr, info->User.Sid, nullptr, &size, nullptr, &domainSize, &use);
186
187 THROW_HR_IF(E_UNEXPECTED, size < 1);
188 std::wstring user(size - 1, '\0');
189 std::wstring domain(domainSize - 1, '\0');
190
191 THROW_IF_WIN32_BOOL_FALSE(LookupAccountSid(nullptr, info->User.Sid, user.data(), &size, domain.data(), &domainSize, &use));
192
193 g_logfile << "Username: " << wsl::shared::string::WideToMultiByte(domain) << "\\"
194 << wsl::shared::string::WideToMultiByte(user) << std::endl;
195 }
196 catch (...)
197 {
198 g_logfile << "OnVmStarted: get_token_information failed: " << wil::ResultFromCaughtException() << std::endl;
199 return E_FAIL;
200 }
201
202 return S_OK;
203 }
204
205 return S_OK;
206 }
207
208 HRESULT OnVmStopping(const WSLSessionInformation* Session)
209 {
210 g_logfile << "VM Stopping" << std::endl;
211
212 if (g_testType == PluginTestType::FailToStopVm)
213 {
214 g_logfile << "OnVmStopping: E_UNEXPECTED" << std::endl;
215 return E_UNEXPECTED;
216 }
217
218 return S_OK;
219 }
220
221 HRESULT OnDistroStarted(const WSLSessionInformation* Session, const WSLDistributionInformation* Distribution)
222 {
223 g_logfile << "Distribution started, name=" << wsl::shared::string::WideToMultiByte(Distribution->Name)
224 << ", package=" << wsl::shared::string::WideToMultiByte(Distribution->PackageFamilyName)
225 << ", PidNs=" << Distribution->PidNamespace << ", InitPid=" << Distribution->InitPid
226 << ", Flavor=" << wsl::shared::string::WideToMultiByte(Distribution->Flavor)
227 << ", Version=" << wsl::shared::string::WideToMultiByte(Distribution->Version) << std::endl;
228
229 if (g_testType == PluginTestType::FailToStartDistro)
230 {
231 g_logfile << "OnDistroStarted: E_UNEXPECTED" << std::endl;
232 return E_UNEXPECTED;
233 }
234 else if (g_testType == PluginTestType::SameDistroId)
235 {
236 if (g_distroGuid.has_value())
237 {
238 if (IsEqualGUID(g_distroGuid.value(), Distribution->Id))
239 {
240 g_logfile << "OnDistroStarted: received same GUID" << std::endl;
241 }
242 else
243 {
244 g_logfile << "OnDistroStarted: received different GUID" << std::endl;
245 }
246 }
247 else
248 {
249 g_distroGuid = Distribution->Id;
250 }
251 }
252 else if (g_testType == PluginTestType::ErrorMessageStartDistro)
253 {
254 g_logfile << "OnDistroStarted: E_FAIL" << std::endl;
255 g_api->PluginError(L"StartDistro plugin error message");
256 return E_FAIL;
257 }
258 else if (g_testType == PluginTestType::InitPidIsDifferent)
259 {
260 if (g_previousInitPid.has_value())
261 {
262 if (g_previousInitPid.value() != Distribution->InitPid)
263 {
264 g_logfile << "Init's pid is different (" << Distribution->InitPid << " ! = " << g_previousInitPid.value() << ")" << std::endl;
265 }
266 else
267 {
268 g_logfile << "Init's pid did not change (" << g_previousInitPid.value() << ")" << std::endl;
269 return E_FAIL;
270 }
271 }
272 else
273 {
274 g_previousInitPid = Distribution->InitPid;
275 }
276 }
277 else if (g_testType == PluginTestType::RunDistroCommand)
278 {
279 // Launch a process
280 std::vector<const char*> arguments = {"/bin/sh", "-c", "cat /etc/issue.net", nullptr};
281 wil::unique_socket socket;
282 RETURN_IF_FAILED(g_api->ExecuteBinaryInDistribution(Session->SessionId, &Distribution->Id, arguments[0], arguments.data(), &socket));
283 g_logfile << "Process created" << std::endl;
284
285 // Validate that the process actually ran inside the distro.
286 auto output = ReadFromSocket(socket.get());
287 const auto expected = "Debian GNU/Linux 13\n";
288 if (std::string(output.begin(), output.end()) != expected)
289 {
290 g_logfile << "Got unexpected output from bash: " << std::string(output.begin(), output.end())
291 << ", expected: " << expected << std::endl;
292 return E_ABORT;
293 }
294
295 // Verify that failure to launch a process behaves properly.
296 arguments = {"/does-not-exist"};
297 g_logfile << "Failed process launch returned: "
298 << g_api->ExecuteBinaryInDistribution(Session->SessionId, &Distribution->Id, arguments[0], arguments.data(), &socket)
299 << std::endl;
300
301 const GUID guid{};
302 g_logfile << "Invalid distro launch returned: "
303 << g_api->ExecuteBinaryInDistribution(Session->SessionId, &guid, arguments[0], arguments.data(), &socket) << std::endl;
304 }
305
306 return S_OK;
307 }
308
309 HRESULT OnDistroStopping(const WSLSessionInformation* Session, const WSLDistributionInformation* Distribution)
310 {
311 g_logfile << "Distribution Stopping, name=" << wsl::shared::string::WideToMultiByte(Distribution->Name)
312 << ", package=" << wsl::shared::string::WideToMultiByte(Distribution->PackageFamilyName)
313 << ", PidNs=" << Distribution->PidNamespace << ", Flavor=" << wsl::shared::string::WideToMultiByte(Distribution->Flavor)
314 << ", Version=" << wsl::shared::string::WideToMultiByte(Distribution->Version) << std::endl;
315
316 if (g_testType == PluginTestType::FailToStopDistro)
317 {
318 g_logfile << "OnDistroStopping: E_UNEXPECTED" << std::endl;
319 return E_UNEXPECTED;
320 }
321 else if (g_testType == PluginTestType::SameDistroId && g_distroGuid.has_value())
322 {
323 if (!IsEqualGUID(g_distroGuid.value(), Distribution->Id))
324 {
325 g_logfile << "OnDistroStarted: received different GUID" << std::endl;
326 }
327 }
328
329 return S_OK;
330 }
331
332 HRESULT OnDistributionRegistered(const WSLSessionInformation* Session, const WslOfflineDistributionInformation* Distribution)
333 {
334 g_logfile << "Distribution registered, name=" << wsl::shared::string::WideToMultiByte(Distribution->Name)
335 << ", package=" << wsl::shared::string::WideToMultiByte(Distribution->PackageFamilyName)
336 << ", Flavor=" << wsl::shared::string::WideToMultiByte(Distribution->Flavor)
337 << ", Version=" << wsl::shared::string::WideToMultiByte(Distribution->Version) << std::endl;
338
339 if (g_testType == PluginTestType::FailToRegisterUnregisterDistro)
340 {
341 g_logfile << "OnDistributionRegistered: E_UNEXPECTED" << std::endl;
342 return E_UNEXPECTED;
343 }
344
345 return S_OK;
346 }
347
348 HRESULT OnDistributionUnregistered(const WSLSessionInformation* Session, const WslOfflineDistributionInformation* Distribution)
349 {
350 g_logfile << "Distribution unregistered, name=" << wsl::shared::string::WideToMultiByte(Distribution->Name)
351 << ", package=" << wsl::shared::string::WideToMultiByte(Distribution->PackageFamilyName)
352 << ", Flavor=" << wsl::shared::string::WideToMultiByte(Distribution->Flavor)
353 << ", Version=" << wsl::shared::string::WideToMultiByte(Distribution->Version) << std::endl;
354
355 if (g_testType == PluginTestType::FailToRegisterUnregisterDistro)
356 {
357 g_logfile << "OnDistributionUnregistered: E_UNEXPECTED" << std::endl;
358 return E_UNEXPECTED;
359 }
360
361 return S_OK;
362 }
363
364 HRESULT OnWslcSessionCreated(const WSLCSessionInformation* Session)
365 try
366 {
367 g_logfile << "WSLC Session created, name=" << wsl::shared::string::WideToMultiByte(Session->DisplayName) << ", id=" << Session->SessionId
368 << ", pid=" << Session->ApplicationPid << ", token=" << (Session->UserToken != nullptr ? "set" : "null")
369 << ", sid=" << (Session->UserSid != nullptr ? "set" : "null") << std::endl;
370
371 if (g_testType == PluginTestType::WslcVmNeverStarted)
372 {
373 // A plugin call is never a reason to create a VM. This one has to be rejected rather than
374 // bringing one up, which the absence of any VM notification in the expected output confirms.
375 std::vector<const char*> args = {"/bin/true", nullptr};
376 WSLCProcessHandle process = nullptr;
377 const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr);
378 if (SUCCEEDED(hr))
379 {
380 g_api->WSLCReleaseProcess(process);
381 }
382
383 g_logfile << "WSLC no-vm caller: " << (hr == WSLC_E_VM_NOT_RUNNING ? "rejected" : "unexpected") << std::endl;
384 return S_OK;
385 }
386
387 if (g_testType == PluginTestType::WslcSessionRejected)
388 {
389 g_logfile << "OnWslcSessionCreated: ERROR_ACCESS_DENIED" << std::endl;
390 return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
391 }
392
393 return S_OK;
394 }
395 CATCH_RETURN();
396
397 // These checks need a running VM, and a plugin call never creates one, so they run from the VM-started
398 // hook rather than from session creation.
399 void RunWslcSuccessChecks(const WSLCSessionInformation* Session)
400 {
401 {
402 // Helper: run a command in the root namespace and return (status, stdout, stderr).
403 auto runCommand = [&](const char* cmd,
404 const std::optional<std::string>& input = {},
405 std::vector<const char*> env = {}) -> std::tuple<int, std::string, std::string> {
406 std::vector<const char*> arguments = {"/bin/sh", "-c", cmd, nullptr};
407 WSLCProcessHandle process = nullptr;
408 THROW_IF_FAILED(g_api->WSLCCreateProcess(
409 Session->SessionId, arguments[0], arguments.data(), env.empty() ? nullptr : env.data(), &process, nullptr));
410 auto releaseProcess = wil::scope_exit([&]() { g_api->WSLCReleaseProcess(process); });
411
412 wil::unique_handle stdinHandle;
413 wil::unique_handle stdoutHandle;
414 wil::unique_handle stderrHandle;
415 wil::unique_handle exitEvent;
416 THROW_IF_FAILED(g_api->WSLCProcessGetFd(process, WSLCProcessFdStdin, &stdinHandle));
417 THROW_IF_FAILED(g_api->WSLCProcessGetFd(process, WSLCProcessFdStdout, &stdoutHandle));
418 THROW_IF_FAILED(g_api->WSLCProcessGetFd(process, WSLCProcessFdStderr, &stderrHandle));
419 THROW_IF_FAILED(g_api->WSLCProcessGetExitEvent(process, &exitEvent));
420
421 std::string out;
422 std::string err;
423
424 MultiHandleWait io;
425 io.AddHandle(std::make_unique<ReadHandle>(
426 std::move(stdoutHandle), [&out](const auto& span) { out.append(span.begin(), span.end()); }));
427
428 io.AddHandle(std::make_unique<ReadHandle>(
429 std::move(stderrHandle), [&err](const auto& span) { err.append(span.begin(), span.end()); }));
430
431 io.AddHandle(std::make_unique<EventHandle>(std::move(exitEvent)));
432
433 if (input.has_value())
434 {
435 io.AddHandle(std::make_unique<WriteHandle>(std::move(stdinHandle), std::vector<char>(input->begin(), input->end())));
436 }
437 else
438 {
439 stdinHandle.reset();
440 }
441
442 io.Run(60000ms);
443
444 int status = 0;
445 THROW_IF_FAILED(g_api->WSLCProcessGetExitCode(process, &status));
446 g_logfile << "Command: '" << cmd << "', status=" << status << ", stdout: " << out << ", stderr: " << err << std::endl;
447
448 return {status, out, err};
449 };
450
451 // Test process creation (output & exit code validated by the test code).
452 {
453 runCommand("echo -n stdout-ok && echo -n stderr-ok >&2");
454 runCommand("cat", "stdin-ok");
455 runCommand("exit 12");
456 runCommand("echo -n $ENV", {}, {"ENV=env-ok", nullptr});
457 }
458
459 // Validate that trying to execute a non-existent file fails with the expected error code.
460 {
461 WSLCProcessHandle process = nullptr;
462 int errnoValue = 0;
463 std::vector<const char*> args = {"does-not-exist", nullptr};
464
465 auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, &errnoValue);
466 g_logfile << "WSLCCreateProcess(does-not-exist): " << std::hex << hr << ", errno=" << std::dec << errnoValue << std::endl;
467 }
468
469 // Validate various error paths
470 {
471 std::vector<const char*> args = {"/bin/sh", "-c", "sleep 9999", nullptr};
472 WSLCProcessHandle process = nullptr;
473 THROW_IF_FAILED(g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr));
474 auto releaseProcess = wil::scope_exit([&]() { g_api->WSLCReleaseProcess(process); });
475
476 // Validate that getting an fd that doesn't exist fails with the expected error code.
477 HANDLE dummy = nullptr;
478 g_logfile << "WSLCProcessGetFd(999): " << g_api->WSLCProcessGetFd(process, static_cast<WSLCProcessFd>(999), &dummy) << std::endl;
479 int exitCode = -1;
480
481 g_logfile << "WSLCProcessGetExitCode(<running>): " << g_api->WSLCProcessGetExitCode(process, &exitCode) << std::endl;
482 }
483
484 const auto testFolder = L"C:\\";
485 constexpr auto testFileName = L"plugin-test.txt";
486 constexpr auto rwMountpoint = "/mnt/wsl-plugin/plugin-rw-test";
487 constexpr auto roMountpoint = "/mnt/wsl-plugin/plugin-ro-test";
488
489 // Validate rw mounts.
490 {
491 auto rwCleanup = wil::scope_exit_log(
492 WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove(std::wstring(testFolder) + testFileName); });
493
494 {
495 std::ofstream file(std::wstring(testFolder) + testFileName);
496 file << "Windows-content";
497 }
498
499 const auto deniedFilePath = std::wstring(testFolder) + L"plugin-denied.txt";
500 {
501 wil::unique_hfile deniedFile{
502 CreateFileW(deniedFilePath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)};
503 THROW_LAST_ERROR_IF(!deniedFile);
504 }
505
506 auto deniedFileCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove(deniedFilePath); });
507
508 PACL originalAcl = nullptr;
509 wil::unique_hlocal originalDescriptor;
510 THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
511 deniedFilePath.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &originalAcl, nullptr, &originalDescriptor));
512
513 auto restoreAcl = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
514 THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(
515 const_cast<LPWSTR>(deniedFilePath.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, originalAcl, nullptr));
516 });
517
518 EXPLICIT_ACCESSW deniedAccess{};
519 deniedAccess.grfAccessPermissions = FILE_READ_DATA;
520 deniedAccess.grfAccessMode = DENY_ACCESS;
521 deniedAccess.grfInheritance = NO_INHERITANCE;
522 deniedAccess.Trustee.TrusteeForm = TRUSTEE_IS_SID;
523 deniedAccess.Trustee.ptstrName = static_cast<LPWSTR>(Session->UserSid);
524
525 wsl::windows::common::security::unique_acl deniedAcl;
526 THROW_IF_WIN32_ERROR(SetEntriesInAclW(1, &deniedAccess, originalAcl, &deniedAcl));
527 THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(
528 const_cast<LPWSTR>(deniedFilePath.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, deniedAcl.get(), nullptr));
529
530 {
531 wil::unique_handle impersonationToken;
532 THROW_LAST_ERROR_IF(!DuplicateTokenEx(
533 Session->UserToken, TOKEN_IMPERSONATE | TOKEN_QUERY, nullptr, SecurityImpersonation, TokenImpersonation, &impersonationToken));
534 auto revert = wil::impersonate_token(impersonationToken.get());
535 wil::unique_hfile deniedFile{
536 CreateFileW(deniedFilePath.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
537 const auto openError = GetLastError();
538 THROW_HR_IF(E_UNEXPECTED, deniedFile || openError != ERROR_ACCESS_DENIED);
539 }
540
541 // Mount read-write and verify the file can be read from Linux.
542 THROW_IF_FAILED(g_api->WSLCMountFolder(Session->SessionId, testFolder, rwMountpoint, false));
543
544 g_logfile << "WSLC RW folder mounted at: " << rwMountpoint << std::endl;
545
546 auto readCmd = std::format("cat {}/{}", rwMountpoint, testFileName);
547 runCommand(readCmd.c_str());
548
549 auto deniedReadCmd = std::format("cat {}/plugin-denied.txt", rwMountpoint);
550 runCommand(deniedReadCmd.c_str());
551
552 THROW_IF_FAILED(g_api->WSLCUnmountFolder(Session->SessionId, rwMountpoint));
553 }
554
555 // Validate ro mounts.
556 {
557 THROW_IF_FAILED(g_api->WSLCMountFolder(Session->SessionId, L"C:\\", roMountpoint, TRUE));
558
559 g_logfile << "WSLC RO folder mounted at: " << roMountpoint << std::endl;
560
561 // Attempt to write from Linux — should fail on a read-only mount.
562 auto writeCmd = std::format("echo fail > {}/should-not-exist.txt", roMountpoint);
563 runCommand(writeCmd.c_str());
564
565 THROW_IF_FAILED(g_api->WSLCUnmountFolder(Session->SessionId, roMountpoint));
566 }
567
568 // Validate that trying to mount a folder that doesn't exist fails with the expected error code.
569 g_logfile << "WSLCMountFolder(nonexistent): " << g_api->WSLCMountFolder(Session->SessionId, L"C:\\nonexistent", roMountpoint, TRUE)
570 << std::endl;
571
572 // Validate that non-absolute mountpoints are rejected.
573 g_logfile << "WSLCMountFolder(relative): " << g_api->WSLCMountFolder(Session->SessionId, L"C:\\", "relative-mountpoint", TRUE)
574 << std::endl;
575
576 g_logfile << "Test completed" << std::endl;
577 }
578 }
579
580 HRESULT OnWslcSessionStopping(const WSLCSessionInformation* Session)
581 {
582 // Drain the stop-window thread first, then report what it observed. Logging from here rather than
583 // from that thread keeps the log single-writer and the expected output deterministic. Safe
584 // because this is the last event of the session, so the join cannot run inside a VM notification.
585 if (g_stopWindowCaller.joinable())
586 {
587 g_stopWindowCaller.join();
588
589 g_logfile << "WSLC stop-window caller: " << (SUCCEEDED(g_stopWindowCallerResult) ? "ok" : "failed") << std::endl;
590 g_logfile << "WSLC leaked process died: " << (g_leakedProcessDied.load() ? "yes" : "no") << std::endl;
591 }
592
593 // Close the duplicated exit event if the stop-window thread did not get far enough to claim it.
594 // The leaked process wrapper is deliberately not released: it holds a COM proxy marshalled to the
595 // OnWslcVmStopping callback's thread, and releasing it from this one risks the same
596 // RPC_E_WRONG_THREAD hazard that forced the exit event to be cached as a plain handle. It is one
597 // wrapper for the lifetime of a test process, so leaking it is the safer trade.
598 if (auto* exitEvent = g_leakedProcessExitEvent.exchange(nullptr); exitEvent != nullptr)
599 {
600 const wil::unique_handle owned{exitEvent};
601 }
602
603 g_logfile << "WSLC Session stopping, name=" << wsl::shared::string::WideToMultiByte(Session->DisplayName)
604 << ", id=" << Session->SessionId << std::endl;
605
606 return S_OK;
607 }
608
609 HRESULT OnWslcContainerStarted(const WSLCSessionInformation* Session, LPCSTR InspectJson)
610 try
611 {
612 auto container = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectContainer>(InspectJson);
613
614 g_logfile << "WSLC Container started, session=" << Session->SessionId << ", id=" << container.Id << ", name=" << container.Name
615 << ", image=" << container.Config.Image << ", state=" << container.State.Status << std::endl;
616
617 if (g_testType == PluginTestType::WslcContainerRejected)
618 {
619 g_logfile << "OnWslcContainerStarted: ERROR_ACCESS_DENIED" << std::endl;
620 return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
621 }
622
623 return S_OK;
624 }
625 CATCH_RETURN();
626
627 HRESULT OnWslcContainerStopping(const WSLCSessionInformation* Session, LPCSTR ContainerId)
628 {
629 g_logfile << "WSLC Container stopping, session=" << Session->SessionId << ", id=" << ContainerId << std::endl;
630 return S_OK;
631 }
632
633 HRESULT OnWslcImageCreated(const WSLCSessionInformation* Session, LPCSTR InspectJson)
634 {
635 auto image = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectImage>(InspectJson);
636 auto name = (image.RepoTags.has_value() && !image.RepoTags->empty()) ? image.RepoTags->front() : "<none>";
637 g_logfile << "WSLC Image created, session=" << Session->SessionId << ", id=" << image.Id << ", name=" << name << std::endl;
638 return S_OK;
639 }
640
641 HRESULT OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId)
642 {
643 g_logfile << "WSLC Image deleted, session=" << Session->SessionId << ", id=" << ImageId << std::endl;
644 return S_OK;
645 }
646
647 HRESULT OnWslcVmStarted(const WSLCSessionInformation* Session)
648 try
649 {
650 if (g_testType == PluginTestType::WslcSuccess)
651 {
652 // Run once: the checks are written against the first VM of the session.
653 static std::atomic<bool> done = false;
654 if (!done.exchange(true))
655 {
656 RunWslcSuccessChecks(Session);
657 }
658
659 return S_OK;
660 }
661
662 if (g_testType == PluginTestType::WslcVmStopCommitted)
663 {
664 g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl;
665 return S_OK;
666 }
667
668 // The VM-never-started test expects no VM hook to fire at all. Logging here is the diagnostic
669 // that makes a regression visible: any line from this hook fails the expected output.
670 if (g_testType == PluginTestType::WslcVmNeverStarted)
671 {
672 g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl;
673 return S_OK;
674 }
675
676 // Only log/exercise for the dedicated VM-restart test so other WSLC plugin tests (which start
677 // and stop VMs incidentally) are not affected by extra log lines.
678 if (g_testType != PluginTestType::WslcVmRestart)
679 {
680 return S_OK;
681 }
682
683 g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl;
684
685 // Prove the VM is usable from within the started hook, and that calling back into the session
686 // (WSLCCreateProcess acquires a VM lease + the runtime lock) does not deadlock.
687 std::vector<const char*> args = {"/bin/true", nullptr};
688 WSLCProcessHandle process = nullptr;
689 const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr);
690 g_logfile << "WSLC VM started reentrant WSLCCreateProcess: " << (SUCCEEDED(hr) ? "ok" : "failed") << std::endl;
691 if (SUCCEEDED(hr))
692 {
693 g_api->WSLCReleaseProcess(process);
694 }
695
696 // Also exercise a reentrant mount + unmount from the started hook; the session is alive here so
697 // both calls succeed, validating that mount management reentrant from OnVmStarted does not deadlock.
698 constexpr auto* mountpoint = "/test-plugin/vm-started-mount";
699 const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE);
700 if (SUCCEEDED(mountHr))
701 {
702 const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint);
703 g_logfile << "WSLC VM started mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl;
704 }
705 else
706 {
707 g_logfile << "WSLC VM started mount+unmount: skipped" << std::endl;
708 }
709
710 return S_OK;
711 }
712 CATCH_RETURN();
713
714 HRESULT OnWslcVmStopping(const WSLCSessionInformation* Session)
715 try
716 {
717 if (g_testType == PluginTestType::WslcVmNeverStarted)
718 {
719 g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl;
720 return S_OK;
721 }
722
723 if (g_testType == PluginTestType::WslcVmStopCommitted)
724 {
725 // Only the idle teardown is interesting here. The session's final teardown races with the
726 // session-stopping notification, which is delivered independently, so logging it would make
727 // the expected output order-dependent on that race.
728 if (g_stopWindowCaller.joinable())
729 {
730 return S_OK;
731 }
732
733 g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl;
734
735 // Deliberately leave a live process behind when this callback returns. The stop is committed
736 // before it is announced, so the VM goes away regardless and this process dies with it --
737 // which is exactly what the callback was just told would happen.
738 //
739 // Created before the thread below is started so its exit event is published first: that thread
740 // claims the event and must not race ahead of it.
741 std::vector<const char*> args = {"/bin/sleep", "60", nullptr};
742 WSLCProcessHandle leaked = nullptr;
743 const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &leaked, nullptr);
744 g_leakedProcess.store(leaked);
745
746 // Cache the exit event while still on the thread the process proxy is marshalled to.
747 if (SUCCEEDED(hr))
748 {
749 HANDLE exitEvent = nullptr;
750 if (SUCCEEDED(g_api->WSLCProcessGetExitEvent(leaked, &exitEvent)))
751 {
752 g_leakedProcessExitEvent.store(exitEvent);
753 }
754 }
755
756 g_logfile << "WSLC VM stopping leaked process: " << (SUCCEEDED(hr) ? "ok" : "failed") << std::endl;
757
758 // A call from a thread this plugin owns is served on the same terms as the callback itself:
759 // by the VM that is stopping, not by a future one and not by a new one. It must not block on
760 // the teardown. Results are logged when this thread is joined, so the output stays deterministic.
761 const auto sessionId = Session->SessionId;
762 g_stopWindowCaller = std::thread([sessionId]() {
763 std::vector<const char*> processArgs = {"/bin/true", nullptr};
764 WSLCProcessHandle process = nullptr;
765 g_stopWindowCallerResult = g_api->WSLCCreateProcess(sessionId, processArgs[0], processArgs.data(), nullptr, &process, nullptr);
766 if (SUCCEEDED(g_stopWindowCallerResult))
767 {
768 g_api->WSLCReleaseProcess(process);
769 }
770
771 // The announced stop takes the VM away and the leaked process with it. Prove that
772 // directly instead of inferring it from the fact that a new VM started -- a process that
773 // outlived the stop is the exact symptom of an announced stop that did not happen.
774 if (auto* exitEvent = g_leakedProcessExitEvent.exchange(nullptr); exitEvent != nullptr)
775 {
776 // GetExitEvent is marshalled as an [out, system_handle(sh_event)] parameter, so this
777 // is a duplicate owned by this process.
778 const wil::unique_handle owned{exitEvent};
779 g_leakedProcessDied = WaitForSingleObject(owned.get(), 30 * 1000) == WAIT_OBJECT_0;
780 }
781 });
782
783 // Give the thread time to issue its call inside the stop window. If it has not, the test still
784 // passes -- it just proves less.
785 std::this_thread::sleep_for(500ms);
786
787 return S_OK;
788 }
789
790 if (g_testType != PluginTestType::WslcVmRestart)
791 {
792 return S_OK;
793 }
794
795 g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl;
796
797 // Proves OnVmStopping doesn't deadlock a plugin that calls back in: on idle teardown these are
798 // served by the VM that is still stopping and succeed; on permanent teardown they fail cleanly.
799 std::vector<const char*> args = {"/bin/true", nullptr};
800 WSLCProcessHandle process = nullptr;
801 const auto processHr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr);
802 g_logfile << "WSLC VM stopping reentrant WSLCCreateProcess: " << (SUCCEEDED(processHr) ? "ok" : "failed") << std::endl;
803 if (SUCCEEDED(processHr))
804 {
805 g_api->WSLCReleaseProcess(process);
806 }
807
808 constexpr auto* mountpoint = "/test-plugin/vm-stopping-mount";
809 const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE);
810 if (SUCCEEDED(mountHr))
811 {
812 const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint);
813 g_logfile << "WSLC VM stopping mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl;
814 }
815 else
816 {
817 g_logfile << "WSLC VM stopping mount+unmount: skipped" << std::endl;
818 }
819
820 return S_OK;
821 }
822 CATCH_RETURN();
823
824 EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPluginAPIV1* Api, WSLPluginHooksV1* Hooks)
825 {
826 try
827 {
828 const auto key = OpenTestRegistryKey(KEY_READ);
829
830 const std::wstring outputFile = ReadString(key.get(), nullptr, c_logFile);
831 g_logfile.open(outputFile);
832 THROW_HR_IF(E_UNEXPECTED, !g_logfile);
833
834 g_testType = static_cast<PluginTestType>(ReadDword(key.get(), nullptr, c_testType, static_cast<DWORD>(PluginTestType::Invalid)));
835 THROW_HR_IF(E_INVALIDARG, static_cast<DWORD>(g_testType) <= 0 || static_cast<DWORD>(g_testType) > static_cast<DWORD>(PluginTestType::WslcVmNeverStarted));
836
837 g_logfile << "Plugin loaded. TestMode=" << static_cast<DWORD>(g_testType) << std::endl;
838 g_api = Api;
839 Hooks->OnVMStarted = &OnVmStarted;
840 Hooks->OnVMStopping = &OnVmStopping;
841 Hooks->OnDistributionStarted = &OnDistroStarted;
842 Hooks->OnDistributionStopping = &OnDistroStopping;
843 Hooks->OnDistributionRegistered = &OnDistributionRegistered;
844 Hooks->OnDistributionUnregistered = &OnDistributionUnregistered;
845 Hooks->OnSessionCreated = &OnWslcSessionCreated;
846 Hooks->OnSessionStopping = &OnWslcSessionStopping;
847 Hooks->ContainerStarted = &OnWslcContainerStarted;
848 Hooks->ContainerStopping = &OnWslcContainerStopping;
849 Hooks->ImageCreated = &OnWslcImageCreated;
850 Hooks->ImageDeleted = &OnWslcImageDeleted;
851 Hooks->WslcVmStarted = &OnWslcVmStarted;
852 Hooks->WslcVmStopping = &OnWslcVmStopping;
853
854 if (g_testType == PluginTestType::FailToLoad)
855 {
856 g_logfile << "OnLoad: E_UNEXPECTED" << std::endl;
857 return E_UNEXPECTED;
858 }
859 else if (g_testType == PluginTestType::PluginRequiresUpdate)
860 {
861 g_logfile << "OnLoad: WSL_E_PLUGINREQUIRESUPDATE" << std::endl;
862
863 WSL_PLUGIN_REQUIRE_VERSION(9999, 99, 99, Api);
864 }
865 }
866 catch (...)
867 {
868 const auto error = wil::ResultFromCaughtException();
869 if (g_logfile)
870 {
871 g_logfile << "Failed to initialize plugin, " << error << std::endl;
872 }
873
874 return error;
875 }
876 return S_OK;
877 }