Repository navigation
emrg: SECURITY_DESCRIPTOR_MIN_LENGTH is the 32-bit size, so the absolute-descriptor buffer is overrun on 64-bit #2509
Description
emrg/sandbox/win32/abi.py declares SECURITY_DESCRIPTOR_MIN_LENGTH = 20, and emrg/sandbox/win32/registry.py uses it as the size of the buffer an absolute security descriptor is built in:
descriptor = ctypes.create_string_buffer(abi.SECURITY_DESCRIPTOR_MIN_LENGTH)
absolute = ctypes.cast(descriptor, ctypes.c_void_p)
api.advapi32.InitializeSecurityDescriptor(absolute, abi.SECURITY_DESCRIPTOR_REVISION)
api.advapi32.SetSecurityDescriptorDacl(absolute, True, ctypes.c_void_p(new_acl), False)Twenty bytes is the 32-bit answer. The Win32 SDK defines SECURITY_DESCRIPTOR_MIN_LENGTH as sizeof(SECURITY_DESCRIPTOR), and on a 64-bit interpreter that struct is 40 bytes, measured with ctypes on this checkout:
Revision offset 0
Sbz1 offset 1
Control offset 2
Owner offset 8
Group offset 16
Sacl offset 24
Dacl offset 32
sizeof(SECURITY_DESCRIPTOR) = 40
So InitializeSecurityDescriptor writes 40 bytes into a 20-byte allocation. create_string_buffer bounds nothing, so no exception is raised: the write goes 20 bytes past the buffer into the process heap, and the damage surfaces later, wherever the allocator hands that memory out next.
Observed symptom
Master's Windows CI leg is red with an access violation that names a test with no connection to the registry code. On run 39870597838 (fede281b), the test leg passed and test-windows failed; the crash report points at tests/test_llm.py::test_headers_carry_an_empty_key_unchanged — a test that takes no fixture — inside pytest's fixture setup (_fillfixtures → getfixturevalue → execute), i.e. the heap was already corrupt when that file started. The xdist worker's previous block was the whole tests/test_bash_v2_win32.py file (-n auto --dist loadfile gives one file per worker), and an earlier crash of the same family sits on the same file boundary.
The test that reaches the overrun is the one that applies a DACL to a real key: tests/test_bash_v2_win32.py::test_a_key_grant_is_applied_checkable_and_reversible_on_a_real_key → apply_dacl.
What the fix must satisfy
- The buffer an absolute security descriptor is built in is sized by the struct, not by a number:
SECURITY_DESCRIPTOR_MIN_LENGTHmust bectypes.sizeofof a declaredSECURITY_DESCRIPTORstruct, so it answers 20 on a 32-bit interpreter and 40 on a 64-bit one without anyone choosing. - A guard holds that property on every platform. The tests that need a real Win32 call are Windows-only, but the struct's layout is computable anywhere, so the guard must run on the Linux and macOS legs too — a guard that only runs on Windows cannot catch the next hand-written size on the two legs that are green today.
- The guard asserts the property that failed, both ways: the constant equals the struct's size, and the struct's last field fits inside it (
Dacloffset + pointer size ≤ the constant). The second holds even where the first is trivially true. - No change to the grant/revoke behaviour: the two other
create_string_buffercalls inregistry.pyare sized fromMakeSelfRelativeSD's andRegGetKeySecurity's own measurements, andacl.py's descriptors are allocated by the API. Only the hand-sized one is the defect.
Confirmation
The pass criterion is the test-windows leg of the PR's own CI run going green, with tests/test_bash_v2_win32.py and the file that crashed after it in the same worker. Neither macOS nor Linux can exercise the Win32 call, so a local green says the guard runs, never that the overrun is gone.
Activity
argszero commentedon Oct 23, 2026
Handled by #2510