Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/linux/init/binfmt.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ Module Name:
#define LX_INIT_BINFMT_NAME "WSLInterop"
#define BINFMT_MISC_MOUNT_TARGET "/proc/sys/fs/binfmt_misc"
#define BINFMT_MISC_REGISTER_FILE BINFMT_MISC_MOUNT_TARGET "/register"

//
// N.B. The 'P' flag preserves Argv[0]. The 'F' flag (fix-binary) opens the interpreter at
// registration time, making it available across mount namespaces and chroot environments.
// WSL1 (lxcore) does not support the 'F' flag, so it must only be used in WSL2 (VM) paths.
//

#define BINFMT_INTEROP_REGISTRATION_STRING(Name) ":" Name ":M::MZ::" LX_INIT_PATH ":P"
#define BINFMT_INTEROP_REGISTRATION_STRING_VM(Name) ":" Name ":M::MZ::" LX_INIT_PATH ":FP"

int CreateNtProcess(int Argc, char* Argv[]);
92 changes: 56 additions & 36 deletions src/linux/init/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ void InitTerminateInstanceInternal(const wsl::linux::WslDistributionConfig& Conf

void InstallSystemdUnit(const char* Path, const std::string& Name, const char* Content);

void LockBinfmtStatusReadOnly();

int GenerateSystemdUnits(int Argc, char** Argv);

int GenerateUserSystemdUnits(int Argc, char** Argv);
Expand Down Expand Up @@ -322,17 +324,13 @@ int GenerateSystemdUnits(int Argc, char** Argv)
LOG_INFO("Generating WSL systemd units in {}", installPath);

bool enableGuiApps = true;
bool protectBinfmt = true;
bool interopEnabled = true;
std::string automountRoot = "/mnt";

wil::unique_file File{fopen("/etc/wsl.conf", "r")};
if (File)
{
std::vector<ConfigKey> ConfigKeys = {
ConfigKey(wsl::linux::c_ConfigEnableGuiAppsOption, enableGuiApps),
ConfigKey(wsl::linux::c_ConfigBootProtectBinfmtOption, protectBinfmt),
ConfigKey(wsl::linux::c_ConfigInteropEnabledOption, interopEnabled),
ConfigKey(wsl::linux::c_ConfigAutoMountRoot, automountRoot),

};
Expand Down Expand Up @@ -378,38 +376,6 @@ ExecStart=/bin/mount -o bind,ro,X-mount.mkdir -t none /mnt/wslg/.X11-unix /tmp/.
InstallSystemdUnit(installPath, "wslg", x11UnitContent);
}

if (interopEnabled && protectBinfmt)
{
// N.B. ExecStop is required to prevent distributions from removing the WSL binfmt entry on shutdown.
auto systemdBinfmtContent = std::format(
R"(# Note: This file is generated by WSL to prevent binfmt.d from overriding WSL's binfmt interpreter.
# To disable this unit, add the following to /etc/wsl.conf:
# [boot]
# protectBinfmt=false

[Service]
ExecStop=
ExecStart=/bin/sh -c '(echo -1 > {}/{}) ; (echo "{}" > {})' )",
BINFMT_MISC_MOUNT_TARGET,
LX_INIT_BINFMT_NAME,
BINFMT_INTEROP_REGISTRATION_STRING(LX_INIT_BINFMT_NAME),
BINFMT_MISC_REGISTER_FILE);

// Install the override for systemd-binfmt.service.
{
auto overrideFolder = std::format("{}/systemd-binfmt.service.d", installPath);
THROW_LAST_ERROR_IF(UtilMkdirPath(overrideFolder.c_str(), 0755) < 0);
THROW_LAST_ERROR_IF(WriteToFile(std::format("{}/override.conf", overrideFolder).c_str(), systemdBinfmtContent.c_str()) < 0);
}

// Install the override for binfmt-support.service.
{
auto overrideFolder = std::format("{}/binfmt-support.service.d", installPath);
THROW_LAST_ERROR_IF(UtilMkdirPath(overrideFolder.c_str(), 0755) < 0);
THROW_LAST_ERROR_IF(WriteToFile(std::format("{}/override.conf", overrideFolder).c_str(), systemdBinfmtContent.c_str()) < 0);
}
}

return 0;
}
CATCH_LOG()
Expand Down Expand Up @@ -2389,6 +2355,14 @@ Return Value:
PointerVector.push_back(nullptr);
};

// The wipe at systemd shutdown clears entries for every distro in
// the VM, not just the terminating one, so install the protection
// regardless of this distro's own InteropEnabled setting.
if (Config.BootProtectBinfmt)
{
LockBinfmtStatusReadOnly();
}

CreateWslSystemdUnits(Config);

const char* Argv[] = {INIT_PATH, nullptr};
Expand Down Expand Up @@ -2839,6 +2813,52 @@ try
}
CATCH_LOG();

void LockBinfmtStatusReadOnly()

