Documentation
¶
Index ¶
- type Manager
- func (m *Manager) Create(sessionID uuid.UUID) (string, error)
- func (m *Manager) CreateSnapshot(sessionID, snapshotID uuid.UUID) (archivePath string, sizeBytes int64, err error)
- func (m *Manager) Delete(sessionID uuid.UUID) error
- func (m *Manager) DeleteSnapshot(archivePath string) error
- func (m *Manager) OpenFile(sessionID uuid.UUID, userPath string) (*os.File, error)
- func (m *Manager) RestoreSnapshot(sessionID uuid.UUID, archivePath string) error
- func (m *Manager) SafePath(sessionID uuid.UUID, userPath string) (string, error)
- func (m *Manager) SessionPath(sessionID uuid.UUID) string
- func (m *Manager) WriteFile(sessionID uuid.UUID, userPath string, r io.Reader, maxBytes int64) (int64, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
func (*Manager) Create ¶
Create initializes an isolated workspace directory for a session.
Why 0o777 is required:
The directory is bind-mounted into the sandbox container, which runs as the unprivileged user "nobody" (UID 65534). From the Linux kernel's perspective, UID 65534 is "other" relative to the host process that owns the workspace, so the container user can only access the directory if the "other" permission bits are set. Without 0o777 (specifically the world-wx bits) the container process cannot create output files or traverse into subdirectories, causing sandbox runs to fail.
Why not use a tighter permission model:
The ideal alternative would be a per-session UID combined with a user namespace so the container runs as a private UID that owns the directory (allowing 0o700). That approach is out of scope for v1 and requires additional kernel capabilities or a suid helper binary.
Limiting blast radius on the host:
0o777 on the session-specific directory means other host processes running as arbitrary UIDs can also write to it. To prevent those host processes from even discovering which session directories exist, the parent baseDir is created with 0o700 (owner-only) in main.go. Only the owner of the host process can list baseDir entries; the 0o777 child directories are therefore not reachable by other host users without already knowing the session UUID.
umask behaviour:
os.MkdirAll honours the process umask, which can silently strip the group/other bits we set (e.g. umask 0o077 → effective 0o700). To guarantee the intended 0o777 we call os.Chmod after creation; Chmod is not affected by the umask.
func (*Manager) CreateSnapshot ¶
func (m *Manager) CreateSnapshot(sessionID, snapshotID uuid.UUID) (archivePath string, sizeBytes int64, err error)
CreateSnapshot creates a gzip-compressed tar archive of the session workspace. The archive is written to {baseDir}/snapshots/<snapshotID>.tar.gz and the archive path + uncompressed size are returned.
func (*Manager) DeleteSnapshot ¶
DeleteSnapshot removes a snapshot archive from disk.
func (*Manager) OpenFile ¶
OpenFile opens a file inside the workspace for reading. Symlink targets are validated to remain inside the workspace.
func (*Manager) RestoreSnapshot ¶
RestoreSnapshot extracts a snapshot archive into the session workspace. Files in the archive overwrite existing files; the workspace is not cleared first so existing files not in the snapshot are preserved.
func (*Manager) SafePath ¶
SafePath resolves a user-provided path within the workspace, enforcing:
- URL-decoded (loop until stable) to catch double/triple-encoded traversals
- No ".." path components (checked after full decoding, before Clean)
- Resolved path stays inside the workspace root (filepath.Clean + HasPrefix)
func (*Manager) WriteFile ¶
func (m *Manager) WriteFile(sessionID uuid.UUID, userPath string, r io.Reader, maxBytes int64) (int64, error)
WriteFile writes data to a safe path inside the workspace, creating intermediate directories as needed.
Security: guards against symlink-escape attacks where a container process creates a symlink inside /workspace pointing to a host path outside the workspace root. Two layers of protection are applied:
- The entire parent-directory chain is symlink-resolved and verified to stay inside the workspace root before any write takes place.
- The file is opened with O_NOFOLLOW so that if the final path component is itself a symlink (e.g. created in a TOCTOU window) the open fails.
Permissions: directories are created 0o777 and files 0o644 so that the container process (running as nobody/UID 65534) can read the files and write new output files. os.Chmod is called after creation to bypass the process umask, which would otherwise silently strip the group/other bits.