Skip to content

std.child_process: opt-in to explicit annotate of all to be inherited handles to prevent leaks on Windows #14251

Open
@matu3ba

Description

@matu3ba

std Proposal and Context

This blog post explains how to do it https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873
The basic principle is to provide the CreateProcess method with a list of all handles: They must be regular ones and they must be set to inheritable.

In comparison Unixes like Linux leak the inheritable descriptors (those not marked with CLOEXEC), if another thread spawns processes at the same time. Another problem exists, if one does use CLOEXEC and one/external_code does not use exec*.

This would improve perf of multi-threaded code depending on no leaks, since there is no explicit mutex necessary.
Rust does not use this and uses a full process mutex rust-lang/rust#38227 to this day and has several other open handle-related problems related to it: rust-lang/rust#54760 rust-lang/rust#70719.

Note that trying out multiple executions along PATH and PATHEXT can increase the potential leaking time on Windows.

I started this, but got stuck on UpdateProcThreadAttribute failing with a bogous error message in https://github.com/matu3ba/zig/tree/extra_streams4.

The idea is to keep a buffer of 3 handles for stdin, stdout and stderr, but otherwise let the user provide one. The main caveats are that Windows may change the underlying attribute structure, as it requires dynamic probing of the attribute list size and usage of the setup function:

 LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL;
 fSuccess = cHandlesToInherit < 0xFFFFFFFF / sizeof(HANDLE) &&
            lpStartupInfo->cb == sizeof(*lpStartupInfo);
 if (!fSuccess) {
  SetLastError(ERROR_INVALID_PARAMETER);
 }
 if (fSuccess) {
  fSuccess = InitializeProcThreadAttributeList(NULL, 1, 0, &size) ||
             GetLastError() == ERROR_INSUFFICIENT_BUFFER;
 }
 if (fSuccess) {
  lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>
                                (HeapAlloc(GetProcessHeap(), 0, size));
  fSuccess = lpAttributeList != NULL;
 }
 if (fSuccess) {
  fSuccess = InitializeProcThreadAttributeList(lpAttributeList,
                    1, 0, &size);
 }
 if (fSuccess) {
  fInitialized = TRUE;
  fSuccess = UpdateProcThreadAttribute(lpAttributeList,
                    0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
                    rgHandlesToInherit,
                    cHandlesToInherit * sizeof(HANDLE), NULL, NULL);
 }

Expected Behavior

Default to no possible leaks on Windows, even if the user forgets to disable inheritance on handles.
UPDATE: Due to backwards incompatibility, since a user can not "overwrite enabling file handle inheritance as list for only this one to be spawned process" an opt-in sounds better.

Possible drawback: External code may use the non-explicit code, which can leak the handles if they are set to inheritable.
Related footgun: Handles must be set to inheritable, even for the explicit list method, to be inherited.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugObserved behavior contradicts documented or intended behavioros-windowsproposalThis issue suggests modifications. If it also has the "accepted" label then it is planned.standard libraryThis issue involves writing Zig code for the standard library.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions