Skip to content

Measure the socket path before tmux sees it - #730

Open
tony wants to merge 1 commit into
fix/723-repr-socket-dirfrom
fix/725-socket-path-length
Open

Measure the socket path before tmux sees it#730
tony wants to merge 1 commit into
fix/723-repr-socket-dirfrom
fix/725-socket-path-length

Conversation

@tony

@tony tony commented Jul 30, 2026

Copy link
Copy Markdown
Member

Stacked on #727 (fix/723-repr-socket-dir), which adds the socket-path resolver this needs. Review that one first; retarget this to master once it merges. The diff shown against master will include #727's commit until then.

Summary

  • Fix Server(...) accepting a socket path that cannot work. A tmux socket is a UNIX domain socket, so its path is capped by sockaddr_un — 107 bytes on Linux, 103 on macOS. Construction succeeded, and the overrun arrived at the first tmux command as a passed-through File name too long, blamed on new-session.
  • Add libtmux.exc.SocketPathTooLong, carrying the byte count, the limit, the path, and the socket name when the path was resolved rather than passed in.
  • Check three routes at construction: an explicit socket_path, the path a socket_name or socket_name_factory() resolves to, and the bare Server() default — whose whole path is inherited from $TMUX_TMPDIR.
  • Document the limit and the workaround on Server, in the pytest-plugin usage guide, and on the environment-variable page.

Why the error needed to move

The path that overruns is almost never one the caller typed. It is a deep pytest tmp_path, an XDG runtime dir, a nested worktree, or a CI checkout under a long workspace prefix. So the number the user needs — how far over, and over what — is exactly what tmux's message does not give them, and the object that is wrong is the one they still have a stack frame for.

Before / After

server = Server(socket_path="/tmp/" + "d" * 120 + "/sock")
server.new_session(session_name="demo")
# libtmux.exc.LibTmuxException: new-session: error connecting to
# /tmp/dddd…/sock (File name too long)
Server(socket_path="/tmp/" + "d" * 120 + "/sock")
# libtmux.exc.SocketPathTooLong: … 130 bytes, limit 107

The case that actually bites is the one where the caller passed something short:

os.environ["TMUX_TMPDIR"] = "/tmp/" + "d" * 120
Server(socket_name="dev")
# libtmux.exc.SocketPathTooLong: length=139 limit=107 socket_name='dev'

Design decisions

A documented per-platform constant, not a runtime probe. sun_path is a fixed-size char array whose size is part of each platform's frozen ABI, and the stdlib publishes no constant for it. So _internal/env.py spells it out — 104 on the BSD-derived kernels, 108 elsewhere, less one for the NUL — with a comment saying why. To stop that number drifting silently, the test suite probes the running kernel: socket.socket(AF_UNIX).connect() makes CPython measure sun_path and raise before any syscall, so the probe touches no filesystem and creates no socket file. Fixed number in the library, empirical check in the tests.

Bytes, not characters. os.fsencode, as the kernel counts. A non-ASCII path runs out sooner than its character count suggests.

Measured at construction, not on first use. The object is what is wrong. Deferring to first use is what produced the misattributed error this PR removes.

The name travels with the exception. socket_name is recorded when the path was resolved rather than given, so the message can say the length was inherited from $TMUX_TMPDIR rather than implying the caller chose it.

if/elif became if/else in __init__ so socket_name, socket_name_factory(), and the bare-Server() default share one check instead of three. Precedence is unchanged: socket_name still wins over socket_name_factory, and socket_path still leaves socket_name unset.

Behaviour change

A bare Server() or a Server(socket_name=...) now raises under a $TMUX_TMPDIR long enough to make the socket unusable, where it previously constructed and failed later. Paths at or under the limit are unaffected. Server.from_env() builds from a $TMUX written by a live tmux, so its path is necessarily within the limit.

Test plan

  • test_socket_path_too_long_raises — explicit long path; asserts length, limit, socket_name is None, and the message
  • test_socket_name_too_long_via_tmux_tmpdir — long $TMUX_TMPDIR with a short socket_name; the case the issue says actually bites
  • test_default_socket_too_long_via_tmux_tmpdir — bare Server() under the same
  • test_socket_path_at_limit_is_accepted — boundary, guarding against an off-by-one that would reject working sockets
  • test_socket_path_max_bytes_matches_platform — probes the running kernel so the per-platform constant cannot drift
  • Doctests on Server, exc.SocketPathTooLong, and check_socket_path_length, all collected and run
  • Full suite clean with nothing loosened — no fixture, test, or call site needed changing, including libtmux.pytest_plugin, which builds sockets under temp directories
  • uv run ruff check . / uv run ruff format . / uv run mypy clean
  • just build-docs clean, with the new {ref}, {exc} and {func} targets resolving

Note for the merger

The CHANGES heading cites the issue number; swap it for this PR's number on merge. It lands under ### What's new rather than ### Fixes because it ships a new public exception and new construction-time behaviour. It will conflict with the sibling PRs for #723, #724 and #726 — keep both sides.

why: A tmux socket is a UNIX domain socket, so its path is capped by
sockaddr_un -- 107 bytes on Linux, 103 on macOS. Server accepted any
path and the overrun arrived at the first tmux command as a passed-
through "File name too long", blamed on new-session rather than on the
socket, and without the one number the caller needed: how far over.
The length is usually inherited rather than typed -- a deep pytest
tmp_path, an XDG runtime dir, a nested worktree, a long $TMUX_TMPDIR --
so the path that failed is one nobody wrote. Closes #725.

what:
- Add libtmux.exc.SocketPathTooLong, carrying the byte count, the
  limit, the path, and the socket name when the path was resolved
  rather than passed in
- Add _internal.env.SOCKET_PATH_MAX_BYTES (sun_path size per platform,
  less the NUL) and check_socket_path_length(), which measures with
  os.fsencode as the kernel does
- Measure in Server.__init__: a given socket_path, and the path a
  socket_name, a socket_name_factory, or a bare Server() resolves to
  under $TMUX_TMPDIR
- Document the limit and the shorter-socket-dir workaround on Server,
  in the pytest plugin usage guide, and on the env var page
- Test both routes, the boundary, and probe the running kernel to keep
  the per-platform size honest
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.72727% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.35%. Comparing base (8f437fa) to head (f3771f7).

Files with missing lines Patch % Lines
src/libtmux/_internal/env.py 33.33% 4 Missing ⚠️
src/libtmux/exc.py 80.00% 2 Missing ⚠️
Additional details and impacted files
@@                     Coverage Diff                     @@
##           fix/723-repr-socket-dir     #730      +/-   ##
===========================================================
+ Coverage                    52.28%   52.35%   +0.06%     
===========================================================
  Files                           26       26              
  Lines                         3737     3755      +18     
  Branches                       747      749       +2     
===========================================================
+ Hits                          1954     1966      +12     
- Misses                        1479     1485       +6     
  Partials                       304      304              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant