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:
@@ -116,3 +116,45 @@ func TestEachModeOnItsOwnIsAccepted(t *testing.T) {
|
||||
t.Fatalf("a scheduled process was refused: %v", problems)
|
||||
}
|
||||
}
|
||||
|
||||
// **The one that cannot be escaped, only refused.**
|
||||
//
|
||||
// Everything else a unit file reinterprets can be escaped: a percent specifier doubled, whitespace
|
||||
// quoted, a quote backslashed. A newline cannot — it ends the line, and what follows is read as a
|
||||
// unit DIRECTIVE. A value carrying one could write ExecStart= and have the machine run something
|
||||
// nobody declared.
|
||||
//
|
||||
// So it is refused here, near whoever wrote it, rather than rendered into a unit at the far end of
|
||||
// a declaration.
|
||||
func TestAnEnvironmentValueCannotCarryALineBreak(t *testing.T) {
|
||||
for _, bad := range []string{
|
||||
"safe\nExecStart=/usr/bin/whatever",
|
||||
"carriage\rreturn",
|
||||
} {
|
||||
p := aProcess()
|
||||
p.Env = map[string]string{"X": bad}
|
||||
problems := p.validate("a process", false)
|
||||
if len(problems) == 0 {
|
||||
t.Fatalf("a value containing %q was accepted", bad)
|
||||
}
|
||||
var said bool
|
||||
for _, problem := range problems {
|
||||
if strings.Contains(problem, "line break") {
|
||||
said = true
|
||||
}
|
||||
}
|
||||
if !said {
|
||||
t.Fatalf("refused for some other reason, which would stop being true: %v", problems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And ordinary awkward values are accepted, because escaping is what handles those — refusing them
|
||||
// too would make a module unable to hold a generated password.
|
||||
func TestOrdinaryAwkwardValuesAreAccepted(t *testing.T) {
|
||||
p := aProcess()
|
||||
p.Env = map[string]string{"PASSWORD": `a%H b"c\d`}
|
||||
if problems := p.validate("a process", false); len(problems) != 0 {
|
||||
t.Fatalf("a password containing the characters passwords contain was refused: %v", problems)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user