diff --git a/bash/patchman-client b/bash/patchman-client index d6686d3..7b07bba 100755 --- a/bash/patchman-client +++ b/bash/patchman-client @@ -1100,8 +1100,7 @@ if ! check_command_exists awk || \ exit 1 fi -os_lock_dir=/var/lock -lock_dir=$(dirname "$(readlink -f ${os_lock_dir})")/patchman +lock_dir=/run/patchman mkdir -p "${lock_dir}" if [ ! -d "${lock_dir}" ] ; then echo "Lock directory does not exist, exiting: ${lock_dir}" diff --git a/contrib/systemd/patchman-client.service b/contrib/systemd/patchman-client.service index 5da7ea2..3f57147 100644 --- a/contrib/systemd/patchman-client.service +++ b/contrib/systemd/patchman-client.service @@ -18,7 +18,7 @@ NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes -ReadOnlyPaths=/ +RuntimeDirectory=patchman [Install] WantedBy=multi-user.target diff --git a/go/cmd/submit.go b/go/cmd/submit.go index 95d1a10..9067e3f 100644 --- a/go/cmd/submit.go +++ b/go/cmd/submit.go @@ -86,7 +86,7 @@ The server URL and API key can be provided via: } // Acquire exclusive lock to prevent concurrent runs - lockDir := "/run/lock/patchman" + lockDir := "/run/patchman" os.MkdirAll(lockDir, 0o755) lockPath := filepath.Join(lockDir, "patchman.lock") lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_WRONLY, 0o644) diff --git a/python/patchman_client/client.py b/python/patchman_client/client.py index 99682fd..cf3911a 100755 --- a/python/patchman_client/client.py +++ b/python/patchman_client/client.py @@ -42,7 +42,7 @@ __version__ = '2.0.0' DEFAULT_CONFIG = '/etc/patchman/patchman-client.conf' -LOCK_DIR = '/run/lock/patchman' +LOCK_DIR = '/run/patchman' LOCK_FILE = os.path.join(LOCK_DIR, 'patchman.lock') diff --git a/tests/test_systemd_lock_consistency.py b/tests/test_systemd_lock_consistency.py new file mode 100644 index 0000000..d6010bd --- /dev/null +++ b/tests/test_systemd_lock_consistency.py @@ -0,0 +1,62 @@ +"""Guard against #866: hardened unit must carve out a writable lock dir and +all clients must agree on it.""" +import os +import re + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SERVICE = os.path.join(REPO, 'contrib', 'systemd', 'patchman-client.service') +BASH = os.path.join(REPO, 'bash', 'patchman-client') +PY = os.path.join(REPO, 'python', 'patchman_client', 'client.py') +GO = os.path.join(REPO, 'go', 'cmd', 'submit.go') + + +def _read(path): + with open(path, encoding='utf-8') as handle: + return handle.read() + + +def _service_values(key): + values = [] + for line in _read(SERVICE).splitlines(): + line = line.strip() + if line.startswith('#') or '=' not in line: + continue + name, _, value = line.partition('=') + if name.strip() == key: + values.append(value.strip()) + return values + + +def _runtime_lock_dir(): + runtime = _service_values('RuntimeDirectory') + assert runtime, 'patchman-client.service must set RuntimeDirectory' + return '/run/' + runtime[0] + + +def test_hardened_unit_carves_out_a_writable_runtime_dir(): + # if the unit locks the filesystem down, it must provide a writable /run dir + protect = _service_values('ProtectSystem') + if protect and protect[0] == 'strict': + assert _service_values('RuntimeDirectory'), \ + 'ProtectSystem=strict needs RuntimeDirectory= or /run is read-only (#866)' + # a blanket ReadOnlyPaths=/ re-mounts the runtime dir read-only; forbid it + assert '/' not in _service_values('ReadOnlyPaths'), \ + 'ReadOnlyPaths=/ makes the lock dir read-only even with RuntimeDirectory (#866)' + + +def test_bash_client_lock_dir_matches_runtime_dir(): + match = re.search(r'^lock_dir=(\S+)', _read(BASH), re.MULTILINE) + assert match, 'could not find lock_dir in bash client' + assert match.group(1) == _runtime_lock_dir() + + +def test_python_client_lock_dir_matches_runtime_dir(): + match = re.search(r"^LOCK_DIR\s*=\s*'([^']+)'", _read(PY), re.MULTILINE) + assert match, 'could not find LOCK_DIR in python client' + assert match.group(1) == _runtime_lock_dir() + + +def test_go_client_lock_dir_matches_runtime_dir(): + match = re.search(r'lockDir\s*:=\s*"([^"]+)"', _read(GO)) + assert match, 'could not find lockDir in go client' + assert match.group(1) == _runtime_lock_dir()