Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import json
import os
import secrets
import shutil
import signal
import socket
Expand Down Expand Up @@ -117,6 +118,7 @@ def __init__(
self._log_path: str | None = None
self._log_file: "typing.IO[bytes] | None" = None
self._proc: subprocess.Popen[bytes] | None = None
self._auth_secret: str | None = None

@property
def uds_path(self) -> str:
Expand All @@ -140,6 +142,12 @@ def pid(self) -> int:
def is_running(self) -> bool:
return self._proc is not None and self._proc.poll() is None

@property
def auth_secret(self) -> str:
if self._auth_secret is None:
raise RuntimeError("AcceleratorDaemon has not been started")
return self._auth_secret

def start(self) -> None:
"""Spawn the daemon and wait for the UDS to become connectable."""
if self._proc is not None:
Expand Down Expand Up @@ -175,6 +183,21 @@ def start(self) -> None:
raise RuntimeError(
f"Failed to spawn accelerator daemon at {self._binary_path}: {exc}"
) from exc
# Mint a 256-bit secret and write it to the daemon's stdin before it
# binds the socket. The daemon reads this line synchronously before
# accepting connections, then validates it on every RPC via metadata.
self._auth_secret = secrets.token_urlsafe(32)
try:
self._proc.stdin.write(f"{self._auth_secret}\n".encode()) # type: ignore[union-attr]
self._proc.stdin.flush() # type: ignore[union-attr]
except (OSError, ValueError) as exc:
self._close_log_file()
self._force_kill()
self._cleanup_tempdir()
raise RuntimeError(
"Failed to send auth secret to accelerator daemon (process died): "
f"{exc}"
) from exc
# The child inherited its own dup of the log fd; the parent no longer
# needs its copy. Startup failures read the tail back from the path.
self._close_log_file()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ class _AsyncAcceleratorClient:
(``_accelerator/_routing.py``) decides which calls reach this object.
"""

def __init__(self, uds_path: str):
def __init__(self, uds_path: str, auth_secret: str):
self._uds_path = uds_path
self._metadata = (("x-accelerator-token", auth_secret),)
self._channel = insecure_channel(f"unix://{uds_path}")
self._mutate_row_stub = self._channel.unary_unary(
_MUTATE_ROW_METHOD,
Expand All @@ -71,7 +72,9 @@ def uds_path(self) -> str:
async def mutate_row(
self, request: MutateRowRequest, *, timeout: float | None = None
) -> MutateRowResponse:
return await self._mutate_row_stub(request, timeout=timeout)
return await self._mutate_row_stub(
request, timeout=timeout, metadata=self._metadata
)

@CrossSync.convert
async def read_rows(
Expand All @@ -84,7 +87,7 @@ async def read_rows(
Shape matches what ``_gapic_client.read_rows`` returns so the existing
chunk-merging machinery in ``_read_rows.py`` works unchanged.
"""
return self._read_rows_stub(request, timeout=timeout)
return self._read_rows_stub(request, timeout=timeout, metadata=self._metadata)

@CrossSync.convert
async def close(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1379,7 +1379,9 @@ def _start_accelerator(self) -> None:
# the daemon's independent ADC resolution could land on a different
# principal. Verify before routing any RPC through it.
self._verify_daemon_identity(server)
self._accelerator_client = AcceleratorClientType(server.uds_path)
self._accelerator_client = AcceleratorClientType(
server.uds_path, server.auth_secret
)
except _AcceleratorUnverified:
server.close()
warnings.warn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ class _AcceleratorClient:
(``_accelerator/_routing.py``) decides which calls reach this object.
"""

def __init__(self, uds_path: str):
def __init__(self, uds_path: str, auth_secret: str):
self._uds_path = uds_path
self._metadata = (("x-accelerator-token", auth_secret),)
self._channel = insecure_channel(f"unix://{uds_path}")
self._mutate_row_stub = self._channel.unary_unary(
_MUTATE_ROW_METHOD,
Expand All @@ -66,7 +67,7 @@ def uds_path(self) -> str:
def mutate_row(
self, request: MutateRowRequest, *, timeout: float | None = None
) -> MutateRowResponse:
return self._mutate_row_stub(request, timeout=timeout)
return self._mutate_row_stub(request, timeout=timeout, metadata=self._metadata)

def read_rows(self, request: ReadRowsRequest, *, timeout: float | None = None):
"""Open the server-streaming ReadRows RPC against the daemon.
Expand All @@ -75,7 +76,7 @@ def read_rows(self, request: ReadRowsRequest, *, timeout: float | None = None):
in async, ``for`` in sync) to consume ``ReadRowsResponse`` messages.
Shape matches what ``_gapic_client.read_rows`` returns so the existing
chunk-merging machinery in ``_read_rows.py`` works unchanged."""
return self._read_rows_stub(request, timeout=timeout)
return self._read_rows_stub(request, timeout=timeout, metadata=self._metadata)

def close(self) -> None:
self._channel.close()
Original file line number Diff line number Diff line change
Expand Up @@ -1076,7 +1076,9 @@ def _start_accelerator(self) -> None:
try:
server.start()
self._verify_daemon_identity(server)
self._accelerator_client = AcceleratorClientType(server.uds_path)
self._accelerator_client = AcceleratorClientType(
server.uds_path, server.auth_secret
)
except _AcceleratorUnverified:
server.close()
warnings.warn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,108 @@ def test_missing_file_raises(self, tmp_path):
daemon.read_identity()


class TestAuthSecret:
def test_raises_before_start(self, tmp_path):
daemon = _make_daemon(tmp_path)
with pytest.raises(RuntimeError, match="has not been started"):
_ = daemon.auth_secret

def test_written_to_stdin_after_popen(self, tmp_path, monkeypatch):
"""start() mints a secret and writes 'secret\n' to the daemon's stdin."""
written = []

class FakeStdin:
def write(self, data):
written.append(data)

def flush(self):
pass

class FakeProc:
pid = 12345
stdin = FakeStdin()

def poll(self):
return None

def fake_popen(argv, **kwargs):
raise _StopStart()

monkeypatch.setattr(_daemon.subprocess, "Popen", fake_popen)
daemon = _make_daemon(tmp_path)
with pytest.raises(_StopStart):
daemon.start()
# Popen raises _StopStart before secret is written; check that the
# secret is written when Popen succeeds by using a real pipe below.

def test_secret_written_to_stdin(self, tmp_path, monkeypatch):
"""The secret is urlsafe and ends with a newline on the wire."""
written = bytearray()

class FakeStdin:
def write(self, data):
written.extend(data)

def flush(self):
pass

class FakeProc:
pid = 12345
stdin = FakeStdin()

def poll(self):
return None

def fake_popen(argv, **kwargs):
return FakeProc()

monkeypatch.setattr(_daemon.subprocess, "Popen", fake_popen)
daemon = _make_daemon(tmp_path)
# _wait_until_ready will fail immediately (process is fake); we just
# need to get past the stdin-write step, so patch _wait_until_ready.
monkeypatch.setattr(daemon, "_wait_until_ready", lambda: None)
daemon.start()
payload = written.decode()
assert payload.endswith("\n"), "secret payload must end with newline"
token = payload.rstrip("\n")
assert token == daemon.auth_secret
# token_urlsafe(32) produces ≥32 chars of base64url characters.
assert len(token) >= 32
import re

assert re.fullmatch(r"[A-Za-z0-9_\-]+", token), "expected urlsafe token"

def test_broken_pipe_kills_and_reraises(self, tmp_path, monkeypatch):
"""A broken pipe during secret write force-kills the daemon and raises."""

class FakeStdin:
def write(self, data):
raise OSError("broken pipe")

def flush(self):
pass

class FakeProc:
pid = 12345
stdin = FakeStdin()

def poll(self):
return None

def fake_popen(argv, **kwargs):
return FakeProc()

killed = []
monkeypatch.setattr(_daemon.subprocess, "Popen", fake_popen)
monkeypatch.setattr(
AcceleratorDaemon, "_force_kill", lambda self: killed.append(True)
)
daemon = _make_daemon(tmp_path)
with pytest.raises(RuntimeError, match="auth secret"):
daemon.start()
assert killed, "_force_kill should have been called on broken pipe"


class _StopStart(Exception):
"""Sentinel to abort AcceleratorDaemon.start() right after Popen so the test
never waits on a real socket."""
Expand Down
Loading