November 1, 2022·4 min read·engineeringgosecuritywindowsprocesses

Go 1.19.3 rejects NUL bytes in child environments

A Windows process-launch defect let one environment entry become two. Go 1.19.3 rejects the embedded separator before converting environment strings into the operating system's representation.

An application can restrict which environment variable a user controls and still accidentally let that user set another variable. The failure occurs when the value can contain a separator recognized by the next representation. Go's Windows process-launch path accepted an embedded NUL byte that turned one logical environment entry into multiple entries.

The Go 1.19.3 release history dates the maintenance release to 1 November 2022. Its security announcement identifies CVE-2022-41716 in syscall and os/exec and announces Go 1.18.8 alongside it. This correction concerns the environment passed to a child process, which is a separate input from its command-line arguments.

I value Go's explicit process APIs because they make the executable and its inputs visible. That clarity depends on each supplied value preserving its boundary during conversion. Here a value that appeared to belong to one key could introduce another key when the operating system read the environment block.

One string acquired another assignment

The primary issue gives a compact example. An entry containing A=B, then a NUL byte, then C=D could set both variables. The following notation describes the bytes rather than a command to execute.

A=B\x00C=D

The intended logical entry has one key and one value. The embedded zero changes what the serialized environment communicates. A policy that approves only the key A no longer describes the full set of variables the child receives.

That difference explains the security property without requiring a shell. The issue is not command substitution or quoting in a shell language. It is the representation used to supply environment entries to the process-creation interface. Escaping command arguments does not address a separator inside a separately supplied environment value.

The advisory names Windows explicitly. It should not be expanded into a claim that the same byte causes identical behavior on every platform. The operating-system boundary and the serialization used there are the relevant parts of the mechanism.

The environment block used NUL separators

The Windows syscall implementation in Go 1.19.3 contains createEnvBlock. It constructs a sequence of environment strings separated by zero bytes and terminated with an additional zero, then converts the representation into UTF-16. An embedded zero in a supplied string would be indistinguishable from an inserted separator in that representation.

The repaired helper scans each input string for a zero byte before calculating the final block and copying the entries. Finding one returns EINVAL. The invalid value is rejected before the routine serializes it into a block whose separators carry structural meaning.

That ordering is the useful part of the fix. After conversion, it is too late to distinguish an intended separator from a byte supplied as value data. Validation must happen while the program still knows which string is supposed to be one entry.

Rejecting the input also preserves the caller's ability to diagnose its source. Silently deleting the separator would produce a different value, concealing the invalid configuration that reached the process-launch boundary.

The helper also handles an empty environment with the appropriate terminating representation. This is a reminder that an empty collection and an invalid member are distinct states. Rejecting a malformed member does not require redefining a legitimate empty environment.

My preference is for checks like this to remain close to the conversion they protect. Application validation can enforce the domain's rules, but the library constructing an operating-system structure also knows which byte values violate that structure. The latter should not rely on every caller independently knowing the serialization format.

Os/exec checks the higher-level entry list

The released os/exec source describes Cmd.Env as a list of key=value strings. A nil list inherits the current process environment. For duplicate keys, the documented behavior retains the last value. Windows also has a special rule for supplying SYSTEMROOT when appropriate.

These rules show that preparing a child environment already involves interpretation. It is not an opaque byte slice handed to the operating system. The package identifies keys and resolves duplicate entries before launching the child.

The repaired deduplication helper checks each supplied string for a NUL byte. It records an error identifying an environment variable containing NUL and excludes the malformed entry from its output. That error needs to remain part of the preparation result. Merely omitting an invalid entry and continuing silently would obscure a failure in the caller's requested configuration.

On Windows, duplicate-key handling ignores case. That is another example of a platform rule applied during preparation. A policy concerning which keys may be supplied needs to account for the same key identity the process interface uses.

The presence of ordinary duplicate handling does not justify accepting embedded separators. Duplicate entries are explicit members of the list, visible to the code constructing it. An embedded separator creates an additional entry only after representation conversion, which is precisely where the intended list boundary is lost.

Exposure depends on who supplies the value

The announcement describes malicious environment values. To understand an application, the relevant question is whether a less-trusted source can influence a value used in a Windows child environment. The application may deliberately choose the key while allowing a user to choose its contents. That arrangement is enough to make preservation of the value boundary important.

The meaning of an injected key depends on the child program. The advisory does not identify one universal effect for every additional variable. A child may interpret environment settings as paths or configuration, but the consequence must be established from that particular program's behavior.

I regard that distinction as necessary for an accurate assessment. The library defect establishes unintended control over another variable. Application analysis establishes what authority that variable carries. November's patch rejects the structural separator at the process boundary so the entry list remains the entry list the caller supplied.