Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/_identity.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

155 statements  

1from __future__ import annotations 

2 

3__lazy_modules__ = ["hashlib", "pathlib", "socket", "typing"] 

4 

5import hashlib 

6import os 

7import socket 

8import sys 

9from errno import EPERM, ESRCH 

10from pathlib import Path 

11from typing import Final 

12 

13#: Every marker format in this package caps the hostname at the RFC 1123 limit and reads a longer one as malformed. 

14_HOST_NAME_LIMIT: Final[int] = 253 

15#: The bytes those formats carry verbatim: printable non-space ASCII, less the ``?`` reserved for the escape. 

16_HOST_NAME_VERBATIM: Final[frozenset[int]] = frozenset(range(0x21, 0x7F)) - {ord("?")} 

17#: The characters at the end of an over-long name that carry a digest of the part cut off. 

18_HOST_NAME_DIGEST_LEN: Final[int] = 8 

19#: The kernel ABI fixes the initial PID namespace's inode (``PID_NS_INIT_INO`` in ``linux/nsfs.h``); every namespace 

20#: created later draws one from ``0xF0000000`` up, so this value names the host's own namespace. 

21_INITIAL_PID_NAMESPACE: Final[int] = 0xEFFFFFFC 

22_NAMESPACE_MARK: Final[str] = "?pidns-" 

23 

24 

25def host_name() -> str: 

26 """ 

27 The hostname recorded alongside an owner, so a marker written on another machine is never probed here. 

28 

29 Marker formats here hold the field to printable non-space ASCII, while ``socket.gethostname()`` reports whatever 

30 the kernel stores: a space, a newline that forges an extra marker line, or a byte no codec encodes, which Python 

31 hands back as a surrogate. A holder that publishes one raw writes a marker it cannot read back, and loses the lock 

32 to the first peer that ages it out as malformed. ``?`` is illegal in a hostname, so escaping it along with every 

33 out-of-grammar byte as ``?<hex>`` leaves a real name untouched and still keeps two hosts apart, which 

34 :func:`owner_is_stale` relies on to refuse to probe a foreign PID. 

35 

36 Linux appends the PID namespace, as Mercurial's lock prefix does. Two containers in one pod share the hostname and 

37 the boot id but not the PID namespace, so a PID recorded in one names another process, or none, in the other. The 

38 suffix starts with a ``?`` no escape can produce, so ``a`` in a namespace never reads as a host literally named 

39 ``a?pidns-...``. The initial namespace adds nothing, which keeps a host's name what older releases recorded. 

40 

41 An over-long name ends in a digest of the part cut to fit the limit. A bare cut gives every name that shares the 

42 kept prefix one identity, and :func:`owner_is_stale` would then probe a local PID for another host's holder. 

43 """ 

44 name = ( 

45 "".join( 

46 chr(byte) if byte in _HOST_NAME_VERBATIM else f"?{byte:02x}" 

47 for byte in socket.gethostname().encode("utf-8", "surrogateescape") 

48 ) 

49 + _pid_namespace_suffix() 

50 ) 

51 if len(name) > _HOST_NAME_LIMIT: 

52 keep = _HOST_NAME_LIMIT - _HOST_NAME_DIGEST_LEN - 1 

53 name = f"{name[:keep]}-{hashlib.sha256(name[keep:].encode()).hexdigest()[:_HOST_NAME_DIGEST_LEN]}" 

54 # An escape is three characters and a kept byte is never '?', so a bare '?' can only mean an empty hostname. 

55 return name or "?" 

56 

57 

58def _pid_namespace_suffix() -> str: 

59 try: 

60 namespace: Final = Path("/proc/self/ns/pid").stat().st_ino 

61 except OSError: # only Linux 3.8+ has the file, and a sandbox may hide it; nothing else tells namespaces apart 

62 return "" 

63 return "" if namespace == _INITIAL_PID_NAMESPACE else f"{_NAMESPACE_MARK}{namespace:x}" 

64 

65 

66def owner_is_stale(pid: int, hostname: str, start_token: int | None) -> bool: 

67 """ 

68 Whether the recorded owner is provably gone, so reclaiming its marker cannot detach a live holder. 

69 

70 Fail closed: return ``True`` only when this process can prove the exact recorded owner is dead. A marker from 

71 another host cannot be probed; a live PID whose start token still matches, or whose token cannot be read, is the 

72 holder or is indistinguishable from it; a live PID whose start token differs is a recycled PID, so the process that 

73 wrote the marker is gone. PostgreSQL, Qt ``QLockFile`` and Mercurial all break a stale lock only on proof of death 

74 and treat an unreadable or foreign owner as still holding. 

75 """ 

