345
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d /tmp/.X11-unix"), 0L);
346
}
347
348
- WSL2_TEST_METHOD(SystemdBinfmtIsRestored)
348
+ WSL2_TEST_METHOD(BinfmtStatusIsLocked)
349
{
350
- // Override WSL's binfmt interpreter
351
- VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mkdir -p /usr/lib/binfmt.d && echo ':WSLInterop:M::MZ::/bin/echo:PF' > /usr/lib/binfmt.d/dummy.conf"), 0L);
352
-
353
- auto cleanupBinfmt = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
354
- LxsstuLaunchWsl(L"rm /usr/lib/binfmt.d/dummy.conf");
355
- WslShutdown(); // Required since this test registers a custom binfmt interpreter.
356
- });
350
+ //
351
+ // Validates the protection mechanism for the cross-distro binfmt wipe bug.
352
+ //
353
+ // Fix: per-distro init bind-mounts a read-only file over
354
+ // /proc/sys/fs/binfmt_misc/status before exec'ing the distro's init
355
+ // (see LockBinfmtStatusReadOnly in src/linux/init/init.cpp). systemd-shutdown's
356
+ // disable_binfmt() writes "-1" to that file to clear the kernel-global
357
+ // binfmt_misc table at shutdown; with the bind-mount in place the write
358
+ // fails with EROFS so the entries shared with other running distros
359
+ // survive. Per-entry operations (registering new entries via /register,
360
+ // unregistering individual entries via the entry file) are unaffected.
361
+ //
362
363
+ // Default: bind-mount must be in place.
364
{
359
- // Enable systemd (restarts distro).
365
+ // EnableSystemd raises /proc/sys/fs/nr_open VM-wide; without a full
366
+ // VM teardown that bumped value persists across distro restarts and
367
+ // breaks later tests like ResourceLimits that assume the kernel default.
368
+ auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
369
auto cleanupSystemd = EnableSystemd();
370
362
- auto validateBinfmt = []() {
363
- // Validate that WSL's binfmt interpreter is still in place.
364
- auto [cmdOutput, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
365
- VERIFY_ARE_EQUAL(cmdOutput, L"ok\r\n");
366
- };
371
+ // /status is its own mount point.
372
+ VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mountpoint -q /proc/sys/fs/binfmt_misc/status"), 0u);
373
368
- validateBinfmt();
374
+ // Reading /status returns the lock-file content ("enabled\n") so
375
+ // callers that just check whether binfmt_misc is enabled still get a
376
+ // sensible answer.
377
+ {
378
+ auto [status, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/fs/binfmt_misc/status");
379
+ VERIFY_ARE_EQUAL(status, L"enabled\n");
380
+ }
381
370
- // Validate that this still works after restarting the distribution.
371
- TerminateDistribution();
372
- validateBinfmt();
382
+ // Direct write to /status — the wipe vector — must fail with EROFS.
383
+ // The shell's redirection error ("cannot create ...: Read-only file
384
+ // system") goes to the shell's stderr when the `>` open fails.
385
+ {
386
+ auto [_, err] = LxsstuLaunchWslAndCaptureOutput(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/status; exit 0'");
387
+ VERIFY_IS_TRUE(err.find(L"Read-only file system") != std::wstring::npos);
388
+ }
389
374
- // Validate that stopping or restarting systemd-binfmt doesn't break interop.
375
- VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl stop systemd-binfmt.service"), 0u);
376
- validateBinfmt();
390
+ // WSLInterop survives the failed wipe attempt.
391
+ VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/WSLInterop"), 0L);
392
378
- VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl restart systemd-binfmt.service"), 0u);
379
- validateBinfmt();
393
+ // Runtime registration via /register still works (we only block /status).
394
+ VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sh -c 'echo \":wsltestbinfmt:M::WSLTESTMAGIC::/bin/echo:\" > /proc/sys/fs/binfmt_misc/register'"), 0L);
395
381
- // Validate that the unit is regenerated after a daemon-reload.
382
- VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl daemon-reload && systemctl restart systemd-binfmt.service"), 0u);
383
- validateBinfmt();
396
+ // binfmt_misc is VM-global, so a leftover wsltestbinfmt entry would
397
+ // cascade into later tests. Always remove it on scope exit.
398
+ auto cleanupTestEntry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
399
+ LxsstuLaunchWsl(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/wsltestbinfmt 2>/dev/null || true'");
400
+ });
401
+
402
+ VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/wsltestbinfmt"), 0L);
403
+
404
+ // Per-entry unregister (writing -1 to the entry file, not /status) still works.
405
+ VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/wsltestbinfmt'"), 0L);
406
+ VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/wsltestbinfmt"), 0L);
407
+ cleanupTestEntry.release();
408
+
409
+ // Interop still works.
410
+ {
411
+ auto [cmd, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
412
+ VERIFY_ARE_EQUAL(cmd, L"ok\r\n");
413
+ }
414
}
415
416
+ // protectBinfmt=false: bind-mount must NOT be installed (kill switch).
417
+ // EnableSystemd's cleanup re-launches the distro to revert wsl.conf and
418
+ // then terminates it; that termination invokes systemd-shutdown's
419
+ // disable_binfmt() which wipes the kernel-global table because
420
+ // protectBinfmt=false leaves /status writable. WslShutdown registered
421
+ // FIRST (runs LAST in LIFO unwind) ensures the VM is fully torn down
422
+ // after the wipe, so the next test starts a fresh VM where mini_init
423
+ // re-registers WSLInterop.
424
{
387
- // Enable systemd (restarts distro).
425
+ auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
426
auto cleanupSystemd = EnableSystemd("protectBinfmt=false");
427
390
- // Validate that WSL's binfmt interpreter is overridden
391
- auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
392
- VERIFY_IS_TRUE(wsl::shared::string::IsEqual(output, L"/mnt/c/Windows/system32/cmd.exe cmd.exe /c echo ok\n", true));
428
+ VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"mountpoint -q /proc/sys/fs/binfmt_misc/status"), 0L);
429
}
430
}
431
449
VERIFY_ARE_EQUAL(out, L"hello\n");
450
}
451
452
+ WSL2_TEST_METHOD(BinfmtSurvivesDistroTermination)
453
+ {
454
+ //
455
+ // Regression test for the "Exec format error" bug: binfmt_misc registrations
456
+ // (most importantly WSLInterop) must survive when a peer systemd-enabled distro
457
+ // terminates. Before this fix, systemd-shutdown's disable_binfmt() wrote `-1`
458
+ // to /proc/sys/fs/binfmt_misc/status, which clears the entire binfmt_misc
459
+ // entry table. binfmt_misc itself is a single kernel-global registry — it is
460
+ // not isolated per distro — so that one write wiped WSLInterop for every
461
+ // running distro and broke Windows interop everywhere.
462
+ //
463
+
464
+ constexpr auto peerDistroName = L"binfmt-peer-test";
465
+
466
+ // EnableSystemd raises /proc/sys/fs/nr_open VM-wide; without a full
467
+ // VM teardown that bumped value persists across distro restarts and
468
+ // breaks later tests like ResourceLimits that assume the kernel default.
469
+ auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
470
+
471
+ // Enable systemd on the primary test distro.
472
+ auto cleanupSystemd = EnableSystemd();
473
+
474
+ // Import a second distro from the same tarball as the test distro.
475
+ VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", peerDistroName, g_testDistroPath)), 0L);
476
+
477
+ auto cleanupPeer =
478
+ wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(std::format(L"--unregister {}", peerDistroName)); });
479
+
480
+ // Enable systemd in the peer distro (no helper exists for non-test distros).
481
+ VERIFY_ARE_EQUAL(
482
+ LxsstuLaunchWsl(std::format(L"-d {} -- sh -c \"mkdir -p /etc && printf '[boot]\\nsystemd=true\\n' > /etc/wsl.conf\"", peerDistroName)),
483
+ 0L);
484
+
485
+ // Terminate so the config takes effect on next start.
486
+ TerminateDistribution(peerDistroName);
487
+
488
+ // Verify interop works in both distros (this also starts the peer with systemd).
489
+ {
490
+ auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo alive");
491
+ VERIFY_ARE_EQUAL(out, L"alive\r\n");
492
+ }
493
+
494
+ {
495
+ auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} -- cmd.exe /c echo alive", peerDistroName));
496
+ VERIFY_ARE_EQUAL(out, L"alive\r\n");
497
+ }
498
+
499
+ // Terminate the peer distro — this triggers systemd shutdown. Without
500
+ // the fix, systemd-shutdown's disable_binfmt() would clear the kernel-
501
+ // global binfmt_misc table for every running distro.
502
+ TerminateDistribution(peerDistroName);
503
+
504
+ // Verify interop still works in the primary distro.
505
+ {
506
+ auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo survived");
507
+ VERIFY_ARE_EQUAL(out, L"survived\r\n");
508
+ }
509
+
510
+ // Verify the binfmt entry still exists and carries the F (fix-binary) flag.
511
+ // The F flag is required so the kernel resolves the interpreter at
512
+ // registration time, making the entry independent of mount-namespace state.
513
+ {
514
+ auto [flags, _] = LxsstuLaunchWslAndCaptureOutput(L"grep ^flags /proc/sys/fs/binfmt_misc/WSLInterop");
515
+ VERIFY_IS_TRUE(flags.find(L"F") != std::wstring::npos);
516
+ }
517
+ }
518
+
519
TEST_METHOD(Dup)
520
{
521
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dup", L"Dup"));