nixos-rebuild-ng: fix too long TMPDIR detection (#537167)
This commit is contained in:
@@ -25,7 +25,7 @@ SSH_DEFAULT_OPTS: Final = [
|
||||
"-o",
|
||||
"ControlMaster=auto",
|
||||
"-o",
|
||||
f"ControlPath={tmpdir.TMPDIR_PATH / 'ssh-%C'}",
|
||||
f"ControlPath={tmpdir.SSH_CONTROL_PATH}",
|
||||
"-o",
|
||||
"ControlPersist=60",
|
||||
]
|
||||
|
||||
@@ -1,38 +1,86 @@
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory, gettempdir
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Final
|
||||
|
||||
logger: Final = logging.getLogger(__name__)
|
||||
|
||||
# The Linux kernel hardcodes a limit of 108 bytes for Unix sockets [0],
|
||||
# but that includes one NULL byte at the very end, so the logical max
|
||||
# length is 107 bytes.
|
||||
# [0]: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/uapi/linux/un.h?h=v6.18.37#n7
|
||||
LINUX_MAX_SOCKET_LENGTH: Final = 107
|
||||
|
||||
# Very long tmp dirs lead to "too long for Unix domain socket"
|
||||
# SSH ControlPath errors. Especially macOS sets long TMPDIR paths.
|
||||
# This is also required for Linux, if the user tries to build
|
||||
# from inside a shell using `--target-host`, which will cause
|
||||
# ssh to fail with "ControlPath too long"
|
||||
# OpenSSH expands %C to `conn_hash_hex` [0],
|
||||
# which is the result of calling `ssh_connection_hash` [1],
|
||||
# which computes a sha1 digest [2],
|
||||
# which is 20 bytes long [3].
|
||||
# which gets hex encoded [4] to double that length [5].
|
||||
#
|
||||
# The constant is based on a worst case example FQDN, e.g.:
|
||||
# `ec2-123-123-123-123.ap-southeast-2.compute.amazonaws.com` (56 bytes).
|
||||
# The `ControlPath` can maximum be 108 bytes. Given the prefix
|
||||
# that is used for the tempdir, ie. `nixos-rebuild.47i6dz8c` (22 bytes),
|
||||
# we have 30 bytes left to work with.
|
||||
# This should be fine for the usual temp folders:
|
||||
# /tmp/tmp.7hBqN2Fm5H (19)
|
||||
# /var/tmp/tmp.7hBqN2Fm5H (23)
|
||||
# /run/user/1000/tmp.7hBqN2Fm5H (29)
|
||||
# [0]: https://github.com/openssh/openssh-portable/blob/V_10_3_P1/sshconnect.h#L70
|
||||
# [1]: https://github.com/openssh/openssh-portable/blob/V_10_3_P1/ssh.c#L1464-L1465
|
||||
# [2]: https://github.com/openssh/openssh-portable/blob/V_10_3_P1/readconf.c#L345
|
||||
# [3]: https://github.com/openssh/openssh-portable/blob/V_10_3_P1/openbsd-compat/sha1.h#L15
|
||||
# [4]: https://github.com/openssh/openssh-portable/blob/V_10_3_P1/readconf.c#L360
|
||||
# [5]: https://github.com/openssh/openssh-portable/blob/V_10_3_P1/misc.c#L1627
|
||||
OPENSSH_PERCENT_C_LENGTH: Final = 40
|
||||
|
||||
# OpenSSH adds a suffix to the given control path [0].
|
||||
# There's 1 character for a `.` separator, followed by 16 random characters [1].
|
||||
# [0]: https://github.com/openssh/openssh-portable/blob/V_10_3_P1/mux.c#L1348
|
||||
# [1]: https://github.com/openssh/openssh-portable/blob/V_10_3_P1/mux.c#L1324
|
||||
OPENSSH_CONTROL_PATH_SUFFIX_LENGTH: Final = 1 + 16
|
||||
|
||||
# Carefully compute a maximum allowed length for a tmpdir, otherwise ssh crashes with errors like this:
|
||||
# > unix_listener: path "/home/runner/work/_temp/nixos-rebuild.aw8hzmq7/ssh-ea7c10de83787b1dec3f06ef20ee26b38c6bb0a5.x9MDykk4gmASsl3R"
|
||||
# > too long for Unix domain socket
|
||||
#
|
||||
# The full path to our tmpdir must be short enough for the resulting Unix domain
|
||||
# sockets that OpenSSH creates to fit within `LINUX_MAX_SOCKET_LENGTH`.
|
||||
# It's common for system configured temp dirs to be more
|
||||
# than a few characters:
|
||||
# - macOS sets long TMPDIR paths.
|
||||
# - Nix dev shells set a longer TMPDIR
|
||||
# - GitHub actions set TMPDIR to something like `/home/runner/work/_temp`.
|
||||
#
|
||||
# Breaking down the socket path into its component pieces:
|
||||
#
|
||||
# /home/runner/work/_temp/nixos-rebuild.aw8hzmq7/ssh-ea7c10de83787b1dec3f06ef20ee26b38c6bb0a5.x9MDykk4gmASsl3R
|
||||
# |-------------- TMPDIR ----------------------|^|----------------- ssh-%C -----------------||---------------|
|
||||
# | |
|
||||
# Note the path separator character. OPENSSH_CONTROL_PATH_SUFFIX_LENGTH
|
||||
#
|
||||
MAX_TMPDIR_LENGTH: Final = (
|
||||
LINUX_MAX_SOCKET_LENGTH
|
||||
- 1 # Path separator between tmpdir and the socket name.
|
||||
# Keep the following in sync with `SSH_CONTROL_PATH`.
|
||||
- len("ssh-")
|
||||
- OPENSSH_PERCENT_C_LENGTH
|
||||
- OPENSSH_CONTROL_PATH_SUFFIX_LENGTH
|
||||
)
|
||||
|
||||
|
||||
def make_tmpdir() -> TemporaryDirectory[str]:
|
||||
tmp = gettempdir()
|
||||
if len(tmp) >= 30:
|
||||
prefix = "nixos-rebuild."
|
||||
tmpdir = TemporaryDirectory(prefix=prefix)
|
||||
if len(os.fsencode(tmpdir.name)) > MAX_TMPDIR_LENGTH:
|
||||
short_tmpdir = TemporaryDirectory(prefix=prefix, dir="/tmp")
|
||||
logger.debug(
|
||||
"tempdir '%s' exceeds 30 bytes limit, defaulting to /tmp instead",
|
||||
tmp,
|
||||
"tempdir '%s' exceeds %s bytes limit, defaulting to '%s' instead",
|
||||
tmpdir,
|
||||
MAX_TMPDIR_LENGTH,
|
||||
short_tmpdir,
|
||||
)
|
||||
|
||||
return TemporaryDirectory(prefix="nixos-rebuild.", dir="/tmp")
|
||||
tmpdir.cleanup()
|
||||
return short_tmpdir
|
||||
|
||||
return TemporaryDirectory(prefix="nixos-rebuild.")
|
||||
return tmpdir
|
||||
|
||||
|
||||
TMPDIR: Final = make_tmpdir()
|
||||
TMPDIR_PATH: Final = Path(TMPDIR.name)
|
||||
|
||||
# Keep this in sync with `MAX_TMPDIR_LENGTH`!
|
||||
SSH_CONTROL_PATH: Final = str(TMPDIR_PATH / "ssh-%C")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import contextlib
|
||||
import os
|
||||
import tempfile
|
||||
import typing
|
||||
from pathlib import Path
|
||||
|
||||
from nixos_rebuild.tmpdir import MAX_TMPDIR_LENGTH, make_tmpdir
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def system_tempdir(path: Path) -> typing.Generator[None]:
|
||||
path.mkdir(exist_ok=True)
|
||||
|
||||
# `tempfile` caches the tempdir, you must clear it for it to recompute.
|
||||
tempfile.tempdir = None
|
||||
|
||||
og_tmpdir = os.environ.get("TMPDIR")
|
||||
|
||||
try:
|
||||
os.environ["TMPDIR"] = str(path)
|
||||
assert Path(tempfile.gettempdir()) == path
|
||||
yield
|
||||
finally:
|
||||
if og_tmpdir is None:
|
||||
del os.environ["TMPDIR"]
|
||||
else:
|
||||
os.environ["TMPDIR"] = og_tmpdir
|
||||
|
||||
|
||||
def test_make_tmpdir() -> None:
|
||||
# Basic test: whatever the default system temp dir happens to be.
|
||||
tmpdir = make_tmpdir()
|
||||
tmp = Path(tmpdir.name)
|
||||
assert tmp.exists()
|
||||
assert tmp.is_dir()
|
||||
assert len(os.fsencode(str(tmp))) <= MAX_TMPDIR_LENGTH
|
||||
|
||||
# Test with a short system temp dir. We should use it unmodified.
|
||||
with system_tempdir(Path("/tmp/not-too-long")):
|
||||
tmpdir = make_tmpdir()
|
||||
tmp = Path(tmpdir.name)
|
||||
|
||||
assert tmp.exists()
|
||||
assert tmp.is_dir()
|
||||
assert len(os.fsencode(str(tmp))) <= MAX_TMPDIR_LENGTH
|
||||
|
||||
# Test with a long system temp dir. We should ignore
|
||||
# it and fall back to something short enough for OpenSSH to
|
||||
# create sockets in.
|
||||
with system_tempdir(Path("/tmp/long" + ("g" * MAX_TMPDIR_LENGTH))):
|
||||
tmpdir = make_tmpdir()
|
||||
tmp = Path(tmpdir.name)
|
||||
|
||||
assert tmp.exists()
|
||||
assert tmp.is_dir()
|
||||
assert len(os.fsencode(str(tmp))) <= MAX_TMPDIR_LENGTH
|
||||
Reference in New Issue
Block a user