76 if hostname != host_name(): 

77 return False 

78 if process_alive(pid) and ( 

79 start_token is None or (current := process_start_token(pid)) is None or _same_start(current, start_token) 

80 ): 

81 return False 

82 # Only a bare hostname can be an older release's, written from a container that shares this host's name. 

83 return _NAMESPACE_MARK in hostname or not _owner_runs_elsewhere(pid, start_token) 

84 

85 

86def owner_is_current_process(pid: int, hostname: str, start_token: int | None) -> bool: 

87 """ 

88 Whether the recorded owner is this process, so a marker a recycled PID left behind never reads as ours. 

89 

90 Fail closed: a recorded start token must match this process's. One that differs was written by an earlier process 

91 that reused this PID, and when this process cannot read its own token nothing proves the marker is ours. A marker 

92 without a token falls back to the PID and hostname. 

93 """ 

94 if pid != os.getpid() or hostname != host_name(): 

95 return False 

96 return start_token is None or start_token == process_start_token(pid) 

97 

98 

99def _same_start(current: int, recorded: int) -> bool: 

100 # A Linux token carries the boot id in its high 64 bits, or 0 there when the process could not read the boot id. 

101 # systemd's ProcSubset=pid hides /proc/sys from one unit, so one process may lack the boot id while its peer has 

102 # it. A missing boot id then proves nothing, and only starttime is compared. Other platforms' tokens fit in the 

103 # low bits. 

104 return current == recorded or (0 in {current >> 64, recorded >> 64} and current % 2**64 == recorded % 2**64) 

105 

106 

107if sys.platform == "win32": # pragma: win32 cover 

108 import ctypes 

109 from ctypes import wintypes 

110 

111 _KERNEL32: Final[ctypes.WinDLL] = ctypes.WinDLL("kernel32", use_last_error=True) 

112 _KERNEL32.CloseHandle.argtypes = [wintypes.HANDLE] 

113 _KERNEL32.CloseHandle.restype = wintypes.BOOL 

114 _KERNEL32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] 

115 _KERNEL32.OpenProcess.restype = wintypes.HANDLE 

116 _KERNEL32.GetProcessTimes.argtypes = [ 

117 wintypes.HANDLE, 

118 ctypes.POINTER(wintypes.FILETIME), 

119 ctypes.POINTER(wintypes.FILETIME), 

120 ctypes.POINTER(wintypes.FILETIME), 

121 ctypes.POINTER(wintypes.FILETIME), 

122 ] 

123 _KERNEL32.GetProcessTimes.restype = wintypes.BOOL 

124 _KERNEL32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD] 

125 _KERNEL32.WaitForSingleObject.restype = wintypes.DWORD 

126 

127 _WIN_SYNCHRONIZE: Final[int] = 0x100000 

128 _WIN_PROCESS_QUERY_LIMITED_INFORMATION: Final[int] = 0x1000 

129 _WIN_ERROR_INVALID_PARAMETER: Final[int] = 87 

130 _WIN_INHERIT_HANDLE: Final[bool] = False 

131 _WIN_WAIT_OBJECT_0: Final[int] = 0 

132 

133 def process_alive(pid: int) -> bool: 

134 """Whether a process with this PID still runs, treating an access denial as proof it does.""" 

135 handle = _KERNEL32.OpenProcess(_WIN_SYNCHRONIZE, _WIN_INHERIT_HANDLE, pid) 

136 if handle: 

137 try: 

138 # Any open handle keeps an exited process openable, but its process object is signaled once it exits. 

139 return _KERNEL32.WaitForSingleObject(handle, 0) != _WIN_WAIT_OBJECT_0 

140 finally: 

141 _KERNEL32.CloseHandle(handle) 

142 return ctypes.get_last_error() != _WIN_ERROR_INVALID_PARAMETER 

143 

144 def process_start_token(pid: int) -> int | None: 

145 """The process creation FILETIME as a 100ns tick count, or ``None`` when it cannot be read.""" 

146 handle = _KERNEL32.OpenProcess(_WIN_PROCESS_QUERY_LIMITED_INFORMATION, _WIN_INHERIT_HANDLE, pid) 

147 if not handle: 

148 return None 

149 creation, exit_time, kernel_time, user_time = (wintypes.FILETIME() for _ in range(4)) 

150 try: 

151 if not _KERNEL32.GetProcessTimes( 

152 handle, 

153 ctypes.byref(creation), 

154 ctypes.byref(exit_time), 

155 ctypes.byref(kernel_time), 

156 ctypes.byref(user_time), 

157 ): 

158 return None # pragma: no cover # win32 GetProcessTimes failure path; not reproducible on a live handle 

159 finally: 

160 _KERNEL32.CloseHandle(handle) 

161 return (creation.dwHighDateTime << 32) | creation.dwLowDateTime 

162 

163else: # pragma: win32 no cover 

164 

165 def process_alive(pid: int) -> bool: 

166 """Whether a process with this PID exists, treating an access denial (``EPERM``) as proof it does.""" 

167 try: 

168 os.kill(pid, 0) 

169 except OSError as error: 

170 if error.errno == ESRCH: 

171 return False 

172 if error.errno == EPERM: 

173 return True 

174 raise 

175 return True 

176 

177 if sys.platform in {"linux", "android"}: # pragma: linux cover 

178 # Termux/Android reports sys.platform == "android" but runs the Linux kernel, so /proc/<pid>/stat and the boot 

179 # id are the same reliable start-time source; treat it exactly like Linux rather than the tokenless fallback. 

180 # comm (field 2) is wrapped in parentheses and may itself contain spaces or a ')', so the fixed fields start 

181 # after the final ')'. starttime is field 22 overall, the twentieth of those trailing fields (index 19). 

182 _STARTTIME_INDEX: Final[int] = 19 

183 

184 def _read_boot_id() -> int: 

185 # starttime is measured in clock ticks since boot, so on its own it repeats across a reboot. Folding the 

186 # boot id into the high bits makes the Linux token reboot-safe like the absolute clocks macOS and Windows 

187 # expose, while staying a single integer so a 3.29 reader still parses the third marker line. 0 when this 

188 # process cannot read the boot id leaves the bare starttime, which _same_start compares alone. 

189 try: 

190 boot_id = Path("/proc/sys/kernel/random/boot_id").read_text(encoding="ascii") 

191 return int(boot_id.strip().replace("-", ""), 16) 

192 except (OSError, ValueError): 

193 return 0 

194 

195 _BOOT_ID: Final[int] = _read_boot_id() 

196 #: ``TIME_NS_INIT_INO`` in ``linux/proc_ns.h``, fixed by the kernel ABI like the initial PID namespace's inode. 

197 _INITIAL_TIME_NAMESPACE: Final[int] = 0xEFFFFFFA 

198 

199 def process_start_token(pid: int) -> int | None: 

200 """ 

201 The ``/proc/<pid>/stat`` ``starttime`` folded with the boot id. 

202 

203 ``None`` when the process is gone, or when this process runs in a time namespace with a boottime offset. 

204 Since Linux 5.6 the kernel shifts ``starttime`` by the reader's offset, so a value read there differs 

205 from what a peer outside reads. 

206 """ 

207 return _read_start_token(pid) if _starttime_unshifted() else None 

208 

209 def _starttime_unshifted() -> bool: 

210 # setns can move this process into another time namespace, so ask on every read. 

211 try: 

212 namespace: Final = Path("/proc/self/ns/time").stat().st_ino 

213 except FileNotFoundError: # no entry means no time namespaces on this kernel 

214 return True 

215 except OSError: # any other failure leaves the namespace unknown, so starttime is not comparable 

216 return False 

217 if namespace == _INITIAL_TIME_NAMESPACE: 

218 return True 

219 # A container runtime may give each container a time namespace without offsets. timens_offsets describes 

220 # the namespace this process's children get, its own unless it unshared one since. 

221 try: 

222 if Path("/proc/self/ns/time_for_children").stat().st_ino != namespace: 

223 return False 

224 offsets: Final = Path("/proc/self/timens_offsets").read_text(encoding="ascii") 

225 except OSError: 

226 return False 

227 # The clock goes by name since Linux 5.8 and by clock id before. 

228 return any(line.split() in (["boottime", "0", "0"], ["7", "0", "0"]) for line in offsets.splitlines()) 

229 

230 def _read_start_token(pid: int) -> int | None: 

231 try: 

232 data = Path(f"/proc/{pid}/stat").read_bytes() 

233 except OSError: 

234 return None 

235 # psutil identifies a process by the same (pid, starttime) pair; the boot id extends that across reboots. 

236 fields = data[data.rfind(b")") + 1 :].split() 

237 if len(fields) <= _STARTTIME_INDEX: # pragma: no cover # a truncated /proc read, never seen in practice 

238 return None 

239 try: 

240 starttime = int(fields[_STARTTIME_INDEX]) 

241 except ValueError: # pragma: no cover # /proc always renders starttime as an integer 

242 return None 

243 return (_BOOT_ID << 64) | starttime 

244 

245 def _owner_runs_elsewhere(pid: int, start_token: int | None) -> bool: 

246 """ 

247 Whether the owner runs in another PID namespace as *pid*. 

248 

249 An older release in a container sharing this hostname recorded its in-container PID, which means nothing 

250 here. Readers without a boottime offset read the same starttime and boot id. A task with the recorded 

251 start token whose innermost PID is *pid* is therefore the owner; one that started in the same tick is not. 

252 A reader with an offset cannot compare start tokens, so the innermost PID alone keeps the marker. 

253 """ 

254 if start_token is None or (_BOOT_ID and start_token >> 64 not in {0, _BOOT_ID}): 

255 return False 

256 unshifted: Final = _starttime_unshifted() 

257 try: 

258 with os.scandir("/proc") as entries: 

259 return any( 

260 entry.name.isdigit() 

261 and (task := int(entry.name)) != pid 

262 and ( 

263 not unshifted 

264 or ((token := _read_start_token(task)) is not None and _same_start(token, start_token)) 

265 ) 

266 and _innermost_pid(task) == pid 

267 for entry in entries 

268 ) 

269 except OSError: 

270 # A /proc this process cannot list proves nothing either way, which leaves 4.0.12's verdict. 

271 return False 

272 

273 def _innermost_pid(task: int) -> int | None: 

274 try: 

275 status: Final = Path(f"/proc/{task}/status").read_bytes() 

276 except OSError: # pragma: no cover # the task exited between the stat read and this one 

277 return None 

278 # The kernel ends a line only with "\n". A task names itself, and its Name line keeps "\r", "\v" and the 

279 # other separators that a text-mode read and str.splitlines() split at. A name could then forge a line. 

280 return next( 

281 (int(line.split()[-1]) for line in status.split(b"\n") if line.startswith(b"NSpid:")), 

282 None, # kernels before 4.1 record no namespace PIDs 

283 ) 

284 

285 elif sys.platform == "darwin": # pragma: darwin cover 

286 import ctypes 

287 import struct 

288 

289 _LIBC: Final[ctypes.CDLL] = ctypes.CDLL(None, use_errno=True) 

290 _CTL_KERN: Final[int] = 1 

291 _KERN_PROC: Final[int] = 14 

292 _KERN_PROC_PID: Final[int] = 1 

293 # kinfo_proc opens with kp_proc.p_starttime (a struct timeval) at offset 0: int64 seconds, int32 microseconds. 

294 # The offset is fixed by the struct chain kinfo_proc -> extern_proc -> p_un, so a read at 0 is not a guess. 

295 _TIMEVAL_AT_ZERO: Final[str] = "<qi" 

296 _TIMEVAL_SIZE: Final[int] = struct.calcsize(_TIMEVAL_AT_ZERO) 

297 

298 def process_start_token(pid: int) -> int | None: 

299 """The process start time in microseconds from ``sysctl(KERN_PROC_PID)``, or ``None`` when it is gone.""" 

300 mib = (ctypes.c_int * 4)(_CTL_KERN, _KERN_PROC, _KERN_PROC_PID, pid) 

301 length = ctypes.c_size_t(0) 

302 # The size probe reports the kinfo_proc size for any PID, so the read below, not the probe, tells a live 

303 # process from a gone one: a gone PID leaves the fetch a zero-length success, so re-check the length after. 

304 if _LIBC.sysctl(mib, 4, None, ctypes.byref(length), None, 0) != 0: 

305 return None 

306 buffer = (ctypes.c_char * length.value)() 

307 if _LIBC.sysctl(mib, 4, buffer, ctypes.byref(length), None, 0) != 0 or length.value < _TIMEVAL_SIZE: 

308 return None 

309 seconds, microseconds = struct.unpack_from(_TIMEVAL_AT_ZERO, buffer.raw, 0) 

310 return seconds * 1_000_000 + microseconds 

311 

312 else: # pragma: no cover # a POSIX platform without a proven start-time source falls back to fail-closed liveness 

313 

314 def process_start_token(pid: int) -> int | None: 

315 """No proven start-time source, so the owner carries no token and liveness rests on the PID alone.""" 

316 del pid 

317 return None 

318 

319 

320if sys.platform not in {"linux", "android"}: # pragma: linux no cover # pragma: android no cover 

321 

322 def _owner_runs_elsewhere(pid: int, start_token: int | None) -> bool: 

323 """Without Linux PID namespaces, a PID that is gone or recycled leaves its owner nowhere else to run.""" 

324 del pid, start_token 

325 return False 

326 

327 

328__all__ = [ 

329 "host_name", 

330 "owner_is_current_process", 

331 "owner_is_stale", 

332 "process_alive", 

333 "process_start_token", 

334]