@@ -17,11 +17,14 @@ use std::process::Command;
1717#[ cfg( target_os = "linux" ) ]
1818use std:: process:: Stdio ;
1919
20- /// Installed location of the privileged helper.
20+ /// Installed location of the privileged helper (Linux/macOS only — Windows
21+ /// elevates in-process via PowerShell and never invokes the helper binary).
22+ #[ cfg( not( target_os = "windows" ) ) ]
2123pub const HELPER_PATH : & str = "/usr/lib/freeyourdisk/freeyourdisk-helper" ;
2224
2325/// Resolve the helper binary: the installed path in production, or a sibling of
24- /// the running executable when developing (`cargo tauri dev`).
26+ /// the running executable when developing (`cargo tauri dev`). Linux/macOS only.
27+ #[ cfg( not( target_os = "windows" ) ) ]
2528pub fn resolve_helper_path ( ) -> PathBuf {
2629 let installed = PathBuf :: from ( HELPER_PATH ) ;
2730 if installed. exists ( ) {
@@ -154,8 +157,8 @@ pub fn pkexec_helper(plan: &DeletionPlan) -> ExecutionReport {
154157}
155158
156159/// Windows: relaunch THIS exe elevated (UAC) in headless `--apply` mode to run
157- /// the root plan. Only the parent PID is passed as an argument (no spaces) ; both
158- /// sides derive `%TEMP%\fyd-apply-<pid >-{plan,report}.json`. Elevation uses
160+ /// the root plan. Only a random digit-only token is passed as an argument; both
161+ /// sides derive `%TEMP%\fyd-apply-<token >-{plan,report}.json`. Elevation uses
159162/// `powershell Start-Process -Verb RunAs -Wait` — the WinAPI-free analogue of the
160163/// macOS osascript-admin path.
161164#[ cfg( target_os = "windows" ) ]
@@ -164,7 +167,7 @@ pub fn pkexec_helper(plan: &DeletionPlan) -> ExecutionReport {
164167 Ok ( json) => json,
165168 Err ( err) => return err_report ( plan, & err. to_string ( ) ) ,
166169 } ;
167- let token = std :: process :: id ( ) . to_string ( ) ;
170+ let token = elevation_token ( ) ;
168171 let tmp = std:: env:: temp_dir ( ) ;
169172 let plan_path = tmp. join ( format ! ( "fyd-apply-{token}-plan.json" ) ) ;
170173 let report_path = tmp. join ( format ! ( "fyd-apply-{token}-report.json" ) ) ;
@@ -223,11 +226,33 @@ pub fn pkexec_smart(devices: &[String]) -> Vec<SmartInfo> {
223226 }
224227}
225228
226- /// Windows SMART is implemented in Phase 3 (bundled smartctl.exe via the elevated
227- /// executor). Until then, return no SMART data (the UI degrades gracefully).
228229#[ cfg( target_os = "windows" ) ]
229230pub fn pkexec_smart ( _devices : & [ String ] ) -> Vec < SmartInfo > {
230- Vec :: new ( )
231+ // The elevated child self-discovers devices via `smartctl --scan-open`, so
232+ // the caller's device list is unused. One UAC prompt; report read from file.
233+ let token = elevation_token ( ) ;
234+ let report_path = std:: env:: temp_dir ( ) . join ( format ! ( "fyd-smart-{token}-report.json" ) ) ;
235+ let _ = std:: fs:: remove_file ( & report_path) ;
236+
237+ let Ok ( exe) = std:: env:: current_exe ( ) else {
238+ return Vec :: new ( ) ;
239+ } ;
240+ let exe_ps = exe. to_string_lossy ( ) . replace ( '\'' , "''" ) ;
241+ let ps = format ! (
242+ "Start-Process -FilePath '{exe_ps}' -ArgumentList '--smart','{token}' -Verb RunAs -Wait -WindowStyle Hidden"
243+ ) ;
244+ let status = Command :: new ( "C:\\ Windows\\ System32\\ WindowsPowerShell\\ v1.0\\ powershell.exe" )
245+ . args ( [ "-NoProfile" , "-NonInteractive" , "-Command" , & ps] )
246+ . status ( ) ;
247+ let result = match status {
248+ Ok ( s) if s. success ( ) => std:: fs:: read_to_string ( & report_path)
249+ . ok ( )
250+ . and_then ( |raw| serde_json:: from_str ( & raw ) . ok ( ) )
251+ . unwrap_or_default ( ) ,
252+ _ => Vec :: new ( ) ,
253+ } ;
254+ let _ = std:: fs:: remove_file ( & report_path) ;
255+ result
231256}
232257
233258// ---------------------------------------------------------------------------
@@ -300,7 +325,8 @@ pub fn pkexec_smart(devices: &[String]) -> Vec<SmartInfo> {
300325/// Install SMART tools as root via the helper (`install-deps <manager> <pkg>…`).
301326/// The helper re-validates the package names against its own allowlist.
302327/// (Linux only — macOS installs via Homebrew at user level.)
303- #[ cfg( not( target_os = "macos" ) ) ]
328+ // Linux only: macOS installs via brew, Windows via winget (winget_install_smart).
329+ #[ cfg( target_os = "linux" ) ]
304330pub fn pkexec_install_deps ( manager : & str , packages : & [ String ] ) -> InstallReport {
305331 let mut cmd = Command :: new ( "pkexec" ) ;
306332 cmd. arg ( resolve_helper_path ( ) )
@@ -321,6 +347,54 @@ pub fn pkexec_install_deps(manager: &str, packages: &[String]) -> InstallReport
321347 }
322348}
323349
350+ /// Windows: install smartmontools via winget. `--id` is a fixed allowlisted
351+ /// package; nothing user-controlled reaches the command line.
352+ #[ cfg( target_os = "windows" ) ]
353+ pub fn winget_install_smart ( ) -> InstallReport {
354+ let out = Command :: new ( "winget" )
355+ . args ( [
356+ "install" ,
357+ "--id" ,
358+ "smartmontools.smartmontools" ,
359+ "--accept-source-agreements" ,
360+ "--accept-package-agreements" ,
361+ "--silent" ,
362+ ] )
363+ . output ( ) ;
364+ match out {
365+ Ok ( o) if o. status . success ( ) => InstallReport {
366+ success : true ,
367+ message : "Installed: smartmontools" . to_string ( ) ,
368+ } ,
369+ Ok ( o) => InstallReport {
370+ success : false ,
371+ message : String :: from_utf8_lossy ( & o. stderr )
372+ . lines ( )
373+ . last ( )
374+ . unwrap_or ( "winget install failed" )
375+ . to_string ( ) ,
376+ } ,
377+ Err ( err) => InstallReport {
378+ success : false ,
379+ message : format ! ( "failed to run winget: {err}" ) ,
380+ } ,
381+ }
382+ }
383+
384+ /// A random, unguessable token (20 decimal digits) for the elevated-IPC temp
385+ /// file names. Random — not the PID — so a local same-user attacker cannot
386+ /// pre-create a junction/file at a predictable path (TOCTOU) to redirect the
387+ /// admin write or inject a forged report. Digit-only to satisfy the elevated
388+ /// child's token guard. Falls back to the PID only if the OS RNG is unavailable.
389+ #[ allow( dead_code) ] // used only by the Windows elevated executors
390+ fn elevation_token ( ) -> String {
391+ let mut buf = [ 0u8 ; 8 ] ;
392+ match getrandom:: getrandom ( & mut buf) {
393+ Ok ( ( ) ) => format ! ( "{:020}" , u64 :: from_le_bytes( buf) ) ,
394+ Err ( _) => std:: process:: id ( ) . to_string ( ) ,
395+ }
396+ }
397+
324398#[ cfg( test) ]
325399mod tests {
326400 use super :: * ;
0 commit comments