fix(windows): repair a data root owned by another account

This commit is contained in:
wonfen
2026-08-30 11:45:04 +08:00
parent 1b76b222fb
commit 632c5b2bc5
3 changed files with 254 additions and 6 deletions
+242 -4
View File
@@ -67,6 +67,15 @@ pub(crate) fn open_or_create_private_current_user_file(path: &Path) -> Result<st
windows_owner::open_or_create_private_file(path)
}
/// Repairs an application data root Windows no longer reports as owned by the current user.
///
/// The Service authenticates by comparing this directory's owner SID against the caller's, so a
/// root left behind by another account locks Service mode out for good.
#[cfg(windows)]
pub(crate) fn repair_app_data_root_owner(app_data_root: &Path) -> Result<()> {
windows_owner::repair_root_owner(app_data_root)
}
#[cfg(windows)]
pub(crate) fn current_user_pipe_sddl() -> Result<String> {
let sid = windows_owner::current_sid()?;
@@ -76,6 +85,7 @@ pub(crate) fn current_user_pipe_sddl() -> Result<String> {
#[cfg(windows)]
mod windows_owner {
use anyhow::{Context as _, Result, bail};
use clash_verge_logging::{Type, logging};
use clash_verge_service_ipc::OWNER_TOKEN_FILE_NAME;
use std::ffi::c_void;
use std::io::{Read as _, Write as _};
@@ -83,7 +93,8 @@ mod windows_owner {
use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _};
use std::path::Path;
use windows_sys::Win32::Foundation::{
CloseHandle, ERROR_FILE_EXISTS, GENERIC_READ, GENERIC_WRITE, GetLastError, INVALID_HANDLE_VALUE, LocalFree,
CloseHandle, ERROR_ACCESS_DENIED, ERROR_FILE_EXISTS, ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND, GENERIC_READ,
GENERIC_WRITE, GetLastError, INVALID_HANDLE_VALUE, LocalFree,
};
use windows_sys::Win32::Security::Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW, GetSecurityInfo, SDDL_REVISION_1,
@@ -97,14 +108,20 @@ mod windows_owner {
};
use windows_sys::Win32::Storage::FileSystem::{
BY_HANDLE_FILE_INFORMATION, CREATE_NEW, CreateFileW, FILE_ALL_ACCESS, FILE_ATTRIBUTE_DIRECTORY,
FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_OPEN_REPARSE_POINT,
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TYPE_DISK, GetFileInformationByHandle, GetFileType,
OPEN_EXISTING, READ_CONTROL, WRITE_DAC,
FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TYPE_DISK,
GetFileInformationByHandle, GetFileType, OPEN_EXISTING, READ_CONTROL, WRITE_DAC, WRITE_OWNER,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
const TOKEN_BYTES: usize = 32;
const ACCESS_ALLOWED_ACE_TYPE: u8 = 0;
/// Reserved files that carry their own owner check and are recreated on demand.
const REGENERATED_FILES: [&str; 3] = [
OWNER_TOKEN_FILE_NAME,
crate::utils::server::INSTANCE_RECORD_FILE,
crate::utils::server::INSTANCE_LOCK_FILE,
];
pub(super) fn current_sid() -> Result<String> {
let mut token = std::ptr::null_mut();
@@ -374,6 +391,227 @@ mod windows_owner {
Ok(())
}
pub(super) fn repair_root_owner(root: &Path) -> Result<()> {
// The Service canonicalizes before checking, so resolve junctions to the same object.
let root = match std::fs::canonicalize(root) {
Ok(path) => path,
// Nothing to repair: this process creates the root and owns it by construction.
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error).context("failed to canonicalize the application data root"),
};
let root = root.as_path();
let sid = current_sid()?;
let descriptor = LocalSecurityDescriptor::from_sid(&sid)?;
let expected = descriptor.owner()?;
let directory = open_no_reparse(root, true, READ_CONTROL)?;
let owned = owner_matches(&directory, expected)?;
drop(directory);
if !owned {
logging!(
warn,
Type::Setup,
"应用数据目录 {root:?} 的所有者不是当前用户,尝试接管"
);
match take_directory_ownership(root, expected) {
Ok(()) => logging!(info, Type::Setup, "已接管应用数据目录所有权"),
// Only a refused takeover is worth moving the user's data over.
Err(error) if is_access_denied(&error) => {
logging!(warn, Type::Setup, "接管所有权被拒绝,改为重建目录: {error:#}");
rebuild_root(root, expected)?;
}
Err(error) => return Err(error),
}
}
// The Service and the singleton check compare these files' owner too.
drop_files_owned_by_others(root, expected)
}
/// Drops the reserved files an earlier account left behind, so they are recreated for this one.
fn drop_files_owned_by_others(root: &Path, expected: PSID) -> Result<()> {
for name in REGENERATED_FILES {
let path = root.join(name);
let ours = match open_no_reparse(&path, false, READ_CONTROL) {
Ok(file) => {
let owned = owner_matches(&file, expected)?;
drop(file);
owned
}
Err(error)
if is_os_error(&error, ERROR_FILE_NOT_FOUND) || is_os_error(&error, ERROR_PATH_NOT_FOUND) =>
{
continue;
}
// A DACL that refuses even the owner read settles it just as well.
Err(_) => false,
};
if !ours {
std::fs::remove_file(&path).with_context(|| format!("failed to drop the stale {name}"))?;
logging!(info, Type::Setup, "已丢弃属于其它账户的 {name}");
}
}
Ok(())
}
fn is_access_denied(error: &anyhow::Error) -> bool {
is_os_error(error, ERROR_ACCESS_DENIED)
}
fn is_os_error(error: &anyhow::Error, code: u32) -> bool {
error
.downcast_ref::<std::io::Error>()
.and_then(std::io::Error::raw_os_error)
== Some(code as i32)
}
/// Rebuilds the root as a directory the current user owns, carrying the contents over.
///
/// Nothing checks the children's owner, so they move as they are. Any failure puts the data
/// back: a half-moved root would read as a fresh install and be filled with defaults.
fn rebuild_root(root: &Path, expected: PSID) -> Result<()> {
let parent = root.parent().context("application data root has no parent")?;
let mut stale = root
.file_name()
.context("application data root has no name")?
.to_os_string();
stale.push(format!(".stale-{}", std::process::id()));
let stale = parent.join(stale);
std::fs::rename(root, &stale).context("failed to set the application data root aside")?;
if let Err(error) = std::fs::create_dir(root) {
// Something else took the name; it is not ours to walk into, so restore blind.
let _ = std::fs::rename(&stale, root);
return Err(error).context("failed to recreate the application data root");
}
if let Err(error) = repopulate_root(root, &stale, expected) {
// The root is the directory just created, so draining it follows no one else's link.
restore_root(root, &stale)?;
return Err(error);
}
// Only the reserved files remain behind.
if let Err(error) = std::fs::remove_dir_all(&stale) {
logging!(warn, Type::Setup, "旧数据目录 {stale:?} 未能清理: {error}");
}
logging!(info, Type::Setup, "已重建应用数据目录");
Ok(())
}
/// Fills the freshly created root from the one set aside, leaving the reserved files behind.
fn repopulate_root(root: &Path, stale: &Path, expected: PSID) -> Result<()> {
// An elevated process creates directories owned by Administrators — the state being repaired.
let directory = open_no_reparse(root, true, READ_CONTROL)?;
let owned = owner_matches(&directory, expected)?;
drop(directory);
if !owned {
take_directory_ownership(root, expected)?;
}
for entry in std::fs::read_dir(stale).context("failed to read the stale application data root")? {
let name = entry
.context("failed to read a stale application data root entry")?
.file_name();
if REGENERATED_FILES.iter().any(|regenerated| name == *regenerated) {
continue;
}
std::fs::rename(stale.join(&name), root.join(&name))
.with_context(|| format!("failed to move {name:?} into the recreated root"))?;
}
Ok(())
}
/// Drains a failed rebuild back into the set-aside root and restores it under the real name.
///
/// Failure must be reported: data split across both names reads as a fresh install.
fn restore_root(root: &Path, stale: &Path) -> Result<()> {
for entry in std::fs::read_dir(root).context("failed to read the partially rebuilt root")? {
let name = entry
.context("failed to read a partially rebuilt root entry")?
.file_name();
std::fs::rename(root.join(&name), stale.join(&name))
.with_context(|| format!("failed to put {name:?} back"))?;
}
std::fs::remove_dir(root).context("failed to remove the partially rebuilt root")?;
std::fs::rename(stale, root).context("failed to restore the application data root")
}
fn take_directory_ownership(path: &Path, expected: PSID) -> Result<()> {
let directory = open_no_reparse(path, true, WRITE_OWNER)?;
let status = unsafe {
SetSecurityInfo(
directory.as_raw_handle(),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION,
expected,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null(),
)
};
if status != 0 {
return Err(std::io::Error::from_raw_os_error(status as i32))
.context("failed to set the application data root owner");
}
Ok(())
}
fn owner_matches(directory: &std::fs::File, expected: PSID) -> Result<bool> {
let mut owner = std::ptr::null_mut();
let mut security = std::ptr::null_mut();
let status = unsafe {
GetSecurityInfo(
directory.as_raw_handle(),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION,
&mut owner,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut security,
)
};
if status != 0 || security.is_null() {
bail!("failed to inspect the application data root owner: Windows error {status}");
}
let security = LocalSecurityDescriptor(security);
let matches = !owner.is_null() && unsafe { EqualSid(owner, expected) } != 0;
drop(security);
Ok(matches)
}
fn open_no_reparse(path: &Path, directory: bool, access: u32) -> Result<std::fs::File> {
let wide = wide_path(path)?;
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(std::io::Error::last_os_error()).with_context(|| format!("failed to open {path:?}"));
}
let file = unsafe { std::fs::File::from_raw_handle(handle) };
let mut information = BY_HANDLE_FILE_INFORMATION::default();
if unsafe { GetFileInformationByHandle(file.as_raw_handle(), &mut information) } == 0 {
return Err(std::io::Error::last_os_error()).with_context(|| format!("failed to inspect {path:?}"));
}
if information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0
|| (information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0) != directory
{
bail!("{path:?} is not the kind of object the owner repair expects");
}
Ok(file)
}
fn sid_to_string(sid: PSID) -> Result<String> {
let mut value = std::ptr::null_mut();
if unsafe { ConvertSidToStringSidW(sid, &mut value) } == 0 || value.is_null() {
+10
View File
@@ -242,6 +242,16 @@ pub fn run() -> std::process::ExitCode {
let _ = utils::dirs::init_portable_flag();
// Runs before the singleton check, which is the first thing to open a file in that directory.
#[cfg(windows)]
if let Err(error) =
utils::dirs::preinit_app_data_dir().and_then(|root| core::owner_identity::repair_app_data_root_owner(&root))
{
// The logger is installed later in setup(), so this would otherwise be lost.
eprintln!("[clash-verge] 应用数据目录所有权修复失败: {error:#}");
logging!(error, Type::Setup, "应用数据目录所有权修复失败: {error:#}");
}
match handle_singleton_startup(app_init::init_singleton_check(), utils::startup::report_error) {
StartupAction::Continue => {}
StartupAction::ExitSuccess => return std::process::ExitCode::SUCCESS,
+2 -2
View File
@@ -19,8 +19,8 @@ use tokio::sync::oneshot;
use warp::Filter as _;
const INSTANCE_TOKEN_HEADER: &str = "x-instance-token";
const INSTANCE_RECORD_FILE: &str = "singleton-instance.json";
const INSTANCE_LOCK_FILE: &str = "singleton-instance.lock";
pub(crate) const INSTANCE_RECORD_FILE: &str = "singleton-instance.json";
pub(crate) const INSTANCE_LOCK_FILE: &str = "singleton-instance.lock";
#[derive(Deserialize, Debug)]
struct QueryParam {