/*++

Routine Description:

Bind-mounts a read-only file over /proc/sys/fs/binfmt_misc/status so that
systemd-shutdown's disable_binfmt() can't wipe the kernel-global binfmt
registry when this distro terminates. Without this, terminating any
systemd-enabled distro would clear WSLInterop in every other running
distro and break Windows interop VM-wide.

Arguments:

None.

Return Value:

None. Failures are logged; this is a best-effort hardening step.

--*/

try
{
constexpr auto* lockFile = "/run/wsl/binfmt-status-lock";
constexpr auto* statusFile = BINFMT_MISC_MOUNT_TARGET "/status";
constexpr std::string_view content{"enabled\n"};

THROW_LAST_ERROR_IF(UtilMkdirPath("/run/wsl", 0755) < 0);

const wil::unique_fd fd{TEMP_FAILURE_RETRY(open(lockFile, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644))};
THROW_LAST_ERROR_IF(!fd);
THROW_LAST_ERROR_IF(write(fd.get(), content.data(), content.size()) != static_cast<ssize_t>(content.size()));

THROW_LAST_ERROR_IF(mount(lockFile, statusFile, nullptr, MS_BIND, nullptr) < 0);

// If the remount fails, tear down the bind-mount so /status either reflects
// the real binfmt_misc control file or is correctly read-only. A writable
// shadow would silently swallow writes that callers expect to reach the
// kernel (e.g. "echo -1 > /status").
auto unmountOnFailure = wil::scope_exit([&]() { umount2(statusFile, MNT_DETACH); });
THROW_LAST_ERROR_IF(mount(nullptr, statusFile, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr) < 0);
Comment thread
benhillis marked this conversation as resolved.
unmountOnFailure.release();
}
Comment thread
benhillis marked this conversation as resolved.
CATCH_LOG();

void HardenMirroredNetworkingSettingsAgainstSystemd()

/*++
Expand Down
2 changes: 1 addition & 1 deletion src/linux/init/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Module Name:
#include "SocketChannel.h"

#define BSDTAR_PATH "/usr/bin/bsdtar"
#define BINFMT_REGISTER_STRING ":" LX_INIT_BINFMT_NAME ":M::MZ::" LX_INIT_PATH ":FP\n"
#define BINFMT_REGISTER_STRING BINFMT_INTEROP_REGISTRATION_STRING_VM(LX_INIT_BINFMT_NAME) "\n"
#define BINFMT_PATH PROCFS_PATH "/sys/fs/binfmt_misc"
#define CHRONY_CONF_PATH ETC_PATH "/chrony.conf"
#define CHRONYD_PATH "/sbin/chronyd"
Expand Down
163 changes: 133 additions & 30 deletions test/windows/UnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,51 +345,87 @@ class UnitTests
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d /tmp/.X11-unix"), 0L);
}

WSL2_TEST_METHOD(SystemdBinfmtIsRestored)
WSL2_TEST_METHOD(BinfmtStatusIsLocked)
{
// Override WSL's binfmt interpreter
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);

auto cleanupBinfmt = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
LxsstuLaunchWsl(L"rm /usr/lib/binfmt.d/dummy.conf");
WslShutdown(); // Required since this test registers a custom binfmt interpreter.
});
//
// Validates the protection mechanism for the cross-distro binfmt wipe bug.
//
// Fix: per-distro init bind-mounts a read-only file over
// /proc/sys/fs/binfmt_misc/status before exec'ing the distro's init
// (see LockBinfmtStatusReadOnly in src/linux/init/init.cpp). systemd-shutdown's
// disable_binfmt() writes "-1" to that file to clear the kernel-global
// binfmt_misc table at shutdown; with the bind-mount in place the write
// fails with EROFS so the entries shared with other running distros
// survive. Per-entry operations (registering new entries via /register,
// unregistering individual entries via the entry file) are unaffected.
//

// Default: bind-mount must be in place.
{
// Enable systemd (restarts distro).
// EnableSystemd raises /proc/sys/fs/nr_open VM-wide; without a full
// VM teardown that bumped value persists across distro restarts and
// breaks later tests like ResourceLimits that assume the kernel default.
auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
auto cleanupSystemd = EnableSystemd();

auto validateBinfmt = []() {
// Validate that WSL's binfmt interpreter is still in place.
auto [cmdOutput, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
VERIFY_ARE_EQUAL(cmdOutput, L"ok\r\n");
};
// /status is its own mount point.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mountpoint -q /proc/sys/fs/binfmt_misc/status"), 0u);

validateBinfmt();
// Reading /status returns the lock-file content ("enabled\n") so
// callers that just check whether binfmt_misc is enabled still get a
// sensible answer.
{
auto [status, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/fs/binfmt_misc/status");
VERIFY_ARE_EQUAL(status, L"enabled\n");
}

// Validate that this still works after restarting the distribution.
TerminateDistribution();
validateBinfmt();
// Direct write to /status — the wipe vector — must fail with EROFS.
// The shell's redirection error ("cannot create ...: Read-only file
// system") goes to the shell's stderr when the `>` open fails.
{
auto [_, err] = LxsstuLaunchWslAndCaptureOutput(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/status; exit 0'");
VERIFY_IS_TRUE(err.find(L"Read-only file system") != std::wstring::npos);
}

// Validate that stopping or restarting systemd-binfmt doesn't break interop.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl stop systemd-binfmt.service"), 0u);
validateBinfmt();
// WSLInterop survives the failed wipe attempt.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/WSLInterop"), 0L);

VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl restart systemd-binfmt.service"), 0u);
validateBinfmt();
// Runtime registration via /register still works (we only block /status).
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sh -c 'echo \":wsltestbinfmt:M::WSLTESTMAGIC::/bin/echo:\" > /proc/sys/fs/binfmt_misc/register'"), 0L);

// Validate that the unit is regenerated after a daemon-reload.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl daemon-reload && systemctl restart systemd-binfmt.service"), 0u);
validateBinfmt();
// binfmt_misc is VM-global, so a leftover wsltestbinfmt entry would
// cascade into later tests. Always remove it on scope exit.
auto cleanupTestEntry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
LxsstuLaunchWsl(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/wsltestbinfmt 2>/dev/null || true'");
});

VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/wsltestbinfmt"), 0L);

// Per-entry unregister (writing -1 to the entry file, not /status) still works.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/wsltestbinfmt'"), 0L);
VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/wsltestbinfmt"), 0L);
Comment thread
benhillis marked this conversation as resolved.
cleanupTestEntry.release();

// Interop still works.
{
auto [cmd, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
VERIFY_ARE_EQUAL(cmd, L"ok\r\n");
}
}

// protectBinfmt=false: bind-mount must NOT be installed (kill switch).
// EnableSystemd's cleanup re-launches the distro to revert wsl.conf and
// then terminates it; that termination invokes systemd-shutdown's
// disable_binfmt() which wipes the kernel-global table because
// protectBinfmt=false leaves /status writable. WslShutdown registered
// FIRST (runs LAST in LIFO unwind) ensures the VM is fully torn down
// after the wipe, so the next test starts a fresh VM where mini_init
// re-registers WSLInterop.
{
// Enable systemd (restarts distro).
auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
auto cleanupSystemd = EnableSystemd("protectBinfmt=false");

// Validate that WSL's binfmt interpreter is overridden
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
VERIFY_IS_TRUE(wsl::shared::string::IsEqual(output, L"/mnt/c/Windows/system32/cmd.exe cmd.exe /c echo ok\n", true));
VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"mountpoint -q /proc/sys/fs/binfmt_misc/status"), 0L);
}
}

Expand All @@ -413,6 +449,73 @@ class UnitTests
VERIFY_ARE_EQUAL(out, L"hello\n");
}

WSL2_TEST_METHOD(BinfmtSurvivesDistroTermination)
{
//
// Regression test for the "Exec format error" bug: binfmt_misc registrations
// (most importantly WSLInterop) must survive when a peer systemd-enabled distro
// terminates. Before this fix, systemd-shutdown's disable_binfmt() wrote `-1`
// to /proc/sys/fs/binfmt_misc/status, which clears the entire binfmt_misc
// entry table. binfmt_misc itself is a single kernel-global registry — it is
// not isolated per distro — so that one write wiped WSLInterop for every
// running distro and broke Windows interop everywhere.
//

constexpr auto peerDistroName = L"binfmt-peer-test";

// EnableSystemd raises /proc/sys/fs/nr_open VM-wide; without a full
// VM teardown that bumped value persists across distro restarts and
// breaks later tests like ResourceLimits that assume the kernel default.
auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });

// Enable systemd on the primary test distro.
auto cleanupSystemd = EnableSystemd();

// Import a second distro from the same tarball as the test distro.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", peerDistroName, g_testDistroPath)), 0L);

auto cleanupPeer =
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(std::format(L"--unregister {}", peerDistroName)); });

// Enable systemd in the peer distro (no helper exists for non-test distros).
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"-d {} -- sh -c \"mkdir -p /etc && printf '[boot]\\nsystemd=true\\n' > /etc/wsl.conf\"", peerDistroName)),
0L);

// Terminate so the config takes effect on next start.
TerminateDistribution(peerDistroName);

// Verify interop works in both distros (this also starts the peer with systemd).
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo alive");
VERIFY_ARE_EQUAL(out, L"alive\r\n");
}

{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} -- cmd.exe /c echo alive", peerDistroName));
VERIFY_ARE_EQUAL(out, L"alive\r\n");
}

// Terminate the peer distro — this triggers systemd shutdown. Without
// the fix, systemd-shutdown's disable_binfmt() would clear the kernel-
// global binfmt_misc table for every running distro.
TerminateDistribution(peerDistroName);

// Verify interop still works in the primary distro.
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo survived");
VERIFY_ARE_EQUAL(out, L"survived\r\n");
}

// Verify the binfmt entry still exists and carries the F (fix-binary) flag.
// The F flag is required so the kernel resolves the interpreter at
// registration time, making the entry independent of mount-namespace state.
{
auto [flags, _] = LxsstuLaunchWslAndCaptureOutput(L"grep ^flags /proc/sys/fs/binfmt_misc/WSLInterop");
VERIFY_IS_TRUE(flags.find(L"F") != std::wstring::npos);
}
}

TEST_METHOD(Dup)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dup", L"Dup"));
Expand Down