1. Overview
The ArchiveSandbox module implements BastionGuard’s isolated execution layer for archive inspection workers. Its role is to launch a dedicated archive analysis worker inside a constrained bwrap sandbox, capture its output, enforce execution limits, and translate the worker’s JSON response into a structured ArchiveInspectionResult.
The module is responsible for sandbox orchestration rather than archive parsing itself. It validates the worker and input paths, prepares an isolated runtime environment, enforces resource and time limits, collects stdout/stderr from the worker process, and derives normalized archive risk signals from the emitted JSON payload.
The module integrates with:
- Bubblewrap (bwrap) – process isolation, namespace separation, and restricted filesystem view
- Archive inspection worker – external analyzer executable invoked inside the sandbox
- ArchiveInspectionResult – high-level structured archive analysis result
- SandboxExecResult – lower-level execution result containing process and transport details
- POSIX process APIs –
fork(),execvp(),waitpid(),pipe(),kill(), and file descriptor management - POSIX resource limits – CPU, address space, output file size, and file descriptor ceilings
- std::filesystem – path validation and temporary sandbox workspace lifecycle
- Minimal JSON field extraction helpers – lightweight parsing of worker-emitted JSON fragments
2. High-Level Responsibilities
The module has two main public responsibilities:
run_archive_worker_bwrap(...)– launch and supervise the archive analysis worker inside a bubblewrap sandboxanalyze_archive_in_sandbox(...)– convert the worker execution outcome into a normalizedArchiveInspectionResult
This creates a clean separation between low-level sandbox/process handling and high-level BastionGuard archive-risk interpretation.
3. Internal Helper Functions
3.1 Stream Capture
The helper read_all_from_fd(int fd, std::string& out) reads all bytes from a file descriptor into a string buffer. It retries on EINTR and appends chunks until EOF is reached.
This helper is used to capture both stdout and stderr of the sandboxed worker after execution completes.
3.2 Resource Limit Setup
The helper set_worker_rlimits() applies strict runtime limits to the sandboxed worker process before execvp() is called.
The current limits are:
- CPU time –
RLIMIT_CPU = 10 seconds - Virtual address space –
RLIMIT_AS = 512 MB - Maximum output file size –
RLIMIT_FSIZE = 128 MB - Open file descriptors –
RLIMIT_NOFILE = 64
These limits provide a second containment layer in addition to bubblewrap isolation.
3.3 Lightweight JSON Field Extraction
The module includes simple string-based helpers for reading values from the worker JSON output without depending on a full JSON parser in this layer:
json_has_true(json, key)– checks whether a boolean field is present and set totruejson_read_int(json, key, fallback)– extracts an integer value for a named keyjson_read_string(json, key)– extracts a string value for a named key
These functions assume that the worker produces a predictable JSON layout and are intended as fast, minimal post-processing helpers rather than robust general-purpose JSON parsing routines.
4. Sandboxed Worker Execution
4.1 Entry Point
The low-level execution entry point is:
SandboxExecResult run_archive_worker_bwrap(
const std::string& worker_path,
const std::string& input_path,
int timeout_seconds)
This function validates both the worker executable path and the target input archive path before attempting sandbox execution.
Both paths must exist and must refer to regular files. Otherwise, execution is rejected immediately with error text such as:
invalid worker_pathinvalid input_path
4.2 Temporary Sandbox Workspace
The sandbox runtime creates a temporary directory using:
/tmp/bastionguard-archive-XXXXXX
This directory becomes the root container for the temporary working area used by the sandbox. Inside it, a work directory is created and later bound into the bubblewrap namespace.
The sandbox root is cleaned up with std::filesystem::remove_all() after execution completes, regardless of success or failure.
4.3 Stdout/Stderr Pipe Setup
Two dedicated pipes are created to collect worker output:
- One pipe for stdout
- One pipe for stderr
In the child process, these are duplicated onto STDOUT_FILENO and STDERR_FILENO with dup2().
4.4 Process Fork and Child Preparation
Execution uses fork(). In the child process, the module:
- Closes the read ends of the pipes
- Redirects stdout and stderr to the write ends
- Closes the original duplicated descriptors
- Applies process resource limits through
set_worker_rlimits() - Builds the bubblewrap command line
- Executes
bwrapthroughexecvp()
If execvp() fails, the child exits with code 127.
5. Bubblewrap Sandbox Layout
5.1 Namespace Isolation
The worker is launched under bubblewrap with a tightly scoped environment. The current command line includes:
--die-with-parent--new-session--unshare-ipc--unshare-pid--unshare-uts--unshare-cgroup-try
This isolates the worker’s process, IPC, UTS, and cgroup-related visibility from the parent environment.
5.2 Filesystem View Inside the Sandbox
The sandbox exposes a restricted filesystem layout. The worker receives:
/procvia--proc /proc/devvia--dev /dev- Read-only system libraries and binaries through:
--ro-bind /usr /usr--ro-bind /bin /bin--ro-bind /lib /lib--ro-bind /lib64 /lib64
- A tmpfs-backed
/tmp - An empty writable working directory mounted as
/work - The target archive bound read-only as:
/work/input.zip
- The worker executable bound read-only as:
/worker
The worker’s current directory is set to /work and the environment is constrained with:
HOME=/tmpTMPDIR=/tmp
5.3 Worker Invocation
Inside the sandbox, the worker is launched as:
/worker /work/input.zip
This means the sandbox layer standardizes the archive input path regardless of its original external filename.
6. Timeout and Process Supervision
6.1 Parent-Side Polling Loop
After launching the child, the parent process monitors completion using waitpid(..., WNOHANG) in a loop.
The supervision loop:
- Checks whether the child has exited
- Tracks elapsed wall-clock time using
std::chrono::steady_clock - Sleeps for
50 msbetween checks
6.2 Timeout Handling
If the child runtime exceeds timeout_seconds, the module:
- Marks the execution as timed out
- Sends
SIGKILLto the child process - Waits for final process reaping with
waitpid(..., 0)
This timeout is independent of the lower-level RLIMIT_CPU bound and therefore protects against both CPU-bound and wall-clock stalls.
6.3 Exit Status Recording
When the child terminates, the module records:
- Whether the process was successfully launched
- Whether it exited normally
- Its numeric exit code
If the process was terminated by signal, the exit code is normalized to:
128 + signal_number
This behavior mirrors common shell conventions for signal-terminated processes.
7. SandboxExecResult Semantics
run_archive_worker_bwrap(...) returns a SandboxExecResult that captures low-level execution details. The result includes fields such as:
launched– whether the child process was successfully startedtimed_out– whether wall-clock timeout occurredexited– whether process termination was observed and recordedexit_code– worker exit statusstdout_text– full captured worker stdoutstderr_text– full captured worker stderr
This low-level result is then interpreted by the higher-level analysis wrapper.
8. ArchiveInspectionResult Translation
8.1 Entry Point
The higher-level analysis wrapper is:
ArchiveInspectionResult analyze_archive_in_sandbox(
const std::string& worker_path,
const std::string& input_path,
int timeout_seconds)
This function invokes run_archive_worker_bwrap(...) and converts the sandbox outcome into BastionGuard-specific inspection semantics.
8.2 Launch Failure
If the sandbox process could not be launched at all, the returned inspection result is marked as failed with:
success = falsesandbox_failed = truereason_code = "ARCHIVE_SANDBOX_LAUNCH_FAILED"detail = exec.stderr_text
8.3 Timeout Outcome
If timeout occurs, the inspection result is marked with:
success = falsesandbox_timeout = truereason_code = "ARCHIVE_SANDBOX_TIMEOUT"detail = "archive worker timeout"raw_json = exec.stdout_text
8.4 Worker Failure
If the worker does not exit cleanly with code 0, the result is interpreted as a sandbox analysis failure:
success = falsesandbox_failed = truereason_code = "ARCHIVE_SANDBOX_ANALYSIS_FAILED"detailderived from stderr or a fallback messageraw_json = exec.stdout_text
8.5 Successful Worker Output
If the worker exits successfully, the function marks:
success = trueraw_json = exec.stdout_text
It then derives typed fields from the worker JSON output, including:
is_zipstored_size_mismatchheader_mismatchinvalid_offsetoverlapping_entriesnested_archiverisk_scorereason_codedetail
8.6 Risk Mapping
The string field risk from the worker JSON is mapped to the internal ArchiveRisk enumeration using the following mapping:
"EVASIVE"→ArchiveRisk::EVASIVE"MALFORMED"→ArchiveRisk::MALFORMED"SUSPICIOUS"→ArchiveRisk::SUSPICIOUS- Any other value →
ArchiveRisk::CLEAN
This makes the worker’s textual result usable in the rest of the application as a typed risk classification.
9. Expected Worker JSON Contract
Although this module does not define the worker itself, the current implementation assumes that the worker emits JSON fields such as:
is_zipstored_size_mismatchheader_mismatchinvalid_offsetoverlapping_entriesnested_archiverisk_scorereason_codedetailrisk
Because extraction is string-based rather than schema-validated through a full JSON parser, the field names and formatting must remain stable for reliable result interpretation.
10. Isolation and Security Characteristics
- Filesystem minimization: the worker sees only a restricted set of read-only system paths, a temporary writable area, the input archive, and the worker binary itself
- Namespace isolation: the worker runs with isolated IPC, PID, UTS, and cgroup-related namespaces
- Lifecycle coupling:
--die-with-parentensures the sandboxed worker is tied to the parent process lifecycle - Resource containment: CPU, memory, file size, and descriptor limits reduce the impact of malicious or pathological archive inputs
- Wall-clock timeout: the parent process enforces a hard runtime limit independent of CPU quota
- Ephemeral workspace: temporary sandbox directories are removed after execution, reducing persistence risk
- Controlled input exposure: the target archive is exposed under a normalized read-only path instead of the original filesystem location
11. Runtime and Robustness Considerations
- Dependency requirement: successful execution depends on the availability of
bwrapand a valid worker executable - Regular-file enforcement: both worker and input archive must be regular files before sandbox startup proceeds
- Partial-output preservation: stdout is preserved in timeout and worker-failure scenarios through
raw_json, which can assist with diagnostics - String-based JSON parsing: the post-processing layer is lightweight but less robust than a strict schema-aware JSON decoder
- Fail-closed result conversion: launch failures, timeouts, and non-zero worker exits are all converted into explicit negative inspection outcomes
- Cleanup discipline: pipes and temporary directories are closed and removed on all primary execution paths