ArchiveSandbox

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 APIsfork(), 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 sandbox
  • analyze_archive_in_sandbox(...) – convert the worker execution outcome into a normalized ArchiveInspectionResult

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 timeRLIMIT_CPU = 10 seconds
  • Virtual address spaceRLIMIT_AS = 512 MB
  • Maximum output file sizeRLIMIT_FSIZE = 128 MB
  • Open file descriptorsRLIMIT_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 to true
  • json_read_int(json, key, fallback) – extracts an integer value for a named key
  • json_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_path
  • invalid 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 bwrap through execvp()

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:

  • /proc via --proc /proc
  • /dev via --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=/tmp
  • TMPDIR=/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 ms between checks

6.2 Timeout Handling

If the child runtime exceeds timeout_seconds, the module:

  • Marks the execution as timed out
  • Sends SIGKILL to 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 started
  • timed_out – whether wall-clock timeout occurred
  • exited – whether process termination was observed and recorded
  • exit_code – worker exit status
  • stdout_text – full captured worker stdout
  • stderr_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 = false
  • sandbox_failed = true
  • reason_code = "ARCHIVE_SANDBOX_LAUNCH_FAILED"
  • detail = exec.stderr_text

8.3 Timeout Outcome

If timeout occurs, the inspection result is marked with:

  • success = false
  • sandbox_timeout = true
  • reason_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 = false
  • sandbox_failed = true
  • reason_code = "ARCHIVE_SANDBOX_ANALYSIS_FAILED"
  • detail derived from stderr or a fallback message
  • raw_json = exec.stdout_text

8.5 Successful Worker Output

If the worker exits successfully, the function marks:

  • success = true
  • raw_json = exec.stdout_text

It then derives typed fields from the worker JSON output, including:

  • is_zip
  • stored_size_mismatch
  • header_mismatch
  • invalid_offset
  • overlapping_entries
  • nested_archive
  • risk_score
  • reason_code
  • detail

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_zip
  • stored_size_mismatch
  • header_mismatch
  • invalid_offset
  • overlapping_entries
  • nested_archive
  • risk_score
  • reason_code
  • detail
  • risk

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-parent ensures 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 bwrap and 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