You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rewrite the container's /etc/hosts proxy pin from Rust with an open-once,
no-follow file handle instead of the current grep + printf shell command
that lxc_runner sends through attach_run.
Why
The shell command is in LxcScriptRunner::build_hosts_pin_command and build_hosts_unpin_command, both of which share LxcScriptRunner::hosts_read_prologue
(src/backends/lxc/common/src/lxc_runner.rs). It reads the file into a shell
variable, then truncates the file and writes the variable back. Three known
gaps follow from that shape, and the doc comment on hosts_read_prologue
already records the last two as unclosed:
The whole file is buffered in shell state. A workload that ran earlier
in a reused container can enlarge /etc/hosts, and the command then does
unbounded command-substitution work and holds the entire result in memory
before the rewrite. Raised in review on PR [LXC] Enforce the deny-all-except-proxy network policy (model 2) #798.
A successful read still loses NUL bytes, because a shell variable
cannot hold one. A hosts file containing a NUL is rewritten truncated at
that byte and still exits 0, so nothing observes the loss.
A symlink swapped in between the -h test and the > redirect is still
followed. The -h test covers the unraced shape only. A dangling
symlink is the worst case: it fails -e as well, and a redirect onto a
dangling link creates the link's target, which on a writable host bind
mount lands outside the container.
All three come from the same constraint -- the command has to work on BusyBox,
so it is restricted to grep and printf, and neither can open a file once
and rewrite it in place.
Proposed fix
Do the rewrite from Rust against /proc/<init_pid>/root/etc/hosts, which is
the container's own mount namespace as seen from the host, so no helper binary
has to exist inside the container. Open with O_NOFOLLOW, stream the filter
rather than buffering the file, and write through the same descriptor.
Two things to get right, and neither is free:
O_NOFOLLOW only protects the final component. A hostile /etc inside the
container still redirects the open, so this wants openat2 with RESOLVE_NO_SYMLINKS, or a component-by-component openat walk, rather
than a single open on the joined path.
The current command runs inside the container's namespaces via attach_run.
Reaching the file through /proc/<pid>/root from the host instead changes
which credentials the write happens under, so the permission model needs to
be re-checked rather than assumed equivalent.
Not urgent
The pin is only written when a proxy is configured, /etc/hosts is normally
a few hundred bytes, and gaps 2 and 3 both need a workload that is already
manipulating its own /etc/hosts. This is correctness and robustness debt,
not a live break.
What
Rewrite the container's
/etc/hostsproxy pin from Rust with an open-once,no-follow file handle instead of the current
grep+printfshell commandthat
lxc_runnersends throughattach_run.Why
The shell command is in
LxcScriptRunner::build_hosts_pin_commandandbuild_hosts_unpin_command, both of which shareLxcScriptRunner::hosts_read_prologue(
src/backends/lxc/common/src/lxc_runner.rs). It reads the file into a shellvariable, then truncates the file and writes the variable back. Three known
gaps follow from that shape, and the doc comment on
hosts_read_prologuealready records the last two as unclosed:
The whole file is buffered in shell state. A workload that ran earlier
in a reused container can enlarge
/etc/hosts, and the command then doesunbounded command-substitution work and holds the entire result in memory
before the rewrite. Raised in review on PR [LXC] Enforce the deny-all-except-proxy network policy (model 2) #798.
A successful read still loses NUL bytes, because a shell variable
cannot hold one. A hosts file containing a NUL is rewritten truncated at
that byte and still exits 0, so nothing observes the loss.
A symlink swapped in between the
-htest and the>redirect is stillfollowed. The
-htest covers the unraced shape only. A danglingsymlink is the worst case: it fails
-eas well, and a redirect onto adangling link creates the link's target, which on a writable host bind
mount lands outside the container.
All three come from the same constraint -- the command has to work on BusyBox,
so it is restricted to
grepandprintf, and neither can open a file onceand rewrite it in place.
Proposed fix
Do the rewrite from Rust against
/proc/<init_pid>/root/etc/hosts, which isthe container's own mount namespace as seen from the host, so no helper binary
has to exist inside the container. Open with
O_NOFOLLOW, stream the filterrather than buffering the file, and write through the same descriptor.
Two things to get right, and neither is free:
O_NOFOLLOWonly protects the final component. A hostile/etcinside thecontainer still redirects the open, so this wants
openat2withRESOLVE_NO_SYMLINKS, or a component-by-componentopenatwalk, ratherthan a single
openon the joined path.attach_run.Reaching the file through
/proc/<pid>/rootfrom the host instead changeswhich credentials the write happens under, so the permission model needs to
be re-checked rather than assumed equivalent.
Not urgent
The pin is only written when a proxy is configured,
/etc/hostsis normallya few hundred bytes, and gaps 2 and 3 both need a workload that is already
manipulating its own
/etc/hosts. This is correctness and robustness debt,not a live break.
Raised by Gudge (@MGudgin) in review on #798:
#798 (comment)