From b180beb9969ef751ac2773fcb0b5d0a9ce8f0811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Tosser?= Date: Fri, 31 Jul 2026 22:04:36 +0200 Subject: [PATCH 1/2] fix: handle long inline keys in server.user_authorized_keys Commit 4a7440e replaced os.path.exists with Path.exists in read_any_pub_key_file. On Python < 3.14, pathlib.Path.exists re-raises OSError (ENAMETOOLONG) for paths exceeding NAME_MAX, so passing a long inline public key (eg a full RSA key) crashed the operation instead of being used as a key. os.path.exists swallows OSError and returns False, which is the correct semantic here ("is this string a file?"). Fixes pyinfra-dev/pyinfra#1907 --- src/pyinfra/operations/server.py | 5 ++-- .../long_inline_key_not_a_file.yaml | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 tests/operations/server.user_authorized_keys/long_inline_key_not_a_file.yaml diff --git a/src/pyinfra/operations/server.py b/src/pyinfra/operations/server.py index e832e6b96..974b9b51f 100644 --- a/src/pyinfra/operations/server.py +++ b/src/pyinfra/operations/server.py @@ -8,7 +8,6 @@ import os from io import StringIO from itertools import filterfalse, tee -from pathlib import Path from time import sleep from typing import TYPE_CHECKING @@ -941,7 +940,9 @@ def read_any_pub_key_file(key): if state.cwd: try_path = os.path.join(state.cwd, key) - if Path(try_path).exists(): + # NOTE: os.path.exists swallows OSError (eg ENAMETOOLONG from long inline + # keys), whereas Path.exists re-raises it on Python < 3.14. + if os.path.exists(try_path): with open(try_path) as f: return [key.strip() for key in f.readlines()] diff --git a/tests/operations/server.user_authorized_keys/long_inline_key_not_a_file.yaml b/tests/operations/server.user_authorized_keys/long_inline_key_not_a_file.yaml new file mode 100644 index 000000000..d40c8083f --- /dev/null +++ b/tests/operations/server.user_authorized_keys/long_inline_key_not_a_file.yaml @@ -0,0 +1,27 @@ +# Long inline public keys must not be treated as files: on Python < 3.14 +# Path.exists() raises OSError (ENAMETOOLONG) for such values, see +# https://github.com/pyinfra-dev/pyinfra/issues/1907 +args: + - someuser +kwargs: + public_keys: + - ssh-rsa AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA bob +facts: + server.Home: + user=someuser: /home/someuser + files.Directory: + path=/home/someuser/.ssh: + user: someuser + group: someuser + mode: 700 + files.File: + path=/home/someuser/.ssh/authorized_keys: + user: someuser + group: someuser + mode: 600 + server.AuthorizedKeys: + path=/home/someuser/.ssh/authorized_keys, user=someuser: [] + files.FindInFile: + extended_regex=False, interpolate_variables=False, path=/home/someuser/.ssh/authorized_keys, pattern=^.*ssh-rsa AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA bob.*$: [] +commands: + - "( [ $(tail -c1 /home/someuser/.ssh/authorized_keys | wc -l) -eq 0 ] && echo ; echo 'ssh-rsa AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA bob' ) >> /home/someuser/.ssh/authorized_keys" From 264f8bc94befc33a237f129bde1de50f7c085dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Tosser?= Date: Mon, 14 Sep 2026 14:36:18 +0200 Subject: [PATCH 2/2] test: raise ENAMETOOLONG in mock fs for over-long path components --- tests/conftest.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 7456b1c2d..808fb2e1c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +import errno +import os from pathlib import Path from unittest.mock import patch @@ -6,16 +8,34 @@ from pyinfra_testing.util import patch_files as _patch_files +def _check_name_too_long(filename): + # Real filesystems reject path components over NAME_MAX (255 bytes) with + # ENAMETOOLONG, eg when an inline SSH key is tried as a file path. + for component in str(filename).split(os.sep): + if len(os.fsencode(component)) > 255: + raise OSError(errno.ENAMETOOLONG, os.strerror(errno.ENAMETOOLONG), str(filename)) + + class patch_files(_patch_files): """ Extend ``pyinfra_testing.util.patch_files`` to also patch ``pathlib.Path`` methods, now that pyinfra itself uses pathlib for local filesystem access. """ + def exists(self, filename, *args): + # os.path.exists swallows OSError (eg ENAMETOOLONG) and returns False. + try: + _check_name_too_long(filename) + except OSError: + return False + return super().exists(filename, *args) + def __enter__(self): patch_self = self def _path_exists(p): + # Path.exists re-raises ENAMETOOLONG on Python < 3.14. + _check_name_too_long(p) return patch_self.exists(str(p)) def _path_is_file(p):