Skip to main content

winreg_artifacts/
lxss.rs

1//! WSL distro registration parser — HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss
2
3use std::io::Cursor;
4use std::path::PathBuf;
5
6use winreg_core::hive::Hive;
7
8// ── Types ─────────────────────────────────────────────────────────────────────
9
10#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
11pub enum DistroVersion {
12    Wsl1,
13    Wsl2,
14    Unknown,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
18pub enum DistroState {
19    Installed,
20    Running,
21    Unknown,
22}
23
24#[derive(Debug, Clone, serde::Serialize)]
25pub struct LxssDistro {
26    pub guid: String,
27    pub distribution_name: String,
28    pub package_family_name: Option<String>,
29    pub base_path: String,
30    pub state: DistroState,
31    pub version: DistroVersion,
32    pub default_uid: Option<u32>,
33    pub is_default: bool,
34    /// The distro GUID subkey's `LastWriteTime` — approximately when WSL
35    /// registered or updated this distro. `None` when the key carries no
36    /// timestamp.
37    pub last_written: Option<chrono::DateTime<chrono::Utc>>,
38}
39
40impl LxssDistro {
41    /// Returns the path to `ext4.vhdx` for WSL2 distros; `None` for WSL1.
42    ///
43    /// WSL2 stores the Linux filesystem at `<BasePath>\ext4.vhdx`.
44    /// WSL1 uses a directory tree under `%LOCALAPPDATA%\lxss\` instead.
45    pub fn vhdx_path(&self) -> Option<PathBuf> {
46        if self.version != DistroVersion::Wsl2 {
47            return None;
48        }
49        let mut path = PathBuf::from(&self.base_path);
50        path.push("ext4.vhdx");
51        Some(path)
52    }
53}
54
55// ── Key paths ─────────────────────────────────────────────────────────────────
56
57const LXSS_PATH: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Lxss";
58
59// ── Helpers ───────────────────────────────────────────────────────────────────
60
61/// Returns true for GUID-format names like `{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}`.
62fn is_guid(name: &str) -> bool {
63    let s = name.trim();
64    if s.len() != 38 {
65        return false;
66    }
67    let b = s.as_bytes();
68    b[0] == b'{'
69        && b[37] == b'}'
70        && b[9] == b'-'
71        && b[14] == b'-'
72        && b[19] == b'-'
73        && b[24] == b'-'
74        && b[1..9].iter().all(u8::is_ascii_hexdigit)
75        && b[10..14].iter().all(u8::is_ascii_hexdigit)
76        && b[15..19].iter().all(u8::is_ascii_hexdigit)
77        && b[20..24].iter().all(u8::is_ascii_hexdigit)
78        && b[25..37].iter().all(u8::is_ascii_hexdigit)
79}
80
81fn str_val(key: &winreg_core::key::Key<'_>, name: &str) -> Option<String> {
82    key.value(name)
83        .ok()
84        .flatten()
85        .and_then(|v| v.as_string().ok())
86}
87
88fn u32_val(key: &winreg_core::key::Key<'_>, name: &str) -> Option<u32> {
89    key.value(name).ok().flatten().and_then(|v| v.as_u32().ok())
90}
91
92// ── Parser ────────────────────────────────────────────────────────────────────
93
94/// Parse WSL distro registrations from an NTUSER.DAT hive.
95pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<LxssDistro> {
96    let Ok(Some(lxss_key)) = hive.open_key(LXSS_PATH) else {
97        return Vec::new();
98    };
99
100    let default_distro_guid = str_val(&lxss_key, "DefaultDistribution").unwrap_or_default();
101
102    let Ok(subkeys) = lxss_key.subkeys() else {
103        return Vec::new();
104    };
105
106    let mut distros = Vec::new();
107
108    for subkey in subkeys {
109        let guid = subkey.name();
110        if !is_guid(&guid) {
111            continue;
112        }
113
114        let Some(distribution_name) = str_val(&subkey, "DistributionName") else {
115            continue;
116        };
117
118        let Some(base_path) = str_val(&subkey, "BasePath") else {
119            continue;
120        };
121
122        let package_family_name = str_val(&subkey, "PackageFamilyName");
123
124        let state = match u32_val(&subkey, "State") {
125            Some(1) => DistroState::Installed,
126            Some(4) => DistroState::Running,
127            _ => DistroState::Unknown,
128        };
129
130        let version = match u32_val(&subkey, "Version") {
131            Some(1) => DistroVersion::Wsl1,
132            Some(2) => DistroVersion::Wsl2,
133            _ => DistroVersion::Unknown,
134        };
135
136        let default_uid = u32_val(&subkey, "DefaultUid");
137        let is_default = !default_distro_guid.is_empty() && guid == default_distro_guid;
138
139        distros.push(LxssDistro {
140            guid,
141            distribution_name,
142            package_family_name,
143            base_path,
144            state,
145            version,
146            default_uid,
147            is_default,
148            last_written: subkey.last_written(),
149        });
150    }
151
152    distros
153}