April 5, 2019·4 min read·engineeringgowindowssecurityruntime

Go 1.12.2 restricts Windows system DLL loading

The runtime now loads system DLLs through a restricted search or an absolute system-directory path. The change closes a directory-injection risk and preserves the restriction on older Windows systems.

A system DLL's filename does not by itself identify which file a loader will open. The directory search is part of the security decision. Go 1.12.2 changes Windows runtime loading so that libraries expected to come from the operating system cannot come from an unintended directory through the affected paths.

The release history records the release on April 5. The comparison with Go 1.12.1 contains the implementation and identifies the DLL directory injection fix as CVE-2019-9634. Its most consequential detail is the fallback behavior on systems that cannot use the preferred restricted loading operation.

I value Go's common runtime because application authors can rely on a shared implementation of platform behavior. That also makes the runtime's assumptions part of an application's security model. A program does not have to implement a plugin loader of its own to depend on DLL-loading decisions.

Restricting where a name can resolve

The runtime patch introduces windowsLoadSystemLib. When the required facilities are available, this helper invokes LoadLibraryExA with _LOAD_LIBRARY_SEARCH_SYSTEM32. The name of the requested DLL is then interpreted under an explicit restriction to the system directory.

On systems without that facility, the helper obtains the system directory using GetSystemDirectoryA. It appends the DLL name to that directory and passes the resulting absolute path to LoadLibraryA. This fallback preserves the intended source of the library even though the older loading function lacks the preferred search option.

The distinction between those paths explains why simply replacing every call with LoadLibraryEx is not a complete description of the fix. Runtime support differs across Windows systems. An implementation that works only on systems with the newer facility would have a compatibility problem, while falling back to an unrestricted filename would reintroduce the security problem.

The helper also checks the result of obtaining the system directory. If it cannot determine a usable directory, it terminates through the runtime's throw path. Continuing with an unresolved directory would lose the property the helper exists to establish. The source makes that failure explicit instead of quietly substituting an ordinary filename search.

Older systems retain the security restriction

Another part of the patch changes the runtime entry point backing syscall.loadsystemlibrary. Its signature now receives both a filename and an absolute filepath. Where restricted loading is available, it uses the filename with the system-directory flag. Otherwise, it passes the absolute filepath to the ordinary loader.

The removed fallback is revealing. Previously, when the necessary Windows update was unavailable, the code loaded the DLL by filename in the normal way. Its comment acknowledged a potential security risk. The new implementation removes that behavior and uses the supplied system path instead.

Jason Donenfeld's March tracking issue describes the intended rule. Prefer LoadLibraryEx, and when that is impossible, use LoadLibrary with a properly computed absolute path or the exact name kernel32.dll. The release implements this policy across runtime paths that earlier work had left behind.

The explicit exception for kernel32.dll matters when describing the patch. The commit states that this library remains loosely loaded while the others receive strict loading. Saying that every DLL request now uses an absolute path would therefore misrepresent both the implementation and the stated goal.

I find the handling of old systems especially useful here. A compatibility path deserves the same security invariant as the preferred path, even when it needs a different mechanism. The fallback in this patch computes the location that the restricted loader would otherwise enforce.

The runtime changes its own imports

The patch changes more than individual calls that already loaded libraries explicitly. It also moves certain runtime dependencies out of implicit dynamic imports and loads them through the new helper. That includes the timer functions in winmm.dll and an overlapped-operation function in ws2_32.dll.

After loading each library, the runtime locates the required functions and checks that they exist. The changes to these imports show why reviewing application-level loader calls alone would miss part of the issue. A runtime's startup and operating-system integration can depend on libraries before ordinary application code has a chance to establish its own policy.

The new helper also replaces existing loads of advapi32.dll and ntdll.dll. These calls previously supplied a bare filename to LoadLibraryA. Routing them through one helper gives the affected sites the same system-directory behavior, including the older-system fallback.

This is a code-loading issue, so the relevant trust question is which file the process accepts as a library. The patch does not claim to verify every DLL's contents or replace operating-system protections. It narrows the location used for libraries that the runtime expects to obtain from the system.

Other fixes expose different runtime assumptions

Go 1.12.2 also corrects the WebAssembly write barrier. The accompanying commit explains that the old implementation recorded the object containing a pointer slot instead of recording the old pointer value being overwritten. During garbage collection, that could leave a still-reachable object unmarked.

The commit gives a concrete sequence. A goroutine's stack is scanned, then the goroutine copies a heap pointer onto its stack and overwrites the heap slot. The deletion barrier must preserve information about the old pointer so that the collector does not lose track of the object. Recording the containing object after the overwrite fails to preserve that information.

This fix has a different scope from the Windows security change. It affects the runtime's accounting of reachable memory on WebAssembly. The release comparison also contains a reverse-proxy correction that starts the flush timer when the latency writer is set up, allowing headers to flush before a first body write. These are documented implementation changes rather than additions to the language.

For Windows programs, the DLL fix requires an executable containing the corrected runtime. Updating the compiler does not rewrite an already distributed executable. The practical consequence is a rebuild and deployment using Go 1.12.2, with the runtime now maintaining its system-directory restriction through both the preferred loader and its fallback.