Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/providers/docker_sbx/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,33 @@
import fcntl
import os
import pty
import shlex
import struct
import termios

from providers.base import SandboxProcess, TerminalSize
from providers.exceptions import ProviderTransportError

from .settings import DockerSbxSettings


def _set_terminal_size(descriptor: int, size: TerminalSize) -> None:
winsize = struct.pack("HHHH", size.rows, size.columns, 0, 0)
fcntl.ioctl(descriptor, termios.TIOCSWINSZ, winsize)


def _session_script(name: str, command: str | None) -> str:
def _session_script(name: str, command: str | None, user: str) -> str:
# The exec enters as root and prepares the per-host home. For a non-root
# user it then drops with `su -m`, which keeps the exported HOME and,
# through PAM, still applies /etc/environment.
home = f"/home/{name}"
payload = command if command is not None else "exec bash -l"
return f"mkdir -p {home} && cd {home} && export HOME={home}\n{payload}"
prepare = f"mkdir -p {home} && cd {home} && export HOME={home}"
if user == "root":
return f"{prepare}\n{payload}"
owner = shlex.quote(user)
drop = f"exec su -m {owner} -s /bin/bash -c {shlex.quote(payload)}"
return f"{prepare} && chown {owner} {home}\n{drop}"


class SbxExecProcess(SandboxProcess):
Expand Down Expand Up @@ -48,10 +59,11 @@ async def open(
command: str | None,
terminal: TerminalSize | None,
) -> "SbxExecProcess":
user = DockerSbxSettings().ssh_username
argv = ["sbx", "exec", "--interactive"]
if terminal:
argv.append("--tty")
argv.extend([name, "bash", "-l", "-c", _session_script(name, command)])
argv.extend([name, "bash", "-l", "-c", _session_script(name, command, user)])
environment = {**os.environ, "SBX_NO_TELEMETRY": "1"}

try:
Expand Down
56 changes: 52 additions & 4 deletions src/providers/docker_sbx/tests/test_process.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import os
import shlex

import pytest

Expand Down Expand Up @@ -48,7 +49,7 @@ async def test_exec_session_bridges_pipes_in_both_directions(monkeypatch):
"-l",
"-c",
)
assert captured["argv"][7] == _session_script("sb-test", "true")
assert captured["argv"][7] == _session_script("sb-test", "true", "root")
assert output == b"round-trip"
assert status == 5

Expand Down Expand Up @@ -85,7 +86,7 @@ async def test_terminal_request_allocates_a_real_pty_at_the_requested_size(monke

assert "--tty" in captured["argv"]
assert captured["argv"][-4:-1] == ("bash", "-l", "-c")
assert captured["argv"][-1] == _session_script("sb-test", None)
assert captured["argv"][-1] == _session_script("sb-test", None, "root")
assert b"ISTTY" in output
assert b"42 101" in output
assert status == 3
Expand All @@ -109,7 +110,7 @@ async def test_aclose_releases_the_terminal_descriptor(monkeypatch):


def test_session_prepares_the_per_host_home_before_the_command():
script = _session_script("sb-abc", "git status")
script = _session_script("sb-abc", "git status", "root")
assert "mkdir -p /home/sb-abc" in script
assert "export HOME=/home/sb-abc" in script
# The home exists and HOME is set before the command runs.
Expand All @@ -118,11 +119,58 @@ def test_session_prepares_the_per_host_home_before_the_command():


def test_session_without_a_command_runs_a_login_shell_in_the_home():
script = _session_script("sb-abc", None)
script = _session_script("sb-abc", None, "root")
assert "mkdir -p /home/sb-abc" in script
assert script.endswith("exec bash -l")


def test_root_session_is_byte_identical_to_the_plain_home_setup():
# The default install must not change: no chown, no su.
assert _session_script("sb-abc", "git status", "root") == (
"mkdir -p /home/sb-abc && cd /home/sb-abc && export HOME=/home/sb-abc\ngit status"
)
assert "su" not in _session_script("sb-abc", None, "root")


def test_non_root_session_chowns_the_home_and_drops_to_the_user():
script = _session_script("sb-abc", "agent run --flag", "druks")
# Root prepares and gives the user the home before it drops.
assert "chown druks /home/sb-abc" in script
assert script.index("mkdir -p /home/sb-abc") < script.index("su -m druks")
assert script.index("chown druks") < script.index("su -m druks")
# su -m keeps the exported HOME; the payload is quoted for /bin/bash -c.
assert f"exec su -m druks -s /bin/bash -c {shlex.quote('agent run --flag')}" in script


def test_non_root_interactive_session_drops_to_a_login_shell():
script = _session_script("sb-abc", None, "druks")
assert f"exec su -m druks -s /bin/bash -c {shlex.quote('exec bash -l')}" in script


def test_non_root_sftp_backing_shell_runs_as_the_user():
# The SFTP backend passes its server command through the same open();
# thus its backing shell drops to the user too.
script = _session_script("sb-abc", "exec /usr/lib/openssh/sftp-server", "druks")
assert "exec su -m druks -s /bin/bash -c" in script
assert shlex.quote("exec /usr/lib/openssh/sftp-server") in script


async def test_open_runs_the_session_as_the_configured_user(monkeypatch):
monkeypatch.setenv("DOCKER_SBX_SSH_USERNAME", "druks")
captured: dict = {}
monkeypatch.setattr(
"providers.docker_sbx.process.asyncio.create_subprocess_exec",
_stub_sbx_with("exit 0", captured),
)

session = await SbxExecProcess.open("sb-test", command="id -un", terminal=None)
await session.wait()
await session.aclose()

assert captured["argv"][7] == _session_script("sb-test", "id -un", "druks")
assert "exec su -m druks" in captured["argv"][7]


async def test_missing_sbx_binary_maps_to_transport_error(monkeypatch):
async def fail_exec(*argv, **kwargs):
raise FileNotFoundError("sbx")
Expand Down