Skip to content
Draft
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
92 changes: 86 additions & 6 deletions sdk/typescript/_bundled_plugin/scripts/finalize_scan_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,24 +326,56 @@ def _windows_scan_local_files() -> Any:
return _WINDOWS_SCAN_LOCAL_FILES


def _open_verified_scan_directory(scan_dir: Path) -> int:
def _open_verified_scan_directory(
scan_dir: Path, expected_root_identity: tuple[int, int] | None = None
) -> int:
scan_dir = scan_dir.absolute()
try:
expected = scan_dir.lstat()
observed_identity = (expected.st_dev, expected.st_ino)
if (
expected_root_identity is not None
and observed_identity != expected_root_identity
):
raise ContractError("scan directory: changed after artifact restoration setup")
canonical = _require_scan_directory(scan_dir)
descriptor = os.open(
canonical,
os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
)
except OSError as exc:
raise ContractError("scan directory: expected an existing non-symlink directory") from exc
raise ContractError(
"scan directory: expected an existing non-symlink directory"
) from exc
opened = os.fstat(descriptor)
if (opened.st_dev, opened.st_ino) != (expected.st_dev, expected.st_ino):
opened_identity = (opened.st_dev, opened.st_ino)
if opened_identity != observed_identity or (
expected_root_identity is not None
and opened_identity != expected_root_identity
):
os.close(descriptor)
raise ContractError("scan directory: changed while it was being opened")
return descriptor


def scan_root_identity(scan_dir: Path) -> tuple[Path, tuple[int, int]]:
"""Return a canonical scan root and its identity from a held handle."""

scan_dir = _require_scan_directory(scan_dir)
if not _descriptor_relative_writes_available():
if not _is_windows():
raise ContractError(
"scan-local output requires descriptor-relative file operations"
)
return _windows_scan_local_files().scan_root_identity(scan_dir)
descriptor = _open_verified_scan_directory(scan_dir)
try:
metadata = os.fstat(descriptor)
return scan_dir, (metadata.st_dev, metadata.st_ino)
finally:
os.close(descriptor)


def _open_scan_local_directory(root_fd: int, parts: tuple[str, ...], *, create: bool) -> int:
descriptor = os.dup(root_fd)
try:
Expand Down Expand Up @@ -492,7 +524,12 @@ def _sha256_scan_local_file(scan_dir: Path, relative_path: str, context: str) ->


def write_scan_local_bytes(
scan_dir: Path, relative_path: str, payload: bytes, *, external_name: bool = False
scan_dir: Path,
relative_path: str,
payload: bytes,
*,
external_name: bool = False,
expected_root_identity: tuple[int, int] | None = None,
) -> None:
scan_dir = _require_scan_directory(scan_dir)
if external_name:
Expand All @@ -505,29 +542,72 @@ def write_scan_local_bytes(
if not _is_windows():
raise ContractError("scan-local output requires descriptor-relative file operations")
try:
_windows_scan_local_files().atomic_write(scan_dir, relative_path, payload)
_windows_scan_local_files().atomic_write(
scan_dir,
relative_path,
payload,
expected_root_identity=expected_root_identity,
)
except OSError as exc:
raise ContractError(f"{relative_path}: {exc}") from exc
return
root_fd: int | None = None
parent_fd: int | None = None
temp_name: str | None = None
try:
root_fd = _open_verified_scan_directory(scan_dir)
root_fd = _open_verified_scan_directory(scan_dir, expected_root_identity)
parts = PurePosixPath(relative_path).parts
try:
parent_fd = _open_scan_local_directory(root_fd, parts[:-1], create=True)
except OSError as exc:
raise ContractError(
f"{relative_path}: expected a path inside the scan directory"
) from exc
# The held descriptor is the authority for the validated parent. A
# concurrent rename cannot redirect later operations through a
# replacement path or link.
try:
metadata = os.stat(parts[-1], dir_fd=parent_fd, follow_symlinks=False)
except FileNotFoundError:
pass
else:
if not stat.S_ISREG(metadata.st_mode):
raise ContractError(f"{relative_path}: expected a regular non-symlink file")
try:
existing_fd = os.open(
parts[-1],
os.O_RDONLY
| getattr(os, "O_NOFOLLOW", 0)
| getattr(os, "O_NONBLOCK", 0),
dir_fd=parent_fd,
)
except OSError as exc:
if exc.errno not in {errno.ENOENT, errno.EACCES, errno.EPERM}:
raise
else:
try:
opened = os.fstat(existing_fd)
if not stat.S_ISREG(opened.st_mode):
raise ContractError(
f"{relative_path}: expected a regular non-symlink file"
)
if (opened.st_dev, opened.st_ino) != (
metadata.st_dev,
metadata.st_ino,
):
raise ContractError(
f"{relative_path}: changed while it was being opened"
)
try:
with os.fdopen(existing_fd, "rb") as handle:
existing_fd = -1
if handle.read() == payload:
return
except OSError:
pass
finally:
if existing_fd >= 0:
os.close(existing_fd)
temp_name = f".{path.name}.{secrets.token_hex(8)}.tmp"
temp_fd = os.open(temp_name, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600, dir_fd=parent_fd)
with os.fdopen(temp_fd, "wb") as handle:
Expand Down
88 changes: 83 additions & 5 deletions sdk/typescript/_bundled_plugin/scripts/windows_scan_local_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class WindowsScanLocalFileError(OSError):
_FILE_NAME_OPENED = 0x00000008
_ERROR_FILE_NOT_FOUND = 2
_ERROR_PATH_NOT_FOUND = 3
_ERROR_ACCESS_DENIED = 5
_ERROR_SHARING_VIOLATION = 32
_ERROR_LOCK_VIOLATION = 33
_ERROR_FILE_EXISTS = 80
_ERROR_ALREADY_EXISTS = 183
_MISSING_ERRORS = {_ERROR_FILE_NOT_FOUND, _ERROR_PATH_NOT_FOUND}
Expand Down Expand Up @@ -398,12 +401,20 @@ def _locked_parent(
relative_path: str,
*,
create: bool,
expected_root_identity: tuple[int, int] | None = None,
) -> Iterator[tuple[Path, str]]:
"""Hold non-deletable handles for every directory in the absolute target path."""

_require_windows()
parts = _validated_parts(relative_path)
root_path, expected_root_identity = _canonical_scan_directory(scan_dir)
root_path, observed_root_identity = _canonical_scan_directory(scan_dir)
if (
expected_root_identity is not None
and observed_root_identity != expected_root_identity
):
raise _invalid_path(
scan_dir, "scan directory changed after artifact restoration setup"
)
handles: list[_OwnedHandle] = []
try:
# Absolute-path Win32 calls remain safe only while every ancestor is
Expand All @@ -414,8 +425,14 @@ def _locked_parent(
assert directory_handle is not None
handles.append(directory_handle)
current_root = root_path.lstat()
if (current_root.st_dev, current_root.st_ino) != expected_root_identity:
raise _invalid_path(scan_dir, "scan directory changed while it was being opened")
current_root_identity = (current_root.st_dev, current_root.st_ino)
if current_root_identity != observed_root_identity or (
expected_root_identity is not None
and current_root_identity != expected_root_identity
):
raise _invalid_path(
scan_dir, "scan directory changed while it was being opened"
)
current_path = root_path
for component in parts[:-1]:
current_path /= component
Expand All @@ -431,6 +448,14 @@ def _locked_parent(
handle.close()


def scan_root_identity(scan_dir: Path) -> tuple[Path, tuple[int, int]]:
"""Return a canonical scan root and its identity while holding it fixed."""

with _locked_parent(scan_dir, ".identity", create=False) as (root_path, _):
metadata = root_path.lstat()
return root_path, (metadata.st_dev, metadata.st_ino)


def open_read_fd(scan_dir: Path, relative_path: str, context: str) -> int:
"""Open a verified regular file and return an owned binary read descriptor."""

Expand Down Expand Up @@ -532,12 +557,65 @@ def _validate_existing_output(path: Path) -> None:
_verify_regular_file(handle.value, path)


def atomic_write(scan_dir: Path, relative_path: str, payload: bytes) -> None:
def _existing_output_matches(path: Path, payload: bytes) -> bool:
try:
handle = _create_file(
path,
access=_GENERIC_READ | _FILE_READ_ATTRIBUTES,
# Deny write and delete sharing while comparing the opened contents.
share=_FILE_SHARE_READ,
disposition=_OPEN_EXISTING,
flags=_FILE_FLAG_OPEN_REPARSE_POINT | _FILE_FLAG_BACKUP_SEMANTICS,
missing_ok=True,
)
except WindowsScanLocalFileError as exc:
if exc.errno in {
_ERROR_ACCESS_DENIED,
_ERROR_SHARING_VIOLATION,
_ERROR_LOCK_VIOLATION,
}:
return False
raise
if handle is None:
return False
with handle:
assert handle.value is not None
_verify_regular_file(handle.value, path)
raw_handle = handle.detach()
try:
assert _msvcrt is not None
descriptor = _msvcrt.open_osfhandle(
raw_handle, os.O_RDONLY | os.O_BINARY
)
except BaseException:
_close_handle(raw_handle)
raise
try:
with os.fdopen(descriptor, "rb") as stream:
return stream.read() == payload
except OSError:
return False


def atomic_write(
scan_dir: Path,
relative_path: str,
payload: bytes,
*,
expected_root_identity: tuple[int, int] | None = None,
) -> None:
"""Atomically replace a scan-local regular file with ``payload``."""

with _locked_parent(scan_dir, relative_path, create=True) as (parent_path, leaf_name):
with _locked_parent(
scan_dir,
relative_path,
create=True,
expected_root_identity=expected_root_identity,
) as (parent_path, leaf_name):
destination_path = parent_path / leaf_name
_validate_existing_output(destination_path)
if _existing_output_matches(destination_path, payload):
return

temp_handle: _OwnedHandle | None = None
temp_path: Path | None = None
Expand Down
54 changes: 27 additions & 27 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
mkdir,
readFile,
realpath,
rename,
rm,
writeFile,
} from "node:fs/promises";
Expand Down Expand Up @@ -56,6 +55,7 @@ import {
} from "./cost.js";
import {
loadContract,
readScanFile,
requireScanFile,
type ScanExpectation,
} from "./contract.js";
Expand Down Expand Up @@ -115,6 +115,7 @@ import {
preserveCodexSecurityPluginRegistration,
pluginExecutionEnvironment,
planOutputArchive,
prepareScanArtifactRestorer,
prepareOutputDir,
preparePersistentOutputRoot,
requireModelSafeOutputDir,
Expand All @@ -127,6 +128,7 @@ import {
type CodexCommand,
type PluginInstall,
type ProcessEnvironment,
type ScanArtifactRestorer,
type WorkbenchCommandOptions,
validateOutputDir,
} from "./runtime.js";
Expand Down Expand Up @@ -334,6 +336,7 @@ interface ClientDependencies {
) => Promise<PreparedRuntime>;
resolvePluginPython?: typeof resolvePluginPython;
prepareOutputDir?: typeof prepareOutputDir;
prepareScanArtifactRestorer?: typeof prepareScanArtifactRestorer;
repositoryRevision?: typeof repositoryRevision;
resolveCodexCommand?: () => CodexCommand;
runWorkbench?: typeof runWorkbench;
Expand Down Expand Up @@ -491,6 +494,9 @@ export class CodexSecurity {
id: string;
options: WorkbenchCommandOptions;
} | null = null;
const prepareArtifactRestorer =
this.#dependencies.prepareScanArtifactRestorer ??
prepareScanArtifactRestorer;
const workbench = this.#dependencies.runWorkbench ?? runWorkbench;
try {
const checkOpen = (): void => {
Expand Down Expand Up @@ -1181,13 +1187,15 @@ export class CodexSecurity {
]),
].map(async (name) => ({
name,
contents: await readFile(
await requireScanFile(scanDir, name, name, signal),
{ signal },
),
contents: await readScanFile(scanDir, name, name, signal),
})),
);
let artifactRestorer: ScanArtifactRestorer | null = null;
try {
artifactRestorer = await prepareArtifactRestorer(
workbenchOptions,
scanDir,
);
await runScanEvents({
thread,
events: (await followUp()).events,
Expand All @@ -1203,28 +1211,20 @@ export class CodexSecurity {
checkOpen();
} catch (error) {
if (signal.aborted || this.#closed) throw error;
for (const artifact of completedArtifacts) {
const path = join(scanDir, artifact.name);
const current = await readFile(path, { signal }).catch(
(readError: NodeJS.ErrnoException) => {
if (readError.code !== "ENOENT") throw readError;
return null;
},
);
if (current?.equals(artifact.contents)) continue;
const temporary = join(
dirname(path),
`.${randomUUID()}.${basename(path)}.restore`,
);
try {
await writeFile(temporary, artifact.contents, {
flag: "wx",
mode: 0o600,
signal,
});
await rename(temporary, path);
} finally {
await rm(temporary, { force: true });
if (artifactRestorer !== null) {
for (const artifact of completedArtifacts) {
try {
await artifactRestorer.restore(
artifact.name,
artifact.contents,
);
} catch (cause) {
if (signal.aborted || this.#closed) throw cause;
throw new OutputDirectoryError(
"Cannot restore an artifact outside the scan directory.",
{ cause },
);
}
}
}
await collectResult(
Expand Down
Loading
Loading