Verify inherited network namespace entry
Add private `enter_netns()` bootstrap validation before actor runtime integration. Deats, - verify the inherited FD against its expected namespace inode - constrain `setns()` to Linux network namespaces - verify `/proc/self/ns/net` after the entry syscall - leave inherited FD ownership to the future spawn caller - exercise guards with real FDs and unprivileged syscall fakes Prompt-IO: ai/prompt-io/opencode/20260827T232500Z_d067505a_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
parent
d067505add
commit
05432e4d00
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
session: tractor-addr-unpacking
|
||||
timestamp: 2026-08-27T23:25:00Z
|
||||
git_ref: d067505a
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260827T232500Z_d067505a_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
After committing the reviewed bindspace-reference changes, the human
|
||||
asked to continue with the next Layer C increment.
|
||||
|
||||
## Response summary
|
||||
|
||||
Added the private Linux network-namespace entry primitive required by
|
||||
future child bootstrap wiring. It verifies the inherited capability
|
||||
before `setns()`, constrains entry to network namespaces, and verifies
|
||||
the resulting process namespace before runtime initialization proceeds.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/spawn/_netns.py` - synchronous verified netns entry.
|
||||
- `tests/test_netns_spawn.py` - stale-FD, syscall and post-entry guards.
|
||||
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - primitive ownership and
|
||||
remaining integration boundary.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human requested the next incremental implementation after accepting
|
||||
the prior split commits. Their established preference for end-to-end
|
||||
tests informed use of real FDs where possible; only privileged `setns()`
|
||||
and procfs observation are replaced to keep the suite unprivileged. In
|
||||
staged review, the human requested the full project license header,
|
||||
explicit comments on both intentional inode mismatches, and independent
|
||||
call traces proving both fake syscall and procfs hooks execute. The
|
||||
agent applied those human-directed revisions; no direct manual source
|
||||
edits were observed.
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-27T23:25:00Z
|
||||
git_ref: d067505a
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
# Raw output - verify pre-runtime netns entry
|
||||
|
||||
After committing realized bindspace references and their deferred codec
|
||||
plan, the human requested the next Layer C increment.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_netns.py tests/test_netns_spawn.py ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
||||
|
||||
Added a private synchronous `enter_netns()` bootstrap primitive. It
|
||||
validates the inherited FD and expected inode, constrains `setns()` to
|
||||
`CLONE_NEWNET`, verifies `/proc/self/ns/net` afterward, and leaves FD
|
||||
closure to the future spawn-bootstrap caller.
|
||||
|
||||
Tests use real stand-in FDs while replacing only the privileged syscall
|
||||
and post-entry procfs observation. They prove stale FDs fail before
|
||||
entry, the syscall receives the exact namespace type, and bootstrap
|
||||
rejects an unexpected post-entry namespace. Ruff and all three focused
|
||||
tests passed.
|
||||
|
|
@ -621,6 +621,12 @@ server bound in the old namespace.
|
|||
- the child spawn/bootstrap trampoline calls `setns()` **before**
|
||||
`_runtime.async_main()`, `IPCServer.listen_on()`, parent-channel
|
||||
connection, or creation of any worker thread/socket.
|
||||
- `spawn._netns.enter_netns()` is the first private bootstrap
|
||||
primitive: it checks the inherited FD against the expected inode,
|
||||
calls `setns(fd, CLONE_NEWNET)`, and verifies
|
||||
`/proc/self/ns/net` before returning. It deliberately does not own
|
||||
or close the FD; spawn propagation and status reporting remain the
|
||||
caller's next integration boundary.
|
||||
- only after successful entry does the child drop namespace-entry
|
||||
privileges and initialize the actor runtime.
|
||||
- a root/single-actor process follows the same ordering: enter during
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
'''
|
||||
Pre-runtime Linux network-namespace entry validation.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import BinaryIO
|
||||
|
||||
import pytest
|
||||
|
||||
from tractor.spawn import _netns
|
||||
|
||||
|
||||
def test_enter_netns_rejects_mismatched_inherited_fd(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
'''
|
||||
A stale inherited FD must not enter a replacement namespace.
|
||||
|
||||
Open a real stand-in FD, declare a different expected inode and
|
||||
replace `os.setns()` with a failure sentinel. The inode check must
|
||||
reject the capability before any irreversible namespace entry.
|
||||
|
||||
'''
|
||||
token_path: Path = tmp_path / 'netns'
|
||||
token_path.touch()
|
||||
|
||||
def fail_setns(namespace_fd: int, nstype: int) -> None:
|
||||
raise AssertionError('`setns()` must not be called')
|
||||
|
||||
monkeypatch.setattr(_netns.os, 'setns', fail_setns)
|
||||
namespace_file: BinaryIO
|
||||
with token_path.open('rb') as namespace_file:
|
||||
inode: int = token_path.stat().st_ino
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=f'{inode}.*{inode + 1}',
|
||||
):
|
||||
_netns.enter_netns(
|
||||
namespace_file.fileno(),
|
||||
# Deliberately differ from `token_path`'s inode.
|
||||
inode + 1,
|
||||
)
|
||||
|
||||
|
||||
def test_enter_netns_verifies_post_entry_inode(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
'''
|
||||
Successful `setns()` is insufficient without post-entry proof.
|
||||
|
||||
Use a real inherited FD and fake only the privileged syscall and
|
||||
`/proc/self/ns/net` observation. The recorded calls prove both
|
||||
hooks execute and `CLONE_NEWNET` constrains the namespace type;
|
||||
the returned inode proves bootstrap observed the expected netns.
|
||||
|
||||
'''
|
||||
token_path: Path = tmp_path / 'netns'
|
||||
token_path.touch()
|
||||
setns_calls: list[tuple[int, int]] = []
|
||||
stat_calls: list[Path] = []
|
||||
|
||||
def fake_setns(namespace_fd: int, nstype: int) -> None:
|
||||
setns_calls.append((namespace_fd, nstype))
|
||||
|
||||
def fake_stat(path: Path) -> SimpleNamespace:
|
||||
stat_calls.append(path)
|
||||
return SimpleNamespace(st_ino=inode)
|
||||
|
||||
namespace_file: BinaryIO
|
||||
with token_path.open('rb') as namespace_file:
|
||||
namespace_fd: int = namespace_file.fileno()
|
||||
inode: int = token_path.stat().st_ino
|
||||
monkeypatch.setattr(_netns.os, 'setns', fake_setns)
|
||||
monkeypatch.setattr(
|
||||
type(_netns._SELF_NETNS),
|
||||
'stat',
|
||||
fake_stat,
|
||||
)
|
||||
|
||||
entered_inode: int = _netns.enter_netns(
|
||||
namespace_fd,
|
||||
inode,
|
||||
)
|
||||
|
||||
assert setns_calls == [
|
||||
(namespace_fd, _netns.os.CLONE_NEWNET),
|
||||
]
|
||||
assert stat_calls == [_netns._SELF_NETNS]
|
||||
assert entered_inode == inode
|
||||
|
||||
|
||||
def test_enter_netns_rejects_wrong_post_entry_namespace(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
'''
|
||||
Bootstrap must stop when the process lands in an unexpected netns.
|
||||
|
||||
Let the inherited FD check and fake syscall succeed, then report a
|
||||
different `/proc/self/ns/net` inode. The post-entry guard must raise
|
||||
instead of allowing actor runtime sockets to start in the wrong
|
||||
namespace.
|
||||
|
||||
'''
|
||||
token_path: Path = tmp_path / 'netns'
|
||||
token_path.touch()
|
||||
|
||||
def fake_setns(namespace_fd: int, nstype: int) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
_netns.os,
|
||||
'setns',
|
||||
fake_setns,
|
||||
)
|
||||
|
||||
namespace_file: BinaryIO
|
||||
with token_path.open('rb') as namespace_file:
|
||||
inode: int = token_path.stat().st_ino
|
||||
|
||||
def fake_stat(path: Path) -> SimpleNamespace:
|
||||
return SimpleNamespace(st_ino=inode + 1)
|
||||
|
||||
monkeypatch.setattr(
|
||||
type(_netns._SELF_NETNS),
|
||||
'stat',
|
||||
fake_stat,
|
||||
)
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match=f'{inode + 1}.*{inode}',
|
||||
):
|
||||
_netns.enter_netns(
|
||||
namespace_file.fileno(),
|
||||
# Deliberately differ from `fake_stat()`'s inode + 1.
|
||||
inode,
|
||||
)
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
# tractor: structured concurrent "actors".
|
||||
# Copyright 2018-eternity Tyler Goodlet.
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
Linux network-namespace actor-bootstrap primitives.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
_SELF_NETNS: Path = Path('/proc/self/ns/net')
|
||||
|
||||
|
||||
def enter_netns(
|
||||
namespace_fd: int,
|
||||
expected_inode: int,
|
||||
) -> int:
|
||||
'''
|
||||
Enter and verify one inherited Linux network namespace.
|
||||
|
||||
The future spawn-bootstrap caller owns and closes `namespace_fd`.
|
||||
|
||||
'''
|
||||
if sys.platform != 'linux':
|
||||
raise RuntimeError(
|
||||
'Network namespace entry is Linux-only!'
|
||||
)
|
||||
if (
|
||||
type(namespace_fd) is not int
|
||||
or
|
||||
namespace_fd < 0
|
||||
):
|
||||
raise ValueError(
|
||||
'`namespace_fd` must be a non-negative `int`!'
|
||||
)
|
||||
if (
|
||||
type(expected_inode) is not int
|
||||
or
|
||||
expected_inode <= 0
|
||||
):
|
||||
raise ValueError(
|
||||
'`expected_inode` must be a positive `int`!'
|
||||
)
|
||||
|
||||
setns: Callable[[int, int], None]|None = getattr(
|
||||
os,
|
||||
'setns',
|
||||
None,
|
||||
)
|
||||
clone_newnet: int|None = getattr(
|
||||
os,
|
||||
'CLONE_NEWNET',
|
||||
None,
|
||||
)
|
||||
if (
|
||||
setns is None
|
||||
or
|
||||
clone_newnet is None
|
||||
):
|
||||
raise RuntimeError(
|
||||
'Python has no Linux network namespace entry support!'
|
||||
)
|
||||
|
||||
inherited_inode: int = os.fstat(namespace_fd).st_ino
|
||||
if inherited_inode != expected_inode:
|
||||
raise ValueError(
|
||||
f'Inherited namespace FD inode {inherited_inode} does not '
|
||||
f'match expected inode {expected_inode}!'
|
||||
)
|
||||
|
||||
try:
|
||||
setns(namespace_fd, clone_newnet)
|
||||
except OSError as exc:
|
||||
raise RuntimeError(
|
||||
f'Could not enter network namespace inode '
|
||||
f'{expected_inode}!'
|
||||
) from exc
|
||||
|
||||
entered_inode: int = _SELF_NETNS.stat().st_ino
|
||||
if entered_inode != expected_inode:
|
||||
raise RuntimeError(
|
||||
f'Entered network namespace inode {entered_inode} does not '
|
||||
f'match expected inode {expected_inode}!'
|
||||
)
|
||||
|
||||
return entered_inode
|
||||
Loading…
Reference in New Issue