ccx-native 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +241 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
"""Internal Linux and Darwin process, filesystem and socket observations.
|
|
2
|
+
|
|
3
|
+
The boundary retains kernel /proc and kill(0) facts; callers make lifecycle,
|
|
4
|
+
identity and ownership decisions instead of treating PID presence as proof.
|
|
5
|
+
Only this module dispatches by OS. A Darwin fact that cannot be verified
|
|
6
|
+
remains unprovable; it never falls back to a Linux PID or pathname.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
import ctypes
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
import errno
|
|
16
|
+
import fcntl
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
import os
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
import re
|
|
21
|
+
import select
|
|
22
|
+
import signal
|
|
23
|
+
import socket
|
|
24
|
+
import stat
|
|
25
|
+
import struct
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
from typing import Iterator, Literal
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Presence = Literal["alive", "dead", "unprovable"]
|
|
34
|
+
PidProbe = Literal["exists", "missing", "denied"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UnsupportedPlatformError(RuntimeError):
|
|
38
|
+
"""No verified implementation of this OS fact is available."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PeerIdentityUnavailable(OSError):
|
|
42
|
+
"""The kernel did not provide usable credentials on this connection."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RootOpenError(OSError):
|
|
46
|
+
"""An explicit root ancestor or leaf could not be opened safely."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if sys.platform not in ("linux", "darwin"):
|
|
50
|
+
raise UnsupportedPlatformError(f"unsupported operating system: {sys.platform}")
|
|
51
|
+
UNIX_SUN_PATH_BYTES = 108 if sys.platform == "linux" else 104
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def claude_project_cwd_is_nfc() -> bool:
|
|
55
|
+
"""Claude normalizes the native project cwd on Darwin, not on Linux."""
|
|
56
|
+
return sys.platform == "darwin"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def direct_oauth_usage_supported() -> bool:
|
|
60
|
+
"""Direct credential-file usage is available only on Linux for now.
|
|
61
|
+
|
|
62
|
+
Claude Code's own promptless get_usage path supports macOS separately.
|
|
63
|
+
"""
|
|
64
|
+
return sys.platform == "linux"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def stdout_reader_gone(fd: int) -> bool:
|
|
68
|
+
"""Prove that a pipe's reader has closed, without masking other EPIPEs.
|
|
69
|
+
|
|
70
|
+
Linux poll reports POLLERR on the abandoned write end. macOS has no
|
|
71
|
+
select.poll; EVFILT_WRITE reports EV_EOF for the same abandoned pipe.
|
|
72
|
+
A missing/inconclusive kernel event must not hide an unrelated socket
|
|
73
|
+
error or alter the CLI's exit status.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
if sys.platform == "linux":
|
|
77
|
+
poller = select.poll()
|
|
78
|
+
poller.register(fd, select.POLLOUT)
|
|
79
|
+
return any(event & select.POLLERR for _, event in poller.poll(0))
|
|
80
|
+
queue = select.kqueue()
|
|
81
|
+
try:
|
|
82
|
+
change = select.kevent(
|
|
83
|
+
fd, filter=select.KQ_FILTER_WRITE,
|
|
84
|
+
flags=select.KQ_EV_ADD | select.KQ_EV_ENABLE,
|
|
85
|
+
)
|
|
86
|
+
return any(
|
|
87
|
+
event.flags & select.KQ_EV_EOF
|
|
88
|
+
for event in queue.control([change], 1, 0)
|
|
89
|
+
)
|
|
90
|
+
finally:
|
|
91
|
+
queue.close()
|
|
92
|
+
except (AttributeError, OSError, ValueError):
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class ProcessInfo:
|
|
98
|
+
pid: int | None
|
|
99
|
+
presence: Presence
|
|
100
|
+
ppid: int | None
|
|
101
|
+
state: str | None
|
|
102
|
+
start_token: str | None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class PeerCredentials:
|
|
107
|
+
uid: int
|
|
108
|
+
pid: int
|
|
109
|
+
gid: int | None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class RuntimeChoice:
|
|
114
|
+
"""Darwin's read-only base selection; only callers may create a safe leaf."""
|
|
115
|
+
|
|
116
|
+
path: Path
|
|
117
|
+
may_create: bool = False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@contextmanager
|
|
121
|
+
def open_root_dir(path: Path) -> Iterator[tuple[Path, int]]:
|
|
122
|
+
"""Pin each root ancestor without following links, even if execute-only."""
|
|
123
|
+
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
|
124
|
+
path_flags = os.O_PATH | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
|
125
|
+
absolute = Path(os.path.abspath(os.fspath(path)))
|
|
126
|
+
parts = absolute.parts[1:]
|
|
127
|
+
|
|
128
|
+
def pin(name: str, mode: int, reason: str, **kwargs) -> int:
|
|
129
|
+
try:
|
|
130
|
+
return os.open(name, mode, **kwargs)
|
|
131
|
+
except OSError as exc:
|
|
132
|
+
raise RootOpenError(reason) from exc
|
|
133
|
+
|
|
134
|
+
fd = pin(absolute.anchor, path_flags if parts else flags, "unsafe explicit root anchor")
|
|
135
|
+
try:
|
|
136
|
+
for index, part in enumerate(parts):
|
|
137
|
+
next_fd = pin(part, flags if index == len(parts) - 1 else path_flags,
|
|
138
|
+
"unsafe explicit root component (symlink or non-directory)", dir_fd=fd)
|
|
139
|
+
previous, fd = fd, next_fd
|
|
140
|
+
os.close(previous)
|
|
141
|
+
yield absolute, fd
|
|
142
|
+
finally:
|
|
143
|
+
os.close(fd)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def peer_credentials(conn: socket.socket) -> PeerCredentials:
|
|
147
|
+
option = getattr(socket, "SO_PEERCRED", None)
|
|
148
|
+
if option is None:
|
|
149
|
+
raise PeerIdentityUnavailable("Linux SO_PEERCRED unavailable")
|
|
150
|
+
try:
|
|
151
|
+
raw = conn.getsockopt(socket.SOL_SOCKET, option, struct.calcsize("3i"))
|
|
152
|
+
pid, uid, gid = struct.unpack("3i", raw)
|
|
153
|
+
except (OSError, struct.error) as exc:
|
|
154
|
+
raise PeerIdentityUnavailable(str(exc)) from exc
|
|
155
|
+
return PeerCredentials(uid=uid, pid=pid, gid=gid)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def probe_pid(pid: object) -> PidProbe:
|
|
159
|
+
try:
|
|
160
|
+
os.kill(pid, 0)
|
|
161
|
+
return "exists"
|
|
162
|
+
except (ProcessLookupError, OverflowError, ValueError, TypeError):
|
|
163
|
+
return "missing"
|
|
164
|
+
except PermissionError:
|
|
165
|
+
return "denied"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _linux_parse_stat(pid: int, raw: bytes) -> ProcessInfo:
|
|
169
|
+
try:
|
|
170
|
+
# comm may contain spaces, ')' and non-UTF-8 bytes. Split after the
|
|
171
|
+
# final ')' before interpreting the ASCII fields (including start #22).
|
|
172
|
+
tail = raw.rsplit(b")", 1)[1].split()
|
|
173
|
+
state = tail[0].decode("ascii")
|
|
174
|
+
except (ValueError, IndexError, TypeError):
|
|
175
|
+
return ProcessInfo(pid, "unprovable", None, None, None)
|
|
176
|
+
def optional(index: int, convert):
|
|
177
|
+
try:
|
|
178
|
+
return convert(tail[index])
|
|
179
|
+
except (ValueError, IndexError, TypeError):
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
ppid = optional(1, int) # field 4
|
|
183
|
+
start = optional(19, lambda text: text.decode("ascii")) # field 22 clock ticks
|
|
184
|
+
dead = state in ("Z", "X")
|
|
185
|
+
presence = (
|
|
186
|
+
"dead" if dead else
|
|
187
|
+
"alive" if start is not None else "unprovable"
|
|
188
|
+
)
|
|
189
|
+
return ProcessInfo(pid, presence, ppid, state, None if dead else start)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def process_info(pid: int | None) -> ProcessInfo:
|
|
193
|
+
if not pid:
|
|
194
|
+
return ProcessInfo(pid, "dead", None, None, None)
|
|
195
|
+
try:
|
|
196
|
+
raw = Path(f"/proc/{int(pid)}/stat").read_bytes()
|
|
197
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
198
|
+
presence = "dead" if isinstance(exc, FileNotFoundError) else "unprovable"
|
|
199
|
+
return ProcessInfo(pid, presence, None, None, None)
|
|
200
|
+
return _linux_parse_stat(pid, raw)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def read_argv(pid: int | None) -> bytes | None:
|
|
204
|
+
if not pid:
|
|
205
|
+
return None
|
|
206
|
+
try:
|
|
207
|
+
return Path(f"/proc/{pid}/cmdline").read_bytes()
|
|
208
|
+
except OSError:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
PROC_LOCKS = "/proc/locks"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _linux_held_flock_inodes() -> frozenset[tuple[int, int, int]] | None:
|
|
216
|
+
"""Every (major, minor, inode) with a granted flock, read without locking.
|
|
217
|
+
|
|
218
|
+
None when the kernel table is unreadable. Never takes a lock itself: a
|
|
219
|
+
probe lock could make a concurrent non-blocking lease acquisition fail.
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
with open(PROC_LOCKS, encoding="ascii", errors="replace") as stream:
|
|
223
|
+
lines = stream.read().splitlines()
|
|
224
|
+
except OSError:
|
|
225
|
+
return None
|
|
226
|
+
held = set()
|
|
227
|
+
for line in lines:
|
|
228
|
+
fields = line.split()
|
|
229
|
+
# A granted lock: "<n>: FLOCK ADVISORY WRITE <pid> MAJ:MIN:INO 0 EOF".
|
|
230
|
+
# A blocked waiter's line has "->" after the index and is skipped.
|
|
231
|
+
if len(fields) < 6 or fields[1] != "FLOCK":
|
|
232
|
+
continue
|
|
233
|
+
parts = fields[5].split(":")
|
|
234
|
+
if len(parts) != 3:
|
|
235
|
+
continue
|
|
236
|
+
try:
|
|
237
|
+
held.add((int(parts[0], 16), int(parts[1], 16), int(parts[2])))
|
|
238
|
+
except ValueError:
|
|
239
|
+
continue
|
|
240
|
+
return frozenset(held)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
held_flock_inodes = _linux_held_flock_inodes
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _darwin_held_flock_inodes() -> None:
|
|
247
|
+
"""Darwin has no lock-free, dependency-free view of other processes'
|
|
248
|
+
flocks (``lsof`` is an external spawn; ``F_GETLK`` sees POSIX locks only;
|
|
249
|
+
a probe ``flock`` would contend). Lease presence is unobservable here."""
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def fits_unix_path(path: Path | str) -> bool:
|
|
254
|
+
return len(os.fsencode(path)) + 1 <= UNIX_SUN_PATH_BYTES
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def pinned_fd_path(fd: int) -> str:
|
|
258
|
+
"""Reopen a still-held Linux file descriptor, never its mutable pathname."""
|
|
259
|
+
return f"/proc/self/fd/{fd}"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def arm_parent_death(parent_pid: int) -> None:
|
|
263
|
+
"""Fail closed when a read-only child cannot arm Linux parent death."""
|
|
264
|
+
try:
|
|
265
|
+
libc = ctypes.CDLL(None, use_errno=True)
|
|
266
|
+
result = libc.prctl(1, signal.SIGKILL, 0, 0, 0)
|
|
267
|
+
except (OSError, AttributeError, TypeError, ctypes.ArgumentError):
|
|
268
|
+
os._exit(0)
|
|
269
|
+
if result != 0 or os.getppid() != parent_pid:
|
|
270
|
+
os._exit(0)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def runtime_base(
|
|
274
|
+
*, purpose: Literal["transport", "live_vars"], suffix_bytes: int = 0,
|
|
275
|
+
) -> RuntimeChoice | None:
|
|
276
|
+
"""Choose an existing private XDG base without writing to the filesystem.
|
|
277
|
+
|
|
278
|
+
An inherited XDG_RUNTIME_DIR may belong to a different user (for example
|
|
279
|
+
when using su without -); transport refuses it, while live-vars can use
|
|
280
|
+
the per-user directory under the system temporary base.
|
|
281
|
+
"""
|
|
282
|
+
private = None
|
|
283
|
+
text = os.environ.get("XDG_RUNTIME_DIR")
|
|
284
|
+
if text and os.path.isabs(text):
|
|
285
|
+
try:
|
|
286
|
+
st = os.lstat(text)
|
|
287
|
+
except OSError:
|
|
288
|
+
pass
|
|
289
|
+
else:
|
|
290
|
+
if (stat.S_ISDIR(st.st_mode) and st.st_uid == os.getuid()
|
|
291
|
+
and not st.st_mode & 0o077):
|
|
292
|
+
private = Path(text)
|
|
293
|
+
if purpose == "transport":
|
|
294
|
+
if (private is not None
|
|
295
|
+
and len(os.fsencode(private)) + suffix_bytes <= UNIX_SUN_PATH_BYTES):
|
|
296
|
+
return RuntimeChoice(private)
|
|
297
|
+
return _short_private_runtime_base(Path("/tmp"), suffix_bytes)
|
|
298
|
+
if purpose == "live_vars":
|
|
299
|
+
return RuntimeChoice(private or Path(tempfile.gettempdir()))
|
|
300
|
+
raise ValueError(f"unsupported runtime purpose: {purpose}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _short_private_runtime_base(parent: Path, suffix_bytes: int) -> RuntimeChoice:
|
|
304
|
+
"""Guard the OS's short sticky tmp ancestor and owner-only UID leaf.
|
|
305
|
+
|
|
306
|
+
Selection never creates anything. Transport/live-vars create a missing leaf
|
|
307
|
+
later, under an ACTIVE generation and a no-follow parent descriptor.
|
|
308
|
+
"""
|
|
309
|
+
st = os.lstat(parent)
|
|
310
|
+
if (not stat.S_ISDIR(st.st_mode) or st.st_uid != 0
|
|
311
|
+
or not st.st_mode & stat.S_ISVTX or not st.st_mode & 0o002):
|
|
312
|
+
raise OSError(f"short runtime ancestor must be root-owned sticky {parent}")
|
|
313
|
+
base = parent / f"ccx-u{os.getuid()}"
|
|
314
|
+
if len(os.fsencode(base)) + suffix_bytes > UNIX_SUN_PATH_BYTES:
|
|
315
|
+
raise OSError("private runtime pathname exceeds AF_UNIX budget")
|
|
316
|
+
try:
|
|
317
|
+
existing = os.lstat(base)
|
|
318
|
+
except FileNotFoundError:
|
|
319
|
+
return RuntimeChoice(base, may_create=True)
|
|
320
|
+
if (not stat.S_ISDIR(existing.st_mode) or existing.st_uid != os.getuid()
|
|
321
|
+
or existing.st_mode & 0o077):
|
|
322
|
+
raise OSError("short runtime base is not private and user-owned")
|
|
323
|
+
return RuntimeChoice(base)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
_ENGLISH_WEEKDAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
|
|
327
|
+
_ENGLISH_MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
328
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
|
329
|
+
_DARWIN_LSTART = re.compile(
|
|
330
|
+
r"^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) "
|
|
331
|
+
r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) "
|
|
332
|
+
r"\s?([0-3]?\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$",
|
|
333
|
+
re.ASCII,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def provider_start_matches(
|
|
338
|
+
provider_start: object, kernel_start: str | None,
|
|
339
|
+
provider_domain: object = None,
|
|
340
|
+
) -> bool:
|
|
341
|
+
"""Does Claude's provider start denote this kernel-observed process start?
|
|
342
|
+
|
|
343
|
+
Linux provider metadata is the exact decimal /proc field-22 token. Darwin
|
|
344
|
+
records a C-locale, UTC one-second lstart timestamp; its kernel token
|
|
345
|
+
retains the finer microseconds, which must not be compared as a string.
|
|
346
|
+
Parse English names explicitly, independent of the host's LC_TIME locale.
|
|
347
|
+
"""
|
|
348
|
+
if not isinstance(kernel_start, str):
|
|
349
|
+
return False
|
|
350
|
+
# Claude's Linux pidDomain includes machine and namespace details, e.g.
|
|
351
|
+
# linux:<machine-id>:pid:[<namespace-inode>]. Only its OS prefix is
|
|
352
|
+
# relevant here: kernel PID/start ownership is proved independently.
|
|
353
|
+
if (provider_domain is not None
|
|
354
|
+
and (not isinstance(provider_domain, str)
|
|
355
|
+
or provider_domain.split(":", 1)[0] != sys.platform)):
|
|
356
|
+
return False
|
|
357
|
+
if sys.platform == "linux":
|
|
358
|
+
return (type(provider_start) in (str, int)
|
|
359
|
+
and str(provider_start).isascii()
|
|
360
|
+
and str(provider_start).isdecimal()
|
|
361
|
+
and str(provider_start) == kernel_start)
|
|
362
|
+
if sys.platform != "darwin" or not isinstance(provider_start, str):
|
|
363
|
+
return False
|
|
364
|
+
match = _DARWIN_LSTART.fullmatch(provider_start)
|
|
365
|
+
token = re.fullmatch(r"darwin:(\d+):(\d{6})", kernel_start, re.ASCII)
|
|
366
|
+
if match is None or token is None:
|
|
367
|
+
return False
|
|
368
|
+
try:
|
|
369
|
+
weekday, month, day, hour, minute, second, year = match.groups()
|
|
370
|
+
parsed = datetime(int(year), _ENGLISH_MONTHS.index(month) + 1,
|
|
371
|
+
int(day), int(hour), int(minute), int(second),
|
|
372
|
+
tzinfo=timezone.utc)
|
|
373
|
+
return (parsed.weekday() == _ENGLISH_WEEKDAYS.index(weekday)
|
|
374
|
+
and int(parsed.timestamp()) == int(token[1])
|
|
375
|
+
and int(token[2]) < 1_000_000)
|
|
376
|
+
except (ValueError, OverflowError):
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def valid_kernel_start_token(token: object) -> bool:
|
|
381
|
+
"""Recognize only this OS's original process-start identity encoding."""
|
|
382
|
+
if not isinstance(token, str) or not token:
|
|
383
|
+
return False
|
|
384
|
+
if sys.platform == "linux":
|
|
385
|
+
return token.isascii() and token.isdecimal()
|
|
386
|
+
if sys.platform == "darwin":
|
|
387
|
+
match = re.fullmatch(r"darwin:(\d+):(\d{6})", token, re.ASCII)
|
|
388
|
+
return bool(match and int(match[1]) > 0 and int(match[2]) < 1_000_000)
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# Darwin struct proc_bsdinfo from sys/proc_info.h (MAXCOMLEN=16). These
|
|
393
|
+
# layouts and the imported constants must be checked against the installed
|
|
394
|
+
# Monterey SDK before treating a real Mac run as release validation.
|
|
395
|
+
_DARWIN_PROC_PIDTBSDINFO = 3
|
|
396
|
+
_DARWIN_SZOMB = 5
|
|
397
|
+
_DARWIN_SOL_LOCAL = 0
|
|
398
|
+
_DARWIN_LOCAL_PEERCRED = 1
|
|
399
|
+
_DARWIN_LOCAL_PEERPID = 2
|
|
400
|
+
_DARWIN_XUCRED_VERSION = 0
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class _DarwinBSDInfo(ctypes.Structure):
|
|
404
|
+
_fields_ = [
|
|
405
|
+
("pbi_flags", ctypes.c_uint32), ("pbi_status", ctypes.c_uint32),
|
|
406
|
+
("pbi_xstatus", ctypes.c_uint32), ("pbi_pid", ctypes.c_uint32),
|
|
407
|
+
("pbi_ppid", ctypes.c_uint32), ("pbi_uid", ctypes.c_uint32),
|
|
408
|
+
("pbi_gid", ctypes.c_uint32), ("pbi_ruid", ctypes.c_uint32),
|
|
409
|
+
("pbi_rgid", ctypes.c_uint32), ("pbi_svuid", ctypes.c_uint32),
|
|
410
|
+
("pbi_svgid", ctypes.c_uint32), ("rfu_1", ctypes.c_uint32),
|
|
411
|
+
("pbi_comm", ctypes.c_char * 16), ("pbi_name", ctypes.c_char * 32),
|
|
412
|
+
("pbi_nfiles", ctypes.c_uint32), ("pbi_pgid", ctypes.c_uint32),
|
|
413
|
+
("pbi_pjobc", ctypes.c_uint32), ("e_tdev", ctypes.c_uint32),
|
|
414
|
+
("e_tpgid", ctypes.c_uint32), ("pbi_nice", ctypes.c_int32),
|
|
415
|
+
("pbi_start_tvsec", ctypes.c_uint64),
|
|
416
|
+
("pbi_start_tvusec", ctypes.c_uint64),
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class _DarwinXucred(ctypes.Structure):
|
|
421
|
+
_fields_ = [
|
|
422
|
+
("cr_version", ctypes.c_uint32), ("cr_uid", ctypes.c_uint32),
|
|
423
|
+
("cr_ngroups", ctypes.c_int16),
|
|
424
|
+
("cr_groups", ctypes.c_uint32 * 16),
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@lru_cache(maxsize=1)
|
|
429
|
+
def _darwin_proc_pidinfo():
|
|
430
|
+
"""Load libproc and configure its ABI once, lazily on first Darwin use."""
|
|
431
|
+
libproc = ctypes.CDLL("/usr/lib/libproc.dylib", use_errno=True)
|
|
432
|
+
call = libproc.proc_pidinfo
|
|
433
|
+
call.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_uint64,
|
|
434
|
+
ctypes.c_void_p, ctypes.c_int]
|
|
435
|
+
call.restype = ctypes.c_int
|
|
436
|
+
return call
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _darwin_pidinfo_call(pid: int, info: _DarwinBSDInfo) -> tuple[int, int]:
|
|
440
|
+
"""One SDK call; injectable on Linux for exact ABI/error tests."""
|
|
441
|
+
call = _darwin_proc_pidinfo()
|
|
442
|
+
ctypes.set_errno(0)
|
|
443
|
+
result = call(pid, _DARWIN_PROC_PIDTBSDINFO, 0,
|
|
444
|
+
ctypes.byref(info), ctypes.sizeof(info))
|
|
445
|
+
return result, ctypes.get_errno()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _darwin_process_info(pid: int | None) -> ProcessInfo:
|
|
449
|
+
if not pid:
|
|
450
|
+
return ProcessInfo(pid, "dead", None, None, None)
|
|
451
|
+
if type(pid) is not int or pid <= 0:
|
|
452
|
+
return ProcessInfo(pid, "unprovable", None, None, None)
|
|
453
|
+
info = _DarwinBSDInfo()
|
|
454
|
+
try:
|
|
455
|
+
size, error = _darwin_pidinfo_call(pid, info)
|
|
456
|
+
except (OSError, AttributeError, TypeError, ValueError):
|
|
457
|
+
return ProcessInfo(pid, "unprovable", None, None, None)
|
|
458
|
+
if size != ctypes.sizeof(info):
|
|
459
|
+
# ESRCH establishes a missing process. A short result, EPERM,
|
|
460
|
+
# race or ambiguous zero must not establish absence/ownership.
|
|
461
|
+
return ProcessInfo(pid, "dead" if size <= 0 and error == errno.ESRCH
|
|
462
|
+
else "unprovable", None, None, None)
|
|
463
|
+
if (info.pbi_pid != pid or info.pbi_status not in (1, 2, 3, 4, 5)
|
|
464
|
+
or info.pbi_uid == 0xffffffff):
|
|
465
|
+
return ProcessInfo(pid, "unprovable", None, None, None)
|
|
466
|
+
state = {1: "I", 2: "R", 3: "S", 4: "T", 5: "Z"}[info.pbi_status]
|
|
467
|
+
if info.pbi_status == _DARWIN_SZOMB:
|
|
468
|
+
return ProcessInfo(pid, "dead", info.pbi_ppid, state, None)
|
|
469
|
+
sec, usec = info.pbi_start_tvsec, info.pbi_start_tvusec
|
|
470
|
+
if sec <= 0 or usec >= 1_000_000:
|
|
471
|
+
return ProcessInfo(pid, "unprovable", info.pbi_ppid, state, None)
|
|
472
|
+
return ProcessInfo(pid, "alive", info.pbi_ppid, state,
|
|
473
|
+
f"darwin:{sec}:{usec:06d}")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _darwin_peer_credentials(conn: socket.socket) -> PeerCredentials:
|
|
477
|
+
"""Both credentials are from this same accepted AF_UNIX connection."""
|
|
478
|
+
try:
|
|
479
|
+
credentials = conn.getsockopt(
|
|
480
|
+
_DARWIN_SOL_LOCAL, _DARWIN_LOCAL_PEERCRED, ctypes.sizeof(_DarwinXucred))
|
|
481
|
+
peer_pid = conn.getsockopt(
|
|
482
|
+
_DARWIN_SOL_LOCAL, _DARWIN_LOCAL_PEERPID, ctypes.sizeof(ctypes.c_int32))
|
|
483
|
+
if (not isinstance(credentials, bytes) or
|
|
484
|
+
len(credentials) != ctypes.sizeof(_DarwinXucred)
|
|
485
|
+
or not isinstance(peer_pid, bytes)
|
|
486
|
+
or len(peer_pid) != ctypes.sizeof(ctypes.c_int32)):
|
|
487
|
+
raise ValueError("incomplete Darwin kernel peer credentials")
|
|
488
|
+
identity = _DarwinXucred.from_buffer_copy(credentials)
|
|
489
|
+
pid = ctypes.c_int32.from_buffer_copy(peer_pid).value
|
|
490
|
+
if (identity.cr_version != _DARWIN_XUCRED_VERSION or
|
|
491
|
+
identity.cr_uid == 0xffffffff or
|
|
492
|
+
not 0 <= identity.cr_ngroups <= 16 or pid <= 0):
|
|
493
|
+
raise ValueError("invalid Darwin xucred version, UID, group count or peer PID")
|
|
494
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
495
|
+
raise PeerIdentityUnavailable(f"Darwin socket peer identity unavailable: {exc}") from exc
|
|
496
|
+
gid = identity.cr_groups[0] if identity.cr_ngroups else None
|
|
497
|
+
return PeerCredentials(uid=identity.cr_uid, pid=pid, gid=gid)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@contextmanager
|
|
501
|
+
def _darwin_open_root_dir(path: Path) -> Iterator[tuple[Path, int]]:
|
|
502
|
+
"""Pin no-follow ancestors and leaf, including search-only ancestors.
|
|
503
|
+
|
|
504
|
+
macOS 27 offers O_SEARCH for traverse-only directories. Older Darwin
|
|
505
|
+
kernels may reject it; retain their established O_RDONLY behavior.
|
|
506
|
+
"""
|
|
507
|
+
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
|
508
|
+
search = getattr(os, "O_SEARCH", 0)
|
|
509
|
+
absolute = Path(os.path.abspath(os.fspath(path)))
|
|
510
|
+
fd: int | None = None
|
|
511
|
+
|
|
512
|
+
def pinned_open(component: str, *, parent: int | None, leaf: bool) -> int:
|
|
513
|
+
options = {} if parent is None else {"dir_fd": parent}
|
|
514
|
+
if not leaf and search:
|
|
515
|
+
try:
|
|
516
|
+
return os.open(component, flags | search, **options)
|
|
517
|
+
except OSError as exc:
|
|
518
|
+
if exc.errno not in (errno.EINVAL, errno.ENOTSUP, errno.EOPNOTSUPP,
|
|
519
|
+
errno.ENOSYS):
|
|
520
|
+
raise
|
|
521
|
+
return os.open(component, flags, **options)
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
parts = absolute.parts[1:]
|
|
525
|
+
fd = pinned_open(absolute.anchor, parent=None, leaf=not parts)
|
|
526
|
+
for index, part in enumerate(parts):
|
|
527
|
+
next_fd = pinned_open(part, parent=fd, leaf=index == len(parts) - 1)
|
|
528
|
+
os.close(fd)
|
|
529
|
+
fd = next_fd
|
|
530
|
+
except OSError as exc:
|
|
531
|
+
if fd is not None:
|
|
532
|
+
os.close(fd)
|
|
533
|
+
raise RootOpenError("unsafe/unreadable Darwin root ancestor or leaf") from exc
|
|
534
|
+
try:
|
|
535
|
+
# Exceptions raised by the context body retain their original type.
|
|
536
|
+
yield absolute, fd
|
|
537
|
+
finally:
|
|
538
|
+
os.close(fd)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _darwin_runtime_base(
|
|
542
|
+
*, purpose: Literal["transport", "live_vars"], suffix_bytes: int = 0,
|
|
543
|
+
) -> RuntimeChoice:
|
|
544
|
+
"""Select a short private base without any filesystem writes."""
|
|
545
|
+
if purpose not in ("transport", "live_vars") or suffix_bytes < 0:
|
|
546
|
+
raise ValueError(f"unsupported Darwin runtime selection: {purpose}")
|
|
547
|
+
xdg = os.environ.get("XDG_RUNTIME_DIR")
|
|
548
|
+
if xdg and os.path.isabs(xdg):
|
|
549
|
+
try:
|
|
550
|
+
st = os.lstat(xdg)
|
|
551
|
+
except OSError:
|
|
552
|
+
pass
|
|
553
|
+
else:
|
|
554
|
+
if (stat.S_ISDIR(st.st_mode) and st.st_uid == os.getuid()
|
|
555
|
+
and not st.st_mode & 0o077
|
|
556
|
+
and len(os.fsencode(xdg)) + suffix_bytes <= 104):
|
|
557
|
+
return RuntimeChoice(Path(xdg))
|
|
558
|
+
# No reliance on macOS's often very long /var/folders/... TMPDIR.
|
|
559
|
+
return _short_private_runtime_base(Path("/private/tmp"), suffix_bytes)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _darwin_pinned_fd_path(fd: int) -> str:
|
|
563
|
+
"""Probe /dev/fd reopen on this exact held inode, never its pathname."""
|
|
564
|
+
original = os.fstat(fd)
|
|
565
|
+
if not stat.S_ISREG(original.st_mode):
|
|
566
|
+
raise OSError("cannot reopen nonregular Darwin source")
|
|
567
|
+
old_offset = os.lseek(fd, 0, os.SEEK_CUR)
|
|
568
|
+
path = f"/dev/fd/{fd}"
|
|
569
|
+
reopened = os.open(path, os.O_RDONLY | os.O_CLOEXEC)
|
|
570
|
+
try:
|
|
571
|
+
observed = os.fstat(reopened)
|
|
572
|
+
if ((observed.st_dev, observed.st_ino, observed.st_mode) !=
|
|
573
|
+
(original.st_dev, original.st_ino, original.st_mode)
|
|
574
|
+
or observed.st_size != original.st_size):
|
|
575
|
+
raise OSError("Darwin /dev/fd did not reopen the original file")
|
|
576
|
+
saved = os.lseek(reopened, 0, os.SEEK_CUR)
|
|
577
|
+
os.lseek(reopened, 0, os.SEEK_SET)
|
|
578
|
+
if os.lseek(fd, 0, os.SEEK_CUR) != old_offset:
|
|
579
|
+
raise OSError("Darwin /dev/fd reopen shares mutable source offset")
|
|
580
|
+
os.lseek(reopened, saved, os.SEEK_SET)
|
|
581
|
+
if os.lseek(fd, 0, os.SEEK_CUR) != old_offset:
|
|
582
|
+
raise OSError("Darwin /dev/fd verification changed source offset")
|
|
583
|
+
finally:
|
|
584
|
+
os.close(reopened)
|
|
585
|
+
if os.lseek(fd, 0, os.SEEK_CUR) != old_offset:
|
|
586
|
+
os.lseek(fd, old_offset, os.SEEK_SET)
|
|
587
|
+
return path
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _darwin_read_argv(_pid: int | None) -> None:
|
|
591
|
+
"""Darwin argv is not an ownership fact; no KERN_PROCARGS2 parser."""
|
|
592
|
+
return None
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def physical_directory_spelling(resolved: Path) -> str:
|
|
596
|
+
"""Linux's physical symlink resolution keeps Path.resolve() spelling."""
|
|
597
|
+
return str(resolved)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _darwin_physical_directory_spelling(resolved: Path) -> str:
|
|
601
|
+
"""Read the real APFS directory case via a pinned descriptor/F_GETPATH.
|
|
602
|
+
|
|
603
|
+
Creation opens the resolved directory (not O_NOFOLLOW: symlinks are valid).
|
|
604
|
+
For an untrusted path that was deleted after its ledger write, read the
|
|
605
|
+
longest existing directory ancestor and append only the missing suffix.
|
|
606
|
+
"""
|
|
607
|
+
ancestor = resolved
|
|
608
|
+
suffix: list[str] = []
|
|
609
|
+
while True:
|
|
610
|
+
try:
|
|
611
|
+
fd = os.open(ancestor, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
|
|
612
|
+
except OSError as exc:
|
|
613
|
+
if exc.errno not in (errno.ENOENT, errno.ENOTDIR) or ancestor == ancestor.parent:
|
|
614
|
+
raise
|
|
615
|
+
suffix.insert(0, ancestor.name)
|
|
616
|
+
ancestor = ancestor.parent
|
|
617
|
+
continue
|
|
618
|
+
try:
|
|
619
|
+
# F_GETPATH is Darwin fcntl command 50; some Python builds lack
|
|
620
|
+
# the symbolic name even though the kernel supplies the operation.
|
|
621
|
+
raw = fcntl.fcntl(fd, getattr(fcntl, "F_GETPATH", 50), bytes(1024))
|
|
622
|
+
finally:
|
|
623
|
+
os.close(fd)
|
|
624
|
+
if not isinstance(raw, bytes) or b"\0" not in raw or raw.startswith(b"\0"):
|
|
625
|
+
raise OSError(errno.EIO, "Darwin F_GETPATH returned an invalid directory path")
|
|
626
|
+
actual = Path(os.fsdecode(raw.split(b"\0", 1)[0]))
|
|
627
|
+
if not actual.is_absolute():
|
|
628
|
+
raise OSError(errno.EIO, "Darwin F_GETPATH returned a relative directory path")
|
|
629
|
+
return str(actual.joinpath(*suffix))
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _darwin_parent_watch(parent_pid: int) -> None:
|
|
633
|
+
while True:
|
|
634
|
+
try:
|
|
635
|
+
if os.getppid() != parent_pid:
|
|
636
|
+
os._exit(0)
|
|
637
|
+
time.sleep(0.3)
|
|
638
|
+
except Exception:
|
|
639
|
+
os._exit(0)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _darwin_arm_parent_death(parent_pid: int) -> None:
|
|
643
|
+
"""Read-only RPC children die on lost parent even during blocked I/O."""
|
|
644
|
+
try:
|
|
645
|
+
if parent_pid <= 0 or os.getppid() != parent_pid:
|
|
646
|
+
os._exit(0)
|
|
647
|
+
watcher = threading.Thread(target=_darwin_parent_watch,
|
|
648
|
+
args=(parent_pid,), daemon=True,
|
|
649
|
+
name="ccx-read-only-parent-watch")
|
|
650
|
+
watcher.start()
|
|
651
|
+
if not watcher.is_alive() or os.getppid() != parent_pid:
|
|
652
|
+
os._exit(0)
|
|
653
|
+
except Exception:
|
|
654
|
+
os._exit(0)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
# The sole OS dispatch in CCX; callers consume facts, not platform branches.
|
|
658
|
+
if sys.platform == "darwin":
|
|
659
|
+
process_info = _darwin_process_info
|
|
660
|
+
read_argv = _darwin_read_argv
|
|
661
|
+
peer_credentials = _darwin_peer_credentials
|
|
662
|
+
open_root_dir = _darwin_open_root_dir
|
|
663
|
+
runtime_base = _darwin_runtime_base
|
|
664
|
+
pinned_fd_path = _darwin_pinned_fd_path
|
|
665
|
+
arm_parent_death = _darwin_arm_parent_death
|
|
666
|
+
physical_directory_spelling = _darwin_physical_directory_spelling
|
|
667
|
+
held_flock_inodes = _darwin_held_flock_inodes
|