Studio (Windows): read a runtime lock held by an elevated Unsloth process as busy, not a broken install (#13301)

* Studio (Windows): read a runtime lock held by an elevated Unsloth process as busy, not a broken install

* Shorten test comments
This commit is contained in:
Daniel Han 2026-10-11 05:35:23 -07:00 • committed by GitHub
parent 9c4c4a1cee
commit 44d6349ba8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 142 additions and 5 deletions

View file

@ -384,10 +384,19 @@ fn acquire_named_studio_runtime_launch_guard(
const WAIT_OBJECT_0: u32 = 0x0000_0000;
const WAIT_ABANDONED: u32 = 0x0000_0080;
const WAIT_TIMEOUT: u32 = 0x0000_0102;
// Only the rights the guard uses. CreateMutexW asks for MUTEX_ALL_ACCESS, which an
// elevated holder's mutex denies to this process: error 5 instead of busy (#9928).
const SYNCHRONIZE: u32 = 0x0010_0000;
const MUTEX_MODIFY_STATE: u32 = 0x0000_0001;
let wide_name: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
let handle = unsafe {
windows_sys::Win32::System::Threading::CreateMutexW(std::ptr::null(), 0, wide_name.as_ptr())
windows_sys::Win32::System::Threading::CreateMutexExW(
std::ptr::null(),
wide_name.as_ptr(),
0,
SYNCHRONIZE | MUTEX_MODIFY_STATE,
)
};
if handle.is_null() {
return Err(format!(
@ -822,6 +831,76 @@ mod studio_runtime_launch_guard_tests {
acquire_named_studio_runtime_launch_guard(&name).unwrap();
}
// The access an elevated holder's mutex grants a non-elevated opener (#9928).
#[test]
fn a_holder_that_denies_full_access_reads_as_busy() {
use windows_sys::Win32::Security::{
AddAccessAllowedAce, CreateWellKnownSid, InitializeAcl, InitializeSecurityDescriptor,
SetSecurityDescriptorDacl, WinWorldSid, ACL, ACL_REVISION, SECURITY_ATTRIBUTES,
SECURITY_DESCRIPTOR, SECURITY_MAX_SID_SIZE,
};
let name = format!(
"Local\\UnslothStudioRuntimeGateAccessTest-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let mut sid = [0_u8; SECURITY_MAX_SID_SIZE as usize];
let mut sid_len = SECURITY_MAX_SID_SIZE;
let mut acl = [0_u64; 16];
let mut descriptor: SECURITY_DESCRIPTOR = unsafe { std::mem::zeroed() };
let descriptor_ptr = (&mut descriptor as *mut SECURITY_DESCRIPTOR).cast();
unsafe {
assert_ne!(
CreateWellKnownSid(
WinWorldSid,
std::ptr::null_mut(),
sid.as_mut_ptr().cast(),
&mut sid_len
),
0
);
let acl_ptr = acl.as_mut_ptr().cast::<ACL>();
assert_ne!(
InitializeAcl(acl_ptr, std::mem::size_of_val(&acl) as u32, ACL_REVISION),
0
);
assert_ne!(
AddAccessAllowedAce(acl_ptr, ACL_REVISION, 0x0010_0001, sid.as_mut_ptr().cast()),
0
);
assert_ne!(InitializeSecurityDescriptor(descriptor_ptr, 1), 0);
assert_ne!(SetSecurityDescriptorDacl(descriptor_ptr, 1, acl_ptr, 0), 0);
}
let attributes = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: descriptor_ptr,
bInheritHandle: 0,
};
let wide_name: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
let holder = unsafe {
windows_sys::Win32::System::Threading::CreateMutexW(&attributes, 1, wide_name.as_ptr())
};
assert!(!holder.is_null());
let contender_name = name.clone();
let error = std::thread::spawn(move || {
acquire_named_studio_runtime_launch_guard(&contender_name)
.err()
.expect("second launcher unexpectedly acquired the gate")
})
.join()
.unwrap();
unsafe {
let _ = windows_sys::Win32::System::Threading::ReleaseMutex(holder);
let _ = windows_sys::Win32::Foundation::CloseHandle(holder);
}
assert_eq!(error, STUDIO_RUNTIME_GATE_BUSY);
}
#[test]
fn guarded_operation_is_skipped_while_busy_and_runs_after_release() {
let name = format!(

View file

@ -441,6 +441,61 @@ def test_runtime_gate_blocks_another_thread_and_recovers():
assert acquired is True
class _SecurityAttributes(ctypes.Structure):
_fields_ = [
("length", wintypes.DWORD),
("descriptor", ctypes.c_void_p),
("inherit", wintypes.BOOL),
]
@pytest.mark.skipif(os.name != "nt", reason = "Windows named mutexes are required")
def test_runtime_gate_reads_a_holder_it_cannot_fully_open_as_busy(tmp_path):
# The access an elevated holder's mutex grants a non-elevated opener (#9928).
kernel32 = ctypes.WinDLL("kernel32", use_last_error = True)
advapi32 = ctypes.WinDLL("advapi32", use_last_error = True)
kernel32.CreateMutexW.argtypes = [ctypes.c_void_p, wintypes.BOOL, wintypes.LPCWSTR]
kernel32.CreateMutexW.restype = wintypes.HANDLE
kernel32.ReleaseMutex.argtypes = [wintypes.HANDLE]
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
kernel32.LocalFree.argtypes = [ctypes.c_void_p]
kernel32.LocalFree.restype = ctypes.c_void_p
convert = advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW
convert.argtypes = [
wintypes.LPCWSTR,
wintypes.DWORD,
ctypes.POINTER(ctypes.c_void_p),
ctypes.c_void_p,
]
convert.restype = wintypes.BOOL
descriptor = ctypes.c_void_p()
assert convert("D:(A;;0x00100001;;;WD)", 1, ctypes.byref(descriptor), None)
attributes = _SecurityAttributes(ctypes.sizeof(_SecurityAttributes), descriptor, False)
name = gate.runtime_mutex_name_for_studio_home(tmp_path)
holder = kernel32.CreateMutexW(ctypes.byref(attributes), True, name)
assert holder
observed: list[str] = []
def contend() -> None:
try:
with gate.studio_runtime_launch_guard(tmp_path):
observed.append("acquired")
except gate.StudioRuntimeGateBusy:
observed.append("blocked")
except OSError as exc:
observed.append(f"winerror {exc.winerror}")
try:
contender = threading.Thread(target = contend)
contender.start()
contender.join(timeout = 10)
assert observed == ["blocked"]
finally:
assert kernel32.ReleaseMutex(holder)
assert kernel32.CloseHandle(holder)
kernel32.LocalFree(descriptor)
@pytest.mark.skipif(os.name != "nt", reason = "Windows named mutexes are required")
def test_custom_root_runtime_gate_blocks_another_thread(tmp_path):
kernel32 = ctypes.WinDLL("kernel32", use_last_error = True)

View file

@ -233,12 +233,13 @@ def studio_runtime_launch_guard(
return
kernel32 = ctypes.WinDLL("kernel32", use_last_error = True)
kernel32.CreateMutexW.argtypes = [
kernel32.CreateMutexExW.argtypes = [
ctypes.c_void_p,
wintypes.BOOL,
wintypes.LPCWSTR,
wintypes.DWORD,
wintypes.DWORD,
]
kernel32.CreateMutexW.restype = wintypes.HANDLE
kernel32.CreateMutexExW.restype = wintypes.HANDLE
kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
kernel32.WaitForSingleObject.restype = wintypes.DWORD
kernel32.ReleaseMutex.argtypes = [wintypes.HANDLE]
@ -247,7 +248,9 @@ def studio_runtime_launch_guard(
kernel32.CloseHandle.restype = wintypes.BOOL
name = runtime_mutex_name_for_studio_home(studio_home)
handle = kernel32.CreateMutexW(None, False, name)
# SYNCHRONIZE | MUTEX_MODIFY_STATE, not CreateMutexW's MUTEX_ALL_ACCESS, which an elevated
# holder's mutex denies to a non-elevated opener: error 5 instead of busy (#9928).
handle = kernel32.CreateMutexExW(None, name, 0, 0x00100001)
if not handle:
raise ctypes.WinError(ctypes.get_last_error())