◐ Off-By-One · answer catalog

python-packaging-path-resolution

2 answer(s)godockergodocker

plugintree = os.path.join(candidate, "plugin", "terminaljail")

📦 Source in repository (JSON)

Answer 1

Root cause. seccomp-loader._setup_path() hard-coded repo_root = dirname(dirname(loader)), which only holds for the repo layout (<root>/standalone/seccomp-loader.py + <root>/plugin/). install.sh flattens the loader into <prefix>/lib/terminal-jail/seccomp-loader.py with the plugin tree at <prefix>/lib/terminal-jail/plugin/, so the old guess resolved to <prefix>/lib and the from terminal_jail.seccomp import … failed with ModuleNotFoundError: No module named 'terminal_jail' (reproduced before fixing).

Fix (~/terminal-jail/standalone/seccomp-loader.py): walk up from the loader's real directory until a directory containing plugin/terminal_jail/ (or a bare terminal_jail package) is found, and insert the plugin-hosting dir into sys.path. Works for both layouts, is bounded at the filesystem root, and resolves symlinks so /usr/local/bin shims still work:

def _setup_path() -> None:
    """...works for repo layout (<root>/standalone/ + <root>/plugin/)
    and installed layout (<prefix>/lib/terminal-jail/ + .../plugin/)."""

    def _find_plugin_root(candidate: str) -> str | None:
        # Layout A/B: <candidate>/plugin/terminal_jail/__init__.py
        plugin_tree = os.path.join(candidate, "plugin", "terminal_jail")
        if os.path.isfile(os.path.join(plugin_tree, "__init__.py")):
            return os.path.join(candidate, "plugin")
        # Layout C: bare package <candidate>/terminal_jail/__init__.py
        bare_pkg = os.path.join(candidate, "terminal_jail")
        if os.path.isfile(os.path.join(bare_pkg, "__init__.py")):
            return candidate
        return None

    loader_dir = os.path.dirname(os.path.realpath(__file__))
    current = loader_dir
    while True:
        plugin_root = _find_plugin_root(current)
        if plugin_root is not None:
            if plugin_root not in sys.path:
                sys.path.insert(0, plugin_root)
            return
        parent = os.path.dirname(current)
        if parent == current:          # reached filesystem root
            break
        current = parent

    # Unrecognised layout: fall back to the historical guess.
    repo_root = os.path.dirname(loader_dir)
    plugin_dir = os.path.join(repo_root, "plugin")
    if plugin_dir not in sys.path:
        sys.path.insert(0, plugin_dir)

Also added ~/terminal-jail/install.sh (ships lib/terminal-jail/seccomp-loader.py, lib/terminal-jail/terminal-jail, and lib/terminal-jail/plugin/…, plus a bin/terminal-jail symlink) and the regression suite tests/test_installed_layout.py (5 tests). Per the problem note, probes invoke the loader with the command directly (echo <marker>) — never --user/--seccomp — because the bash wrapper strips its own flags before the loader sees them; the loader execs argv[1] verbatim.

Evidence & signatures

**Regression reproduced (pre-fix):** simulating the installed layout and running with a stripped environment gave exactly the reported failure:
```
ModuleNotFoundError: No module named 'terminal_jail'   (exit=1)
```

**Post-fix verification** — `install.sh <tmp_prefix>`, then `env -i PATH=… python3 <prefix>/lib/terminal-jail/seccomp-loader.py echo …` (strictest possible: no `PYTHONPATH`, no inherited env):
- Installed layout: `installed-ok`, exit 0 (no ModuleNotFoundError, command output present) — the exact regression scenario from the problem.
- Repo layout: `repo-ok`, exit 0 (no regression).
- `install.sh` also works invoked from another cwd via a relative path.

**Test-of-the-test:** with the old buggy `_setup_path` swapped back in, the suite fails 4/5 with the reported `ModuleNotFoundError` (the one passing test is the repo layout — precisely why the bug went unnoticed). With the fix, **5/5 pass**:
1. `test_installed_layout_loader_finds_plugin` — main regression (install into tmp_path → strip PYTHONPATH → run loader → no ModuleNotFoundError + output)
2. `test_installed_layout_setup_path_puts_plugin_dir_on_sys_path` — unit check that `_setup_path` puts `<prefix>/lib/terminal-jail/plugin` on `sys.path` and `import terminal_jail.seccomp` succeeds
3. `test_repo_layout_loader_still_works` — old layout preserved
4. `test_symlinked_loader_finds_plugin` — realpath resolution through a shim symlink
5. `test_loader_contract_command_direct_not_wrapper_flags` — passing `--user` to the loader execs a nonexistent binary (`FileNotFoundError`), pinning the "loader takes the command directly" contract

**Edge cases / observations:**
- **`TERMINAL_JAIL_SECCOMP=1`:** the probe dies with SIGSYS (exit 159) — but this is **pre-existing and identical on the untouched original loader** (`/usr/local/lib/terminal-jail`), i.e. a property of the hand-built BPF filter in this kernel, orthogonal to path resolution. Out of scope; the mandated regression test uses the default (seccomp-off) path, which passes cleanly.
- `unshare` is `EPERM` in this container, so wrapper-level probes aren't runnable here; the problem-specified test targets the loader directly, which is what the suite does.
- `install.sh` prunes `__pycache__` at copy time (runtime pyc regeneration afterwards is expected Python behavior).
{"model": "deepseek-v4-flash", "problem_class": "python-packaging-path-resolution", "result": "passed", "tests": 5}

Answer 2

Root cause. seccomp-loader._setup_path() hard-coded repo_root = dirname(dirname(loader)), which only holds for the repo layout (<root>/standalone/seccomp-loader.py + <root>/plugin/). install.sh flattens the loader into <prefix>/lib/terminal-jail/seccomp-loader.py with the plugin tree at <prefix>/lib/terminal-jail/plugin/, so the old guess resolved to <prefix>/lib and the from terminal_jail.seccomp import … failed with ModuleNotFoundError: No module named 'terminal_jail' (reproduced before fixing).

Fix (~/terminal-jail/standalone/seccomp-loader.py): walk up from the loader's real directory until a directory containing plugin/terminal_jail/ (or a bare terminal_jail package) is found, and insert the plugin-hosting dir into sys.path. Works for both layouts, is bounded at the filesystem root, and resolves symlinks so /usr/local/bin shims still work:

def _setup_path() -> None:
    """...works for repo layout (<root>/standalone/ + <root>/plugin/)
    and installed layout (<prefix>/lib/terminal-jail/ + .../plugin/)."""

    def _find_plugin_root(candidate: str) -> str | None:
        # Layout A/B: <candidate>/plugin/terminal_jail/__init__.py
        plugin_tree = os.path.join(candidate, "plugin", "terminal_jail")
        if os.path.isfile(os.path.join(plugin_tree, "__init__.py")):
            return os.path.join(candidate, "plugin")
        # Layout C: bare package <candidate>/terminal_jail/__init__.py
        bare_pkg = os.path.join(candidate, "terminal_jail")
        if os.path.isfile(os.path.join(bare_pkg, "__init__.py")):
            return candidate
        return None

    loader_dir = os.path.dirname(os.path.realpath(__file__))
    current = loader_dir
    while True:
        plugin_root = _find_plugin_root(current)
        if plugin_root is not None:
            if plugin_root not in sys.path:
                sys.path.insert(0, plugin_root)
            return
        parent = os.path.dirname(current)
        if parent == current:          # reached filesystem root
            break
        current = parent

    # Unrecognised layout: fall back to the historical guess.
    repo_root = os.path.dirname(loader_dir)
    plugin_dir = os.path.join(repo_root, "plugin")
    if plugin_dir not in sys.path:
        sys.path.insert(0, plugin_dir)

Also added ~/terminal-jail/install.sh (ships lib/terminal-jail/seccomp-loader.py, lib/terminal-jail/terminal-jail, and lib/terminal-jail/plugin/…, plus a bin/terminal-jail symlink) and the regression suite tests/test_installed_layout.py (5 tests). Per the problem note, probes invoke the loader with the command directly (echo <marker>) — never --user/--seccomp — because the bash wrapper strips its own flags before the loader sees them; the loader execs argv[1] verbatim.

Evidence & signatures

**Regression reproduced (pre-fix):** simulating the installed layout and running with a stripped environment gave exactly the reported failure:
```
ModuleNotFoundError: No module named 'terminal_jail'   (exit=1)
```

**Post-fix verification** — `install.sh <tmp_prefix>`, then `env -i PATH=… python3 <prefix>/lib/terminal-jail/seccomp-loader.py echo …` (strictest possible: no `PYTHONPATH`, no inherited env):
- Installed layout: `installed-ok`, exit 0 (no ModuleNotFoundError, command output present) — the exact regression scenario from the problem.
- Repo layout: `repo-ok`, exit 0 (no regression).
- `install.sh` also works invoked from another cwd via a relative path.

**Test-of-the-test:** with the old buggy `_setup_path` swapped back in, the suite fails 4/5 with the reported `ModuleNotFoundError` (the one passing test is the repo layout — precisely why the bug went unnoticed). With the fix, **5/5 pass**:
1. `test_installed_layout_loader_finds_plugin` — main regression (install into tmp_path → strip PYTHONPATH → run loader → no ModuleNotFoundError + output)
2. `test_installed_layout_setup_path_puts_plugin_dir_on_sys_path` — unit check that `_setup_path` puts `<prefix>/lib/terminal-jail/plugin` on `sys.path` and `import terminal_jail.seccomp` succeeds
3. `test_repo_layout_loader_still_works` — old layout preserved
4. `test_symlinked_loader_finds_plugin` — realpath resolution through a shim symlink
5. `test_loader_contract_command_direct_not_wrapper_flags` — passing `--user` to the loader execs a nonexistent binary (`FileNotFoundError`), pinning the "loader takes the command directly" contract

**Edge cases / observations:**
- **`TERMINAL_JAIL_SECCOMP=1`:** the probe dies with SIGSYS (exit 159) — but this is **pre-existing and identical on the untouched original loader** (`/usr/local/lib/terminal-jail`), i.e. a property of the hand-built BPF filter in this kernel, orthogonal to path resolution. Out of scope; the mandated regression test uses the default (seccomp-off) path, which passes cleanly.
- `unshare` is `EPERM` in this container, so wrapper-level probes aren't runnable here; the problem-specified test targets the loader directly, which is what the suite does.
- `install.sh` prunes `__pycache__` at copy time (runtime pyc regeneration afterwards is expected Python behavior).
{"model": "deepseek-v4-flash", "problem_class": "python-packaging-path-resolution", "result": "passed", "tests": 5}
Generated from the verified corpus · MIT licensedBack to the catalog