A unit file reinterprets an environment value; a container does not
Found by being asked whether processes and containers handle environment the
same way. They do not, and the difference is not cosmetic.
Docker passes --env through literally. A unit file reads three things out of a
value that nothing else does, and a module's environment routinely contains all
three because a generated password is arbitrary bytes:
- % begins a specifier. %H is the hostname. A password containing one is
silently replaced, and it fails later as an authentication error nobody can
explain by reading the declaration.
- whitespace separates assignments. Unquoted, K=a b sets K to "a" and reads
"b" as another assignment.
- a newline ends the line, and what follows is read as a unit DIRECTIVE.
The first two are escaped: quoted, with quotes and backslashes escaped and
percent doubled. The third cannot be — a unit's environment has no way to carry
a line break — so it is refused in validation, near whoever wrote it. Without
that, an environment value could write ExecStart= and have the machine run
something nobody declared.
Ordinary awkward values stay accepted, because refusing those too would leave a
module unable to hold a generated password.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -130,3 +130,36 @@ func TestAProcessThatStaysUpIsStillRestartedWhenItExits(t *testing.T) {
|
||||
t.Fatalf("a process that should stay up is declared a step:\n%s", unit)
|
||||
}
|
||||
}
|
||||
|
||||
// **A unit file reinterprets a value in three ways nothing else does**, and a module's environment
|
||||
// routinely contains all three — a generated password is arbitrary bytes.
|
||||
func TestAnEnvironmentValueMeansWhatTheDeclarationSaid(t *testing.T) {
|
||||
// A percent begins a specifier: %H is the hostname. A password containing one would be
|
||||
// silently replaced, failing as an authentication error nobody can explain from the
|
||||
// declaration.
|
||||
percent := aProcess()
|
||||
percent.Env = map[string]string{"PASSWORD": "a%Hb"}
|
||||
if !strings.Contains(unitFor(percent), "%%H") {
|
||||
t.Fatalf("a percent was left as a systemd specifier:\n%s", unitFor(percent))
|
||||
}
|
||||
|
||||
// Whitespace separates assignments: unquoted, K=a b sets K to "a" and reads "b" as another.
|
||||
spaced := aProcess()
|
||||
spaced.Env = map[string]string{"GREETING": "hello there"}
|
||||
if !strings.Contains(unitFor(spaced), `"GREETING=hello there"`) {
|
||||
t.Fatalf("a value with a space was not quoted:\n%s", unitFor(spaced))
|
||||
}
|
||||
|
||||
// A quote would end the quoting early, and what follows would be read as unit syntax.
|
||||
quoted := aProcess()
|
||||
quoted.Env = map[string]string{"TOKEN": `a"b`}
|
||||
line := ""
|
||||
for _, l := range strings.Split(unitFor(quoted), "\n") {
|
||||
if strings.HasPrefix(l, "Environment=") {
|
||||
line = l
|
||||
}
|
||||
}
|
||||
if !strings.Contains(line, `\"`) {
|
||||
t.Fatalf("a quote was not escaped, so the value ends early: %s", line)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user