Harden alpha8 publication safety

This commit is contained in:
2026-07-20 14:26:22 -07:00
parent a9add2a7c1
commit b02bcc74ef
19 changed files with 532 additions and 102 deletions

View File

@@ -15,6 +15,8 @@ module Opencode
# not here — the file doesn't know which turn opened "after." That's
# a property of the scan, not a property of the file.
class SandboxFile
UnsafeFileError = Class.new(Opencode::Error)
attr_reader :path, :sandbox_prefix
def initialize(path:, sandbox_prefix:, max_bytes:)
@@ -36,7 +38,13 @@ module Opencode
end
def content
File.read(path)
with_safe_file do |file|
content = file.read(@max_bytes + 1) || "".b
if content.bytesize > @max_bytes
raise UnsafeFileError, "Sandbox file exceeds size limit while reading: #{basename}"
end
content
end
end
def content_type
@@ -52,17 +60,11 @@ module Opencode
# - Reject anything over the size cap (default
# Opencode::ResponseParser::MAX_ARTIFACT_SIZE = 10 MB).
#
# The Sandbox scan filters non-files (directories, FIFOs) before
# yielding, so we don't re-check #file? here.
# Revalidates the opened file descriptor so a path swap between the
# sandbox scan and the read cannot redirect content outside the sandbox.
def safe?
return false if File.symlink?(path)
return false unless Pathname.new(path).realpath.to_s.start_with?(sandbox_prefix)
return false if size > @max_bytes
true
rescue Errno::ENOENT
# Concurrent deletion between scan-yield and safety-check — treat
# as unsafe so the orchestrator skips rather than crashing.
with_safe_file { true }
rescue UnsafeFileError
false
end
@@ -77,5 +79,31 @@ module Opencode
content_type: content_type
)
end
private
def with_safe_file
before = File.lstat(path)
raise UnsafeFileError, "Unsafe sandbox file: #{basename}" unless before.file? && !before.symlink?
resolved = Pathname.new(path).realpath.to_s
unless resolved.start_with?(sandbox_prefix)
raise UnsafeFileError, "Sandbox file escapes its root: #{basename}"
end
File.open(path, "rb") do |file|
opened = file.stat
unless opened.file? && opened.dev == before.dev && opened.ino == before.ino
raise UnsafeFileError, "Sandbox file changed while opening: #{basename}"
end
if opened.size > @max_bytes
raise UnsafeFileError, "Sandbox file exceeds size limit: #{basename}"
end
yield file
end
rescue Errno::ENOENT, Errno::ELOOP => e
raise UnsafeFileError, "Unsafe sandbox file #{basename}: #{e.message}"
end
end
end