Compare commits

...

29 Commits

Author SHA1 Message Date
viyatb-oai
666c35091a test(linux-sandbox): add codex CLI bwrap smoke script 2026-02-01 12:45:35 -08:00
viyatb-oai
8273e9e8aa fix(linux-sandbox): set bwrap argv0 for helper re-entry 2026-02-01 12:41:15 -08:00
viyatb-oai
3cb7678e0a feat(linux-sandbox): gate bwrap rollout and keep legacy fallback off 2026-02-01 12:00:51 -08:00
viyatb-oai
05b025f311 fix(linux-sandbox): fully initialize sockaddr_nl 2026-01-30 16:02:38 -08:00
viyatb-oai
f32fc95376 refactor(linux-sandbox): remove external bwrap fallback 2026-01-30 15:52:21 -08:00
viyatb-oai
c960b0881f refactor(linux-sandbox): standardize on vendored bwrap 2026-01-30 15:40:54 -08:00
viyatb-oai
ade723257f fix(linux-sandbox): rerun build.rs for vendored bwrap 2026-01-30 13:46:15 -08:00
viyatb-oai
2f38647a2e feat(linux-sandbox): vendor bubblewrap sources 2026-01-30 13:25:11 -08:00
viyatb-oai
fbcd5ad9a5 feat(linux-sandbox): add build-time bubblewrap FFI path 2026-01-30 12:15:26 -08:00
viyatb-oai
5063676691 refactor(linux-sandbox): prefer explicit bwrap path in smoke script 2026-01-30 11:58:22 -08:00
viyatb-oai
ea3f414631 core: bundle sandbox transform args 2026-01-27 13:49:57 -08:00
viyatb-oai
50c3b38723 Merge origin/main into viyat/bwrap 2026-01-27 13:06:32 -08:00
viyatb-oai
683f7cfe74 exec: add missing bwrap path override 2026-01-27 11:21:06 -08:00
viyatb-oai
905c4fb6b1 core+linux-sandbox: make bwrap opt-in path-only 2026-01-27 10:55:03 -08:00
viyatb-oai
811a8bef38 linux-sandbox: align landlock tests with legacy defaults 2026-01-26 23:11:24 -08:00
viyatb-oai
e900c6c61d linux-sandbox: fix unfulfilled lint expectations 2026-01-26 22:56:58 -08:00
viyatb-oai
f3a860782e linux-sandbox: import Path for clippy fix 2026-01-26 22:48:42 -08:00
viyatb-oai
cb6a885cda exec-server+linux-sandbox: fix bwrap state and clippy 2026-01-26 22:44:42 -08:00
viyatb-oai
982693e0fe linux-sandbox: update lockfile for bwrap deps 2026-01-26 22:30:26 -08:00
viyatb-oai
cf62897926 core+servers: thread bwrap opt-in via sandbox state 2026-01-26 21:43:41 -08:00
viyatb-oai
f3beeaefa9 linux-sandbox: expand rollout matrix script 2026-01-26 19:34:54 -08:00
viyatb-oai
4387f44060 core+linux-sandbox: opt-in bwrap sandbox pipeline 2026-01-26 18:54:44 -08:00
viyatb-oai
425ae2cd52 linux-sandbox: stage seccomp after bwrap 2026-01-26 15:51:48 -08:00
viyatb-oai
9be8e39ac2 linux-sandbox: dev-bind /dev/null under ro root 2026-01-26 15:48:47 -08:00
viyatb-oai
6e8e4bdbaa linux-sandbox: avoid no_new_privs when network is allowed 2026-01-26 15:38:14 -08:00
viyatb-oai
81d9915cc0 linux-sandbox: add bwrap check and debug argv hook 2026-01-26 15:34:39 -08:00
viyatb-oai
4c6ad6c386 linux-sandbox: add which dependency for bwrap 2026-01-26 15:28:15 -08:00
viyatb-oai
7d9a7560fe Merge remote-tracking branch 'origin/main' into viyat/bwrap 2026-01-26 13:47:39 -08:00
viyatb-oai
c5f7930a01 linux-sandbox: add bubblewrap wrapper and --no-proc 2026-01-26 13:37:49 -08:00
81 changed files with 11914 additions and 378 deletions

4
codex-rs/Cargo.lock generated
View File

@@ -1593,15 +1593,19 @@ dependencies = [
name = "codex-linux-sandbox"
version = "0.0.0"
dependencies = [
"cc",
"clap",
"codex-core",
"codex-utils-absolute-path",
"landlock",
"libc",
"pkg-config",
"pretty_assertions",
"seccompiler",
"serde_json",
"tempfile",
"tokio",
"which",
]
[[package]]

View File

@@ -1294,6 +1294,7 @@ impl CodexMessageProcessor {
let outgoing = self.outgoing.clone();
let req_id = request_id;
let sandbox_cwd = self.config.cwd.clone();
let use_linux_sandbox_bwrap = self.config.features.enabled(Feature::UseLinuxSandboxBwrap);
tokio::spawn(async move {
match codex_core::exec::process_exec_tool_call(
@@ -1301,6 +1302,7 @@ impl CodexMessageProcessor {
&effective_policy,
sandbox_cwd.as_path(),
&codex_linux_sandbox_exe,
use_linux_sandbox_bwrap,
None,
)
.await

View File

@@ -224,16 +224,19 @@ async fn run_command_under_sandbox(
.await?
}
SandboxType::Landlock => {
use codex_core::features::Feature;
#[expect(clippy::expect_used)]
let codex_linux_sandbox_exe = config
.codex_linux_sandbox_exe
.expect("codex-linux-sandbox executable not found");
let use_bwrap_sandbox = config.features.enabled(Feature::UseLinuxSandboxBwrap);
spawn_command_under_linux_sandbox(
codex_linux_sandbox_exe,
command,
cwd,
config.sandbox_policy.get(),
sandbox_policy_cwd.as_path(),
use_bwrap_sandbox,
stdio_policy,
env,
)

View File

@@ -207,6 +207,9 @@
"unified_exec": {
"type": "boolean"
},
"use_linux_sandbox_bwrap": {
"type": "boolean"
},
"web_search": {
"type": "boolean"
},
@@ -1200,6 +1203,9 @@
"unified_exec": {
"type": "boolean"
},
"use_linux_sandbox_bwrap": {
"type": "boolean"
},
"web_search": {
"type": "boolean"
},

View File

@@ -451,6 +451,7 @@ pub(crate) struct TurnContext {
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
pub(crate) tools_config: ToolsConfig,
pub(crate) features: Features,
pub(crate) ghost_snapshot: GhostSnapshotConfig,
pub(crate) final_output_json_schema: Option<Value>,
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
@@ -632,6 +633,7 @@ impl Session {
windows_sandbox_level: session_configuration.windows_sandbox_level,
shell_environment_policy: per_turn_config.shell_environment_policy.clone(),
tools_config,
features: per_turn_config.features.clone(),
ghost_snapshot: per_turn_config.ghost_snapshot.clone(),
final_output_json_schema: None,
codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(),
@@ -877,6 +879,7 @@ impl Session {
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: session_configuration.cwd.clone(),
use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap),
};
let cancel_token = sess.mcp_startup_cancellation_token().await;
@@ -1117,6 +1120,9 @@ impl Session {
sandbox_policy: per_turn_config.sandbox_policy.get().clone(),
codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: per_turn_config.cwd.clone(),
use_linux_sandbox_bwrap: per_turn_config
.features
.enabled(Feature::UseLinuxSandboxBwrap),
};
if let Err(e) = self
.services
@@ -2138,6 +2144,7 @@ impl Session {
sandbox_policy: turn_context.sandbox_policy.clone(),
codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(),
sandbox_cwd: turn_context.cwd.clone(),
use_linux_sandbox_bwrap: turn_context.features.enabled(Feature::UseLinuxSandboxBwrap),
};
let cancel_token = self.reset_mcp_startup_cancellation_token().await;
@@ -2924,6 +2931,7 @@ async fn spawn_review_thread(
sub_id: sub_id.to_string(),
client,
tools_config,
features: parent_turn_context.features.clone(),
ghost_snapshot: parent_turn_context.ghost_snapshot.clone(),
developer_instructions: None,
user_instructions: None,

View File

@@ -59,6 +59,7 @@ pub async fn list_accessible_connectors_from_mcp_tools(
sandbox_policy: SandboxPolicy::ReadOnly,
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap),
};
mcp_connection_manager

View File

@@ -140,6 +140,7 @@ pub async fn process_exec_tool_call(
sandbox_policy: &SandboxPolicy,
sandbox_cwd: &Path,
codex_linux_sandbox_exe: &Option<PathBuf>,
use_linux_sandbox_bwrap: bool,
stdout_stream: Option<StdoutStream>,
) -> Result<ExecToolCallOutput> {
let windows_sandbox_level = params.windows_sandbox_level;
@@ -184,14 +185,15 @@ pub async fn process_exec_tool_call(
let manager = SandboxManager::new();
let exec_env = manager
.transform(
.transform(crate::sandboxing::SandboxTransformRequest {
spec,
sandbox_policy,
sandbox_type,
sandbox_cwd,
codex_linux_sandbox_exe.as_ref(),
policy: sandbox_policy,
sandbox: sandbox_type,
sandbox_policy_cwd: sandbox_cwd,
codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(),
use_linux_sandbox_bwrap,
windows_sandbox_level,
)
})
.map_err(CodexErr::from)?;
// Route through the sandboxing module for a single, unified execution path.
@@ -1035,6 +1037,7 @@ mod tests {
&SandboxPolicy::DangerFullAccess,
cwd.as_path(),
&None,
false,
None,
)
.await;

View File

@@ -89,6 +89,8 @@ pub enum Feature {
WebSearchCached,
/// Gate the execpolicy enforcement for shell/unified exec.
ExecPolicy,
/// Use the bubblewrap-based Linux sandbox pipeline.
UseLinuxSandboxBwrap,
/// Enable Windows sandbox (restricted token) on Windows.
WindowsSandbox,
/// Use the elevated Windows sandbox pipeline (setup + runner).
@@ -391,6 +393,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::UnderDevelopment,
default_enabled: true,
},
FeatureSpec {
id: Feature::UseLinuxSandboxBwrap,
key: "use_linux_sandbox_bwrap",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::WindowsSandbox,
key: "experimental_windows_sandbox",

View File

@@ -6,26 +6,34 @@ use std::path::Path;
use std::path::PathBuf;
use tokio::process::Child;
/// Spawn a shell tool command under the Linux Landlock+seccomp sandbox helper
/// (codex-linux-sandbox).
/// Spawn a shell tool command under the Linux sandbox helper
/// (codex-linux-sandbox), which currently uses bubblewrap for filesystem
/// isolation plus seccomp for network restrictions.
///
/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux
/// helper accepts a list of `--sandbox-permission`/`-s` flags mirroring the
/// public CLI. We convert the internal [`SandboxPolicy`] representation into
/// the equivalent CLI options.
#[allow(clippy::too_many_arguments)]
pub async fn spawn_command_under_linux_sandbox<P>(
codex_linux_sandbox_exe: P,
command: Vec<String>,
command_cwd: PathBuf,
sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
use_bwrap_sandbox: bool,
stdio_policy: StdioPolicy,
env: HashMap<String, String>,
) -> std::io::Result<Child>
where
P: AsRef<Path>,
{
let args = create_linux_sandbox_command_args(command, sandbox_policy, sandbox_policy_cwd);
let args = create_linux_sandbox_command_args(
command,
sandbox_policy,
sandbox_policy_cwd,
use_bwrap_sandbox,
);
let arg0 = Some("codex-linux-sandbox");
spawn_child_async(
codex_linux_sandbox_exe.as_ref().to_path_buf(),
@@ -40,10 +48,14 @@ where
}
/// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`.
///
/// The helper performs the actual sandboxing (bubblewrap + seccomp) after
/// parsing these arguments. See `docs/linux_sandbox.md` for the Linux semantics.
pub(crate) fn create_linux_sandbox_command_args(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
use_bwrap_sandbox: bool,
) -> Vec<String> {
#[expect(clippy::expect_used)]
let sandbox_policy_cwd = sandbox_policy_cwd
@@ -60,13 +72,51 @@ pub(crate) fn create_linux_sandbox_command_args(
sandbox_policy_cwd,
"--sandbox-policy".to_string(),
sandbox_policy_json,
// Separator so that command arguments starting with `-` are not parsed as
// options of the helper itself.
"--".to_string(),
];
if use_bwrap_sandbox {
linux_cmd.push("--use-bwrap-sandbox".to_string());
linux_cmd.push("--use-vendored-bwrap".to_string());
}
// Separator so that command arguments starting with `-` are not parsed as
// options of the helper itself.
linux_cmd.push("--".to_string());
// Append the original tool command.
linux_cmd.extend(command);
linux_cmd
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn bwrap_flags_are_feature_gated() {
let command = vec!["/bin/true".to_string()];
let cwd = Path::new("/tmp");
let policy = SandboxPolicy::ReadOnly;
let with_bwrap = create_linux_sandbox_command_args(command.clone(), &policy, cwd, true);
assert_eq!(
with_bwrap.contains(&"--use-bwrap-sandbox".to_string()),
true
);
assert_eq!(
with_bwrap.contains(&"--use-vendored-bwrap".to_string()),
true
);
let without_bwrap = create_linux_sandbox_command_args(command, &policy, cwd, false);
assert_eq!(
without_bwrap.contains(&"--use-bwrap-sandbox".to_string()),
false
);
assert_eq!(
without_bwrap.contains(&"--use-vendored-bwrap".to_string()),
false
);
}
}

View File

@@ -160,6 +160,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent
sandbox_policy: SandboxPolicy::ReadOnly,
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap),
};
mcp_connection_manager

View File

@@ -302,6 +302,8 @@ pub struct SandboxState {
pub sandbox_policy: SandboxPolicy,
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub sandbox_cwd: PathBuf,
#[serde(default)]
pub use_linux_sandbox_bwrap: bool,
}
/// A thin wrapper around a set of running [`RmcpClient`] instances.

View File

@@ -51,6 +51,19 @@ pub struct ExecEnv {
pub arg0: Option<String>,
}
/// Bundled arguments for sandbox transformation.
///
/// This keeps call sites self-documenting when several fields are optional.
pub(crate) struct SandboxTransformRequest<'a> {
pub spec: CommandSpec,
pub policy: &'a SandboxPolicy,
pub sandbox: SandboxType,
pub sandbox_policy_cwd: &'a Path,
pub codex_linux_sandbox_exe: Option<&'a PathBuf>,
pub use_linux_sandbox_bwrap: bool,
pub windows_sandbox_level: WindowsSandboxLevel,
}
pub enum SandboxPreference {
Auto,
Require,
@@ -104,13 +117,17 @@ impl SandboxManager {
pub(crate) fn transform(
&self,
mut spec: CommandSpec,
policy: &SandboxPolicy,
sandbox: SandboxType,
sandbox_policy_cwd: &Path,
codex_linux_sandbox_exe: Option<&PathBuf>,
windows_sandbox_level: WindowsSandboxLevel,
request: SandboxTransformRequest<'_>,
) -> Result<ExecEnv, SandboxTransformError> {
let SandboxTransformRequest {
mut spec,
policy,
sandbox,
sandbox_policy_cwd,
codex_linux_sandbox_exe,
use_linux_sandbox_bwrap,
windows_sandbox_level,
} = request;
let mut env = spec.env;
if !policy.has_full_network_access() {
env.insert(
@@ -141,8 +158,12 @@ impl SandboxManager {
SandboxType::LinuxSeccomp => {
let exe = codex_linux_sandbox_exe
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
let mut args =
create_linux_sandbox_command_args(command.clone(), policy, sandbox_policy_cwd);
let mut args = create_linux_sandbox_command_args(
command.clone(),
policy,
sandbox_policy_cwd,
use_linux_sandbox_bwrap,
);
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(exe.to_string_lossy().to_string());
full_command.append(&mut args);

View File

@@ -8,6 +8,7 @@ retry without sandbox on denial (no reapproval thanks to caching).
use crate::error::CodexErr;
use crate::error::SandboxErr;
use crate::exec::ExecToolCallOutput;
use crate::features::Feature;
use crate::sandboxing::SandboxManager;
use crate::tools::sandboxing::ApprovalCtx;
use crate::tools::sandboxing::ExecApprovalRequirement;
@@ -97,12 +98,14 @@ impl ToolOrchestrator {
// Platform-specific flag gating is handled by SandboxManager::select_initial
// via crate::safety::get_platform_sandbox(..).
let use_linux_sandbox_bwrap = turn_ctx.features.enabled(Feature::UseLinuxSandboxBwrap);
let initial_attempt = SandboxAttempt {
sandbox: initial_sandbox,
policy: &turn_ctx.sandbox_policy,
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(),
use_linux_sandbox_bwrap,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
};
@@ -154,6 +157,7 @@ impl ToolOrchestrator {
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
codex_linux_sandbox_exe: None,
use_linux_sandbox_bwrap,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
};

View File

@@ -274,6 +274,7 @@ pub(crate) struct SandboxAttempt<'a> {
pub(crate) manager: &'a SandboxManager,
pub(crate) sandbox_cwd: &'a Path,
pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
pub use_linux_sandbox_bwrap: bool,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
}
@@ -282,14 +283,16 @@ impl<'a> SandboxAttempt<'a> {
&self,
spec: CommandSpec,
) -> Result<crate::sandboxing::ExecEnv, SandboxTransformError> {
self.manager.transform(
spec,
self.policy,
self.sandbox,
self.sandbox_cwd,
self.codex_linux_sandbox_exe,
self.windows_sandbox_level,
)
self.manager
.transform(crate::sandboxing::SandboxTransformRequest {
spec,
policy: self.policy,
sandbox: self.sandbox,
sandbox_policy_cwd: self.sandbox_cwd,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe,
use_linux_sandbox_bwrap: self.use_linux_sandbox_bwrap,
windows_sandbox_level: self.windows_sandbox_level,
})
}
}

View File

@@ -44,7 +44,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput
let policy = SandboxPolicy::new_read_only_policy();
process_exec_tool_call(params, &policy, tmp.path(), &None, None).await
process_exec_tool_call(params, &policy, tmp.path(), &None, false, None).await
}
/// Command succeeds with exit code 0 normally

View File

@@ -0,0 +1,59 @@
# Linux Sandbox (Vendored bubblewrap, Landlock legacy)
The Linux sandbox helper (`codex-linux-sandbox`) supports two filesystem
pipelines:
- Vendored bubblewrap (feature-flag opt-in): bubblewrap (`bwrap`) for
filesystem isolation plus seccomp.
- Legacy: mount-based protections plus Landlock enforcement.
## Requirements
- The bubblewrap pipeline requires a Linux build that enables the vendored
bubblewrap FFI path.
- During rollout, enable bubblewrap with
`features.use_linux_sandbox_bwrap = true`.
## Filesystem Semantics (bubblewrap pipeline)
When the bubblewrap pipeline is enabled and disk writes are restricted
(`read-only` or `workspace-write`), the helper builds the filesystem view with
bubblewrap in this order:
1. `--ro-bind / /` makes the entire filesystem read-only.
2. `--bind <root> <root>` re-enables writes for each writable root.
3. `--ro-bind <subpath> <subpath>` re-applies read-only protections under
writable roots so protected paths win.
4. `--dev-bind /dev/null /dev/null` preserves the common sink.
Writable roots and protected subpaths are derived from
`SandboxPolicy::get_writable_roots_with_cwd()`.
Protected subpaths include:
- top-level `.git` (directory or pointer file),
- the resolved `gitdir:` target for worktrees and submodules, and
- top-level `.codex`.
### Deny-path Hardening
To reduce symlink and path-creation attacks inside writable roots:
- If any component of a protected path is a symlink within a writable root, the
helper mounts `/dev/null` on that symlink.
- If a protected path does not exist, the helper mounts `/dev/null` on the
first missing path component (when it is within a writable root).
## Process and Network Semantics
- In the bubblewrap pipeline, the helper isolates the PID namespace via
`--unshare-pid`.
- In the bubblewrap pipeline, it mounts a fresh `/proc` via `--proc /proc` by
default.
- In restrictive container environments, you can skip the `/proc` mount with
the helper flag `--no-proc` while still keeping PID isolation enabled.
- Network restrictions are enforced with seccomp when network access is
disabled.
## Notes
- The CLI still exposes legacy names such as `codex debug landlock`.
- Landlock remains the default filesystem enforcement pipeline when
`features.use_linux_sandbox_bwrap` is not enabled.

View File

@@ -95,6 +95,7 @@ impl EscalateServer {
&sandbox_state.sandbox_policy,
&sandbox_state.sandbox_cwd,
&sandbox_state.codex_linux_sandbox_exe,
sandbox_state.use_linux_sandbox_bwrap,
None,
)
.await?;

View File

@@ -126,6 +126,7 @@ impl ExecTool {
sandbox_policy: SandboxPolicy::ReadOnly,
codex_linux_sandbox_exe: None,
sandbox_cwd: PathBuf::from(&params.workdir),
use_linux_sandbox_bwrap: false,
});
let escalate_server = EscalateServer::new(
self.bash_path.clone(),

View File

@@ -91,6 +91,7 @@ where
sandbox_policy: SandboxPolicy::ReadOnly,
codex_linux_sandbox_exe,
sandbox_cwd: sandbox_cwd.as_ref().to_path_buf(),
use_linux_sandbox_bwrap: false,
};
send_sandbox_state_update(sandbox_state, service).await
}
@@ -118,6 +119,7 @@ where
},
codex_linux_sandbox_exe,
sandbox_cwd: writable_folder.as_ref().to_path_buf(),
use_linux_sandbox_bwrap: false,
};
send_sandbox_state_update(sandbox_state, service).await
}

View File

@@ -50,6 +50,7 @@ async fn spawn_command_under_sandbox(
command_cwd,
sandbox_policy,
sandbox_cwd,
false,
stdio_policy,
env,
)

View File

@@ -22,6 +22,8 @@ codex-utils-absolute-path = { workspace = true }
landlock = { workspace = true }
libc = { workspace = true }
seccompiler = { workspace = true }
serde_json = { workspace = true }
which = { workspace = true }
[target.'cfg(target_os = "linux")'.dev-dependencies]
pretty_assertions = { workspace = true }
@@ -33,3 +35,7 @@ tokio = { workspace = true, features = [
"rt-multi-thread",
"signal",
] }
[build-dependencies]
cc = "1"
pkg-config = "0.3"

View File

@@ -6,3 +6,28 @@ This crate is responsible for producing:
- a lib crate that exposes the business logic of the executable as `run_main()` so that
- the `codex-exec` CLI can check if its arg0 is `codex-linux-sandbox` and, if so, execute as if it were `codex-linux-sandbox`
- this should also be true of the `codex` multitool CLI
On Linux, the bubblewrap pipeline uses the vendored bubblewrap path compiled
into this binary.
**Current Behavior**
- Legacy Landlock + mount protections remain available as the legacy pipeline.
- The bubblewrap pipeline is standardized on the vendored path.
- During rollout, the bubblewrap pipeline is gated by the temporary feature
flag `features.use_linux_sandbox_bwrap` (legacy remains default when off).
- When enabled, the bubblewrap pipeline applies `PR_SET_NO_NEW_PRIVS` and a
seccomp network filter in-process.
- When enabled, the filesystem is read-only by default via `--ro-bind / /`.
- When enabled, writable roots are layered with `--bind <root> <root>`.
- When enabled, protected subpaths under writable roots (for example `.git`,
resolved `gitdir:`, and `.codex`) are re-applied as read-only via `--ro-bind`.
- When enabled, symlink-in-path and non-existent protected paths inside
writable roots are blocked by mounting `/dev/null` on the symlink or first
missing component.
- When enabled, the helper isolates the PID namespace via `--unshare-pid`.
- When enabled, it mounts a fresh `/proc` via `--proc /proc` by default, but
you can skip this in restrictive container environments with `--no-proc`.
**Notes**
- The CLI surface still uses legacy names like `codex debug landlock`.
- See `docs/linux_sandbox.md` for the full Linux sandbox semantics.

View File

@@ -0,0 +1,113 @@
use std::env;
use std::path::Path;
use std::path::PathBuf;
fn main() {
// Tell rustc/clippy that this is an expected cfg value.
println!("cargo:rustc-check-cfg=cfg(vendored_bwrap_available)");
println!("cargo:rerun-if-env-changed=CODEX_BWRAP_ENABLE_FFI");
println!("cargo:rerun-if-env-changed=CODEX_BWRAP_SOURCE_DIR");
// Rebuild if the vendored bwrap sources change.
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
let vendor_dir = manifest_dir.join("../vendor/bubblewrap");
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join("bubblewrap.c").display()
);
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join("bind-mount.c").display()
);
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join("network.c").display()
);
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join("utils.c").display()
);
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "linux" {
return;
}
// Opt-in: do not attempt to fetch/compile bwrap unless explicitly enabled.
let enable_ffi = matches!(env::var("CODEX_BWRAP_ENABLE_FFI"), Ok(value) if value == "1");
if !enable_ffi {
return;
}
if let Err(err) = try_build_vendored_bwrap() {
// Keep normal builds working even if the experiment fails.
println!("cargo:warning=build-time bubblewrap disabled: {err}");
}
}
fn try_build_vendored_bwrap() -> Result<(), String> {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|err| err.to_string())?);
let out_dir = PathBuf::from(env::var("OUT_DIR").map_err(|err| err.to_string())?);
let src_dir = resolve_bwrap_source_dir(&manifest_dir)?;
let libcap = pkg_config::Config::new()
.probe("libcap")
.map_err(|err| format!("libcap not available via pkg-config: {err}"))?;
let config_h = out_dir.join("config.h");
std::fs::write(
&config_h,
"#pragma once\n#define PACKAGE_STRING \"bubblewrap built at codex build-time\"\n",
)
.map_err(|err| format!("failed to write {}: {err}", config_h.display()))?;
let mut build = cc::Build::new();
build
.file(src_dir.join("bubblewrap.c"))
.file(src_dir.join("bind-mount.c"))
.file(src_dir.join("network.c"))
.file(src_dir.join("utils.c"))
.include(&out_dir)
.include(&src_dir)
.define("_GNU_SOURCE", None)
// Rename `main` so we can call it via FFI.
.define("main", Some("bwrap_main"));
for include_path in libcap.include_paths {
build.include(include_path);
}
build.compile("build_time_bwrap");
println!("cargo:rustc-cfg=vendored_bwrap_available");
Ok(())
}
/// Resolve the bubblewrap source directory used for build-time compilation.
///
/// Priority:
/// 1. `CODEX_BWRAP_SOURCE_DIR` points at an existing bubblewrap checkout.
/// 2. The vendored bubblewrap tree under `codex-rs/vendor/bubblewrap`.
fn resolve_bwrap_source_dir(manifest_dir: &Path) -> Result<PathBuf, String> {
if let Ok(path) = env::var("CODEX_BWRAP_SOURCE_DIR") {
let src_dir = PathBuf::from(path);
if src_dir.exists() {
return Ok(src_dir);
}
return Err(format!(
"CODEX_BWRAP_SOURCE_DIR was set but does not exist: {}",
src_dir.display()
));
}
let vendor_dir = manifest_dir.join("../vendor/bubblewrap");
if vendor_dir.exists() {
return Ok(vendor_dir);
}
Err(format!(
"expected vendored bubblewrap at {}, but it was not found.\n\
Set CODEX_BWRAP_SOURCE_DIR to an existing checkout or vendor bubblewrap under codex-rs/vendor.",
vendor_dir.display()
))
}

View File

@@ -0,0 +1,222 @@
#!/usr/bin/env bash
set -euo pipefail
# Linux sandbox smoke test script.
#
# This is designed for Linux devboxes where bwrap is available. It builds the
# codex-linux-sandbox binary and runs a small matrix of behavior checks:
# - workspace writes succeed
# - protected paths (.git, .codex) remain read-only
# - writes outside allowed roots fail
# - network_access=false blocks outbound sockets
#
# Usage:
# codex-rs/linux-sandbox/scripts/test_linux_sandbox.sh
#
# Optional env vars:
# CODEX_BWRAP_ENABLE_FFI=1 # default: 1 (build vendored bwrap path)
# CODEX_LINUX_SANDBOX_NO_PROC=0 # default: 0 (let helper auto-retry with --no-proc)
# CODEX_LINUX_SANDBOX_DEBUG=1 # default: 0 (pass debug env var through)
# CODEX_LINUX_SANDBOX_USE_BWRAP=1 # default: 1 (run the bwrap suite)
# CODEX_LINUX_SANDBOX_USE_LEGACY=1 # default: 0 (run the legacy suite)
# CODEX_LINUX_SANDBOX_USE_CODEX_CLI=1 # default: 1 (run codex CLI bwrap smoke)
if [[ "$(uname -s)" != "Linux" ]]; then
echo "This script is intended to run on Linux." >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
CODEX_RS_DIR="${REPO_ROOT}/codex-rs"
BWRAP_ENABLE_FFI="${CODEX_BWRAP_ENABLE_FFI:-1}"
NO_PROC="${CODEX_LINUX_SANDBOX_NO_PROC:-0}"
DEBUG="${CODEX_LINUX_SANDBOX_DEBUG:-0}"
USE_BWRAP_SUITE="${CODEX_LINUX_SANDBOX_USE_BWRAP:-1}"
USE_LEGACY_SUITE="${CODEX_LINUX_SANDBOX_USE_LEGACY:-0}"
USE_CODEX_CLI_SMOKE="${CODEX_LINUX_SANDBOX_USE_CODEX_CLI:-1}"
SANDBOX_BIN="${CODEX_RS_DIR}/target/debug/codex-linux-sandbox"
CODEX_BIN="${CODEX_RS_DIR}/target/debug/codex"
tmp_root=""
build_binaries() {
echo "==> Building codex-linux-sandbox"
(
cd "${CODEX_RS_DIR}" && \
CODEX_BWRAP_ENABLE_FFI="${BWRAP_ENABLE_FFI}" cargo build -p codex-linux-sandbox >/dev/null
)
if [[ "${USE_CODEX_CLI_SMOKE}" == "1" ]]; then
echo "==> Building codex (local target/debug/codex)"
(
cd "${CODEX_RS_DIR}" && \
CODEX_BWRAP_ENABLE_FFI="${BWRAP_ENABLE_FFI}" cargo build -p codex-cli >/dev/null
)
fi
}
policy_json() {
local network_access="$1"
printf '{"type":"workspace-write","writable_roots":[],"network_access":%s}' "${network_access}"
}
run_sandbox() {
local network_access="$1"
local use_bwrap="$2"
shift
shift
local no_proc_flag=()
if [[ "${NO_PROC}" == "1" && "${use_bwrap}" == "1" ]]; then
no_proc_flag=(--no-proc)
fi
local debug_env=()
if [[ "${DEBUG}" == "1" ]]; then
debug_env=(env CODEX_LINUX_SANDBOX_DEBUG=1)
fi
local bwrap_flag=()
if [[ "${use_bwrap}" == "1" ]]; then
bwrap_flag=(--use-bwrap-sandbox --use-vendored-bwrap)
fi
"${debug_env[@]}" "${SANDBOX_BIN}" \
--sandbox-policy-cwd "${REPO_ROOT}" \
--sandbox-policy "$(policy_json "${network_access}")" \
"${bwrap_flag[@]}" \
"${no_proc_flag[@]}" \
-- "$@"
}
expect_success() {
local label="$1"
local network_access="$2"
local use_bwrap="$3"
shift
shift
shift
echo "==> ${label}"
if run_sandbox "${network_access}" "${use_bwrap}" "$@"; then
echo " PASS"
else
echo " FAIL (expected success)" >&2
exit 1
fi
}
expect_failure() {
local label="$1"
local network_access="$2"
local use_bwrap="$3"
shift
shift
shift
echo "==> ${label}"
if run_sandbox "${network_access}" "${use_bwrap}" "$@"; then
echo " FAIL (expected failure)" >&2
exit 1
else
echo " PASS (failed as expected)"
fi
}
run_suite() {
local suite_name="$1"
local use_bwrap="$2"
echo
echo "==== Suite: ${suite_name} (use_bwrap=${use_bwrap}) ===="
# Create a disposable writable root for workspace-write checks.
if [[ -n "${tmp_root:-}" ]]; then
rm -rf -- "${tmp_root}"
fi
tmp_root="$(mktemp -d "${REPO_ROOT}/.codex-sandbox-test.XXXXXX")"
trap 'rm -rf -- "${tmp_root:-}"' EXIT
mkdir -p "${REPO_ROOT}/.codex"
expect_success \
"workspace write succeeds inside repo" \
true \
"${use_bwrap}" \
/usr/bin/bash -lc "cd '${tmp_root}' && touch OK_IN_WORKSPACE"
expect_failure \
"writes outside allowed roots fail" \
true \
"${use_bwrap}" \
/usr/bin/bash -lc "touch /etc/SHOULD_FAIL"
# Only the bwrap suite enforces `.git` and `.codex` as read-only.
if [[ "${use_bwrap}" == "1" ]]; then
expect_failure \
".git and .codex remain read-only (bwrap)" \
true \
"${use_bwrap}" \
/usr/bin/bash -lc "cd '${REPO_ROOT}' && touch .git/SHOULD_FAIL && touch .codex/SHOULD_FAIL"
else
expect_success \
".git and .codex are NOT protected in legacy landlock path" \
true \
"${use_bwrap}" \
/usr/bin/bash -lc "cd '${REPO_ROOT}' && mkdir -p .codex && touch .git/SHOULD_SUCCEED && touch .codex/SHOULD_SUCCEED"
fi
expect_failure \
"network_access=false blocks outbound sockets" \
false \
"${use_bwrap}" \
/usr/bin/bash -lc "exec 3<>/dev/tcp/1.1.1.1/443"
}
run_codex_cli_smoke() {
if [[ "${USE_CODEX_CLI_SMOKE}" != "1" ]]; then
return
fi
echo
echo "==== codex CLI smoke (feature flag path) ===="
echo "==> codex -c features.use_linux_sandbox_bwrap=true sandbox linux ..."
local output=""
if ! output=$(
"${CODEX_BIN}" \
-c features.use_linux_sandbox_bwrap=true \
sandbox linux --full-auto -- /usr/bin/bash -lc 'echo BWRAP_OK' 2>&1
); then
echo "${output}" >&2
echo " FAIL (expected codex CLI bwrap smoke success)" >&2
exit 1
fi
if [[ "${output}" != *"BWRAP_OK"* ]]; then
echo "${output}" >&2
echo " FAIL (missing BWRAP_OK output)" >&2
exit 1
fi
echo "${output}"
echo " PASS"
}
main() {
build_binaries
run_codex_cli_smoke
if [[ "${USE_BWRAP_SUITE}" == "1" ]]; then
run_suite "bwrap opt-in" "1"
fi
if [[ "${USE_LEGACY_SUITE}" == "1" ]]; then
run_suite "legacy default" "0"
fi
echo
echo "All requested linux-sandbox suites passed."
}
main "$@"

View File

@@ -0,0 +1,252 @@
//! Bubblewrap-based filesystem sandboxing for Linux.
//!
//! This module mirrors the semantics used by the macOS Seatbelt sandbox:
//! - the filesystem is read-only by default,
//! - explicit writable roots are layered on top, and
//! - sensitive subpaths such as `.git` and `.codex` remain read-only even when
//! their parent root is writable.
//!
//! The overall Linux sandbox is composed of:
//! - seccomp + `PR_SET_NO_NEW_PRIVS` applied in-process, and
//! - bubblewrap used to construct the filesystem view before exec.
use std::collections::BTreeSet;
use std::path::Path;
use std::path::PathBuf;
use codex_core::error::CodexErr;
use codex_core::error::Result;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::WritableRoot;
/// Options that control how bubblewrap is invoked.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BwrapOptions {
/// Whether to mount a fresh `/proc` inside the PID namespace.
///
/// This is the secure default, but some restrictive container environments
/// deny `--proc /proc` even when PID namespaces are available.
pub mount_proc: bool,
}
impl Default for BwrapOptions {
fn default() -> Self {
Self { mount_proc: true }
}
}
/// Wrap a command with bubblewrap so the filesystem is read-only by default,
/// with explicit writable roots and read-only subpaths layered afterward.
///
/// When the policy grants full disk write access, this returns `command`
/// unchanged so we avoid unnecessary sandboxing overhead.
pub(crate) fn create_bwrap_command_args(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
cwd: &Path,
options: BwrapOptions,
) -> Result<Vec<String>> {
if sandbox_policy.has_full_disk_write_access() {
return Ok(command);
}
create_bwrap_flags(command, sandbox_policy, cwd, options)
}
/// Build the bubblewrap flags (everything after `argv[0]`).
fn create_bwrap_flags(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
cwd: &Path,
options: BwrapOptions,
) -> Result<Vec<String>> {
let mut args = Vec::new();
args.push("--new-session".to_string());
args.push("--die-with-parent".to_string());
args.extend(create_filesystem_args(sandbox_policy, cwd)?);
// Isolate the PID namespace.
args.push("--unshare-pid".to_string());
// Mount a fresh /proc unless the caller explicitly disables it.
if options.mount_proc {
args.push("--proc".to_string());
args.push("/proc".to_string());
}
args.push("--".to_string());
args.extend(command);
Ok(args)
}
/// Build the bubblewrap filesystem mounts for a given sandbox policy.
///
/// The mount order is important:
/// 1. `--ro-bind / /` makes the entire filesystem read-only.
/// 2. `--bind <root> <root>` re-enables writes for allowed roots.
/// 3. `--ro-bind <subpath> <subpath>` re-applies read-only protections under
/// those writable roots so protected subpaths win.
/// 4. `--dev-bind /dev/null /dev/null` preserves the common sink even under a
/// read-only root.
fn create_filesystem_args(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result<Vec<String>> {
let writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
ensure_mount_targets_exist(&writable_roots)?;
let mut args = Vec::new();
// Read-only root, then selectively re-enable writes.
args.push("--ro-bind".to_string());
args.push("/".to_string());
args.push("/".to_string());
for writable_root in &writable_roots {
let root = writable_root.root.as_path();
args.push("--bind".to_string());
args.push(path_to_string(root));
args.push(path_to_string(root));
}
// Re-apply read-only subpaths after the writable binds so they win.
let allowed_write_paths: Vec<PathBuf> = writable_roots
.iter()
.map(|writable_root| writable_root.root.as_path().to_path_buf())
.collect();
for subpath in collect_read_only_subpaths(&writable_roots) {
if let Some(symlink_path) = find_symlink_in_path(&subpath, &allowed_write_paths) {
args.push("--ro-bind".to_string());
args.push("/dev/null".to_string());
args.push(path_to_string(&symlink_path));
continue;
}
if !subpath.exists() {
if let Some(first_missing) = find_first_non_existent_component(&subpath)
&& is_within_allowed_write_paths(&first_missing, &allowed_write_paths)
{
args.push("--ro-bind".to_string());
args.push("/dev/null".to_string());
args.push(path_to_string(&first_missing));
}
continue;
}
if is_within_allowed_write_paths(&subpath, &allowed_write_paths) {
args.push("--ro-bind".to_string());
args.push(path_to_string(&subpath));
args.push(path_to_string(&subpath));
}
}
// Ensure `/dev/null` remains usable regardless of the root bind.
args.push("--dev-bind".to_string());
args.push("/dev/null".to_string());
args.push("/dev/null".to_string());
Ok(args)
}
/// Collect unique read-only subpaths across all writable roots.
fn collect_read_only_subpaths(writable_roots: &[WritableRoot]) -> Vec<PathBuf> {
let mut subpaths: BTreeSet<PathBuf> = BTreeSet::new();
for writable_root in writable_roots {
for subpath in &writable_root.read_only_subpaths {
subpaths.insert(subpath.as_path().to_path_buf());
}
}
subpaths.into_iter().collect()
}
/// Validate that writable roots exist before constructing mounts.
///
/// Bubblewrap requires bind mount targets to exist. We fail fast with a clear
/// error so callers can present an actionable message.
fn ensure_mount_targets_exist(writable_roots: &[WritableRoot]) -> Result<()> {
for writable_root in writable_roots {
let root = writable_root.root.as_path();
if !root.exists() {
return Err(CodexErr::UnsupportedOperation(format!(
"Sandbox expected writable root {root}, but it does not exist.",
root = root.display()
)));
}
}
Ok(())
}
fn path_to_string(path: &Path) -> String {
path.to_string_lossy().to_string()
}
/// Returns true when `path` is under any allowed writable root.
fn is_within_allowed_write_paths(path: &Path, allowed_write_paths: &[PathBuf]) -> bool {
allowed_write_paths
.iter()
.any(|root| path.starts_with(root))
}
/// Find the first symlink along `target_path` that is also under a writable root.
///
/// This blocks symlink replacement attacks where a protected path is a symlink
/// inside a writable root (e.g., `.codex -> ./decoy`). In that case we mount
/// `/dev/null` on the symlink itself to prevent rewiring it.
fn find_symlink_in_path(target_path: &Path, allowed_write_paths: &[PathBuf]) -> Option<PathBuf> {
let mut current = PathBuf::new();
for component in target_path.components() {
use std::path::Component;
match component {
Component::RootDir => {
current.push(Path::new("/"));
continue;
}
Component::CurDir => continue,
Component::ParentDir => {
current.pop();
continue;
}
Component::Normal(part) => current.push(part),
Component::Prefix(_) => continue,
}
let metadata = match std::fs::symlink_metadata(&current) {
Ok(metadata) => metadata,
Err(_) => break,
};
if metadata.file_type().is_symlink()
&& is_within_allowed_write_paths(&current, allowed_write_paths)
{
return Some(current);
}
}
None
}
/// Find the first missing path component while walking `target_path`.
///
/// Mounting `/dev/null` on the first missing component prevents the sandboxed
/// process from creating the protected path hierarchy.
fn find_first_non_existent_component(target_path: &Path) -> Option<PathBuf> {
let mut current = PathBuf::new();
for component in target_path.components() {
use std::path::Component;
match component {
Component::RootDir => {
current.push(Path::new("/"));
continue;
}
Component::CurDir => continue,
Component::ParentDir => {
current.pop();
continue;
}
Component::Normal(part) => current.push(part),
Component::Prefix(_) => continue,
}
if !current.exists() {
return Some(current);
}
}
None
}

View File

@@ -1,3 +1,7 @@
//! In-process Linux sandbox primitives: `no_new_privs` and seccomp.
//!
//! Filesystem restrictions are enforced by bubblewrap in `linux_run_main`.
//! Landlock helpers remain available here as legacy/backup utilities.
use std::collections::BTreeMap;
use std::path::Path;
@@ -8,6 +12,7 @@ use codex_core::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use landlock::ABI;
#[allow(unused_imports)]
use landlock::Access;
use landlock::AccessFs;
use landlock::CompatLevel;
@@ -27,11 +32,24 @@ use seccompiler::apply_filter;
/// Apply sandbox policies inside this thread so only the child inherits
/// them, not the entire CLI process.
///
/// This function is responsible for:
/// - enabling `PR_SET_NO_NEW_PRIVS` when restrictions apply, and
/// - installing the network seccomp filter when network access is disabled.
///
/// Filesystem restrictions are intentionally handled by bubblewrap.
pub(crate) fn apply_sandbox_policy_to_current_thread(
sandbox_policy: &SandboxPolicy,
cwd: &Path,
apply_landlock_fs: bool,
) -> Result<()> {
if !sandbox_policy.has_full_disk_write_access() || !sandbox_policy.has_full_network_access() {
// `PR_SET_NO_NEW_PRIVS` is required for seccomp, but it also prevents
// setuid privilege elevation. Many `bwrap` deployments rely on setuid, so
// we avoid this unless we need seccomp or we are explicitly using the
// legacy Landlock filesystem pipeline.
if !sandbox_policy.has_full_network_access()
|| (apply_landlock_fs && !sandbox_policy.has_full_disk_write_access())
{
set_no_new_privs()?;
}
@@ -39,7 +57,7 @@ pub(crate) fn apply_sandbox_policy_to_current_thread(
install_network_seccomp_filter_on_current_thread()?;
}
if !sandbox_policy.has_full_disk_write_access() {
if apply_landlock_fs && !sandbox_policy.has_full_disk_write_access() {
let writable_roots = sandbox_policy
.get_writable_roots_with_cwd(cwd)
.into_iter()
@@ -54,6 +72,7 @@ pub(crate) fn apply_sandbox_policy_to_current_thread(
Ok(())
}
/// Enable `PR_SET_NO_NEW_PRIVS` so seccomp can be applied safely.
fn set_no_new_privs() -> Result<()> {
let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if result != 0 {
@@ -68,6 +87,9 @@ fn set_no_new_privs() -> Result<()> {
///
/// # Errors
/// Returns [`CodexErr::Sandbox`] variants when the ruleset fails to apply.
///
/// Note: this is currently unused because filesystem sandboxing is performed
/// via bubblewrap. It is kept for reference and potential fallback use.
fn install_filesystem_landlock_rules_on_current_thread(
writable_roots: Vec<AbsolutePathBuf>,
) -> Result<()> {
@@ -98,6 +120,9 @@ fn install_filesystem_landlock_rules_on_current_thread(
/// Installs a seccomp filter that blocks outbound network access except for
/// AF_UNIX domain sockets.
///
/// The filter is applied to the current thread so only the sandboxed child
/// inherits it.
fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(), SandboxErr> {
// Build rule map.
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();

View File

@@ -1,9 +1,16 @@
//! Linux sandbox helper entry point.
//!
//! On Linux, `codex-linux-sandbox` applies:
//! - in-process restrictions (`no_new_privs` + seccomp), and
//! - bubblewrap for filesystem isolation.
#[cfg(target_os = "linux")]
mod bwrap;
#[cfg(target_os = "linux")]
mod landlock;
#[cfg(target_os = "linux")]
mod linux_run_main;
#[cfg(target_os = "linux")]
mod mounts;
mod vendored_bwrap;
#[cfg(target_os = "linux")]
pub fn run_main() -> ! {

View File

@@ -1,10 +1,22 @@
use clap::Parser;
use std::ffi::CString;
use std::fs::File;
use std::io::Read;
use std::os::fd::FromRawFd;
use std::path::Path;
use std::path::PathBuf;
use crate::bwrap::BwrapOptions;
use crate::bwrap::create_bwrap_command_args;
use crate::landlock::apply_sandbox_policy_to_current_thread;
use crate::vendored_bwrap::exec_vendored_bwrap;
use crate::vendored_bwrap::run_vendored_bwrap_main;
#[derive(Debug, Parser)]
/// CLI surface for the Linux sandbox helper.
///
/// The type name remains `LandlockCommand` for compatibility with existing
/// wiring, but the filesystem sandbox now uses bubblewrap.
pub struct LandlockCommand {
/// It is possible that the cwd used in the context of the sandbox policy
/// is different from the cwd of the process to spawn.
@@ -14,26 +26,301 @@ pub struct LandlockCommand {
#[arg(long = "sandbox-policy")]
pub sandbox_policy: codex_core::protocol::SandboxPolicy,
/// Full command args to run under landlock.
/// Opt-in: use the bubblewrap-based Linux sandbox pipeline.
///
/// When not set, we fall back to the legacy Landlock + mount pipeline.
#[arg(long = "use-bwrap-sandbox", hide = true, default_value_t = false)]
pub use_bwrap_sandbox: bool,
/// Experimental: call a build-time bubblewrap `main()` via FFI.
///
/// This is opt-in and only works when the build script compiles bwrap.
#[arg(long = "use-vendored-bwrap", hide = true, default_value_t = false)]
pub use_vendored_bwrap: bool,
/// Internal: apply seccomp and `no_new_privs` in the already-sandboxed
/// process, then exec the user command.
///
/// This exists so we can run bubblewrap first (which may rely on setuid)
/// and only tighten with seccomp after the filesystem view is established.
#[arg(long = "apply-seccomp-then-exec", hide = true, default_value_t = false)]
pub apply_seccomp_then_exec: bool,
/// When set, skip mounting a fresh `/proc` even though PID isolation is
/// still enabled. This is primarily intended for restrictive container
/// environments that deny `--proc /proc`.
#[arg(long = "no-proc", default_value_t = false)]
pub no_proc: bool,
/// Full command args to run under the Linux sandbox helper.
#[arg(trailing_var_arg = true)]
pub command: Vec<String>,
}
/// Entry point for the Linux sandbox helper.
///
/// The sequence is:
/// 1. When needed, wrap the command with bubblewrap to construct the
/// filesystem view.
/// 2. Apply in-process restrictions (no_new_privs + seccomp).
/// 3. `execvp` into the final command.
pub fn run_main() -> ! {
let LandlockCommand {
sandbox_policy_cwd,
sandbox_policy,
use_bwrap_sandbox,
use_vendored_bwrap,
apply_seccomp_then_exec,
no_proc,
command,
} = LandlockCommand::parse();
if let Err(e) = apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd) {
panic!("error running landlock: {e:?}");
}
let use_bwrap_sandbox = use_bwrap_sandbox || use_vendored_bwrap;
if command.is_empty() {
panic!("No command specified to execute.");
}
// Inner stage: apply seccomp/no_new_privs after bubblewrap has already
// established the filesystem view.
if apply_seccomp_then_exec {
if let Err(e) =
apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, false)
{
panic!("error applying Linux sandbox restrictions: {e:?}");
}
exec_or_panic(command);
}
if sandbox_policy.has_full_disk_write_access() {
if let Err(e) =
apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, false)
{
panic!("error applying Linux sandbox restrictions: {e:?}");
}
exec_or_panic(command);
}
if use_bwrap_sandbox {
// Outer stage: bubblewrap first, then re-enter this binary in the
// sandboxed environment to apply seccomp. This path never falls back
// to legacy Landlock on failure.
let inner = build_inner_seccomp_command(
&sandbox_policy_cwd,
&sandbox_policy,
use_bwrap_sandbox,
command,
);
run_bwrap_with_proc_fallback(&sandbox_policy_cwd, &sandbox_policy, inner, !no_proc);
}
// Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled.
if let Err(e) =
apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, true)
{
panic!("error applying legacy Linux sandbox restrictions: {e:?}");
}
exec_or_panic(command);
}
fn run_bwrap_with_proc_fallback(
sandbox_policy_cwd: &Path,
sandbox_policy: &codex_core::protocol::SandboxPolicy,
inner: Vec<String>,
mount_proc: bool,
) -> ! {
let mut mount_proc = mount_proc;
if mount_proc && !preflight_proc_mount_support(sandbox_policy_cwd, sandbox_policy) {
eprintln!("codex-linux-sandbox: bwrap could not mount /proc; retrying with --no-proc");
mount_proc = false;
}
let options = BwrapOptions { mount_proc };
let argv = build_bwrap_argv(inner, sandbox_policy, sandbox_policy_cwd, options);
exec_vendored_bwrap(argv);
}
fn build_bwrap_argv(
inner: Vec<String>,
sandbox_policy: &codex_core::protocol::SandboxPolicy,
sandbox_policy_cwd: &Path,
options: BwrapOptions,
) -> Vec<String> {
let mut args = create_bwrap_command_args(inner, sandbox_policy, sandbox_policy_cwd, options)
.unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}"));
let command_separator_index = args
.iter()
.position(|arg| arg == "--")
.unwrap_or_else(|| panic!("bubblewrap argv is missing command separator '--'"));
args.splice(
command_separator_index..command_separator_index,
["--argv0".to_string(), "codex-linux-sandbox".to_string()],
);
let mut argv = vec!["bwrap".to_string()];
argv.extend(args);
argv
}
fn preflight_proc_mount_support(
sandbox_policy_cwd: &Path,
sandbox_policy: &codex_core::protocol::SandboxPolicy,
) -> bool {
let preflight_command = vec![resolve_true_command()];
let preflight_argv = build_bwrap_argv(
preflight_command,
sandbox_policy,
sandbox_policy_cwd,
BwrapOptions { mount_proc: true },
);
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
!is_proc_mount_failure(stderr.as_str())
}
fn resolve_true_command() -> String {
for candidate in ["/usr/bin/true", "/bin/true"] {
if Path::new(candidate).exists() {
return candidate.to_string();
}
}
"true".to_string()
}
fn run_bwrap_in_child_capture_stderr(argv: Vec<String>) -> String {
let mut pipe_fds = [0; 2];
let pipe_res = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) };
if pipe_res < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to create stderr pipe for bubblewrap: {err}");
}
let read_fd = pipe_fds[0];
let write_fd = pipe_fds[1];
let pid = unsafe { libc::fork() };
if pid < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to fork for bubblewrap: {err}");
}
if pid == 0 {
// Child: redirect stderr to the pipe, then run bubblewrap.
unsafe {
libc::close(read_fd);
if libc::dup2(write_fd, libc::STDERR_FILENO) < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to redirect stderr for bubblewrap: {err}");
}
libc::close(write_fd);
}
let exit_code = run_vendored_bwrap_main(&argv);
std::process::exit(exit_code);
}
// Parent: close the write end and read stderr while the child runs.
unsafe {
libc::close(write_fd);
}
let mut stderr = String::new();
// SAFETY: `read_fd` is a valid owned fd in the parent.
let mut read_file = unsafe { File::from_raw_fd(read_fd) };
if let Err(err) = read_file.read_to_string(&mut stderr) {
panic!("failed to read bubblewrap stderr: {err}");
}
let mut status: libc::c_int = 0;
let wait_res = unsafe { libc::waitpid(pid, &mut status as *mut libc::c_int, 0) };
if wait_res < 0 {
let err = std::io::Error::last_os_error();
panic!("waitpid failed for bubblewrap child: {err}");
}
stderr
}
fn is_proc_mount_failure(stderr: &str) -> bool {
stderr.contains("Can't mount proc")
&& stderr.contains("/newroot/proc")
&& stderr.contains("Invalid argument")
}
#[cfg(test)]
mod tests {
use super::*;
use codex_core::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
#[test]
fn detects_proc_mount_invalid_argument_failure() {
let stderr = "bwrap: Can't mount proc on /newroot/proc: Invalid argument";
assert_eq!(is_proc_mount_failure(stderr), true);
}
#[test]
fn ignores_non_proc_mount_errors() {
let stderr = "bwrap: Can't bind mount /dev/null: Operation not permitted";
assert_eq!(is_proc_mount_failure(stderr), false);
}
#[test]
fn inserts_bwrap_argv0_before_command_separator() {
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&SandboxPolicy::ReadOnly,
Path::new("/"),
BwrapOptions { mount_proc: true },
);
let command_separator_index = argv
.iter()
.position(|arg| arg == "--")
.expect("expected command separator in bubblewrap argv");
let argv0_flag_index = argv
.iter()
.position(|arg| arg == "--argv0")
.expect("expected --argv0 in bubblewrap argv");
assert_eq!(argv[0], "bwrap");
assert_eq!(argv[argv0_flag_index + 1], "codex-linux-sandbox");
assert_eq!(argv0_flag_index < command_separator_index, true);
}
}
/// Build the inner command that applies seccomp after bubblewrap.
fn build_inner_seccomp_command(
sandbox_policy_cwd: &Path,
sandbox_policy: &codex_core::protocol::SandboxPolicy,
use_bwrap_sandbox: bool,
command: Vec<String>,
) -> Vec<String> {
let current_exe = match std::env::current_exe() {
Ok(path) => path,
Err(err) => panic!("failed to resolve current executable path: {err}"),
};
let policy_json = match serde_json::to_string(sandbox_policy) {
Ok(json) => json,
Err(err) => panic!("failed to serialize sandbox policy: {err}"),
};
let mut inner = vec![
current_exe.to_string_lossy().to_string(),
"--sandbox-policy-cwd".to_string(),
sandbox_policy_cwd.to_string_lossy().to_string(),
"--sandbox-policy".to_string(),
policy_json,
];
if use_bwrap_sandbox {
inner.push("--use-bwrap-sandbox".to_string());
inner.push("--apply-seccomp-then-exec".to_string());
}
inner.push("--".to_string());
inner.extend(command);
inner
}
/// Exec the provided argv, panicking with context if it fails.
fn exec_or_panic(command: Vec<String>) -> ! {
#[expect(clippy::expect_used)]
let c_command =
CString::new(command[0].as_str()).expect("Failed to convert command to CString");

View File

@@ -1,339 +0,0 @@
#![allow(dead_code)]
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use codex_core::error::CodexErr;
use codex_core::error::Result;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::WritableRoot;
use codex_utils_absolute_path::AbsolutePathBuf;
/// Apply read-only bind mounts for protected subpaths before Landlock.
///
/// This unshares mount namespaces (and user namespaces for non-root) so the
/// read-only remounts do not affect the host, then bind-mounts each protected
/// target onto itself and remounts it read-only.
pub(crate) fn apply_read_only_mounts(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result<()> {
let writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
let mount_targets = collect_read_only_mount_targets(&writable_roots)?;
if mount_targets.is_empty() {
return Ok(());
}
// Root can unshare the mount namespace directly; non-root needs a user
// namespace to gain capabilities for remounting.
if is_running_as_root() {
unshare_mount_namespace()?;
} else {
let original_euid = unsafe { libc::geteuid() };
let original_egid = unsafe { libc::getegid() };
unshare_user_and_mount_namespaces()?;
write_user_namespace_maps(original_euid, original_egid)?;
}
make_mounts_private()?;
for target in mount_targets {
// Bind and remount read-only works for both files and directories.
bind_mount_read_only(target.as_path())?;
}
// Drop ambient capabilities acquired from the user namespace so the
// sandboxed command cannot remount or create new bind mounts.
if !is_running_as_root() {
drop_caps()?;
}
Ok(())
}
/// Collect read-only mount targets, resolving worktree `.git` pointer files.
fn collect_read_only_mount_targets(
writable_roots: &[WritableRoot],
) -> Result<Vec<AbsolutePathBuf>> {
let mut targets = Vec::new();
for writable_root in writable_roots {
for ro_subpath in &writable_root.read_only_subpaths {
// The policy expects these paths to exist; surface actionable errors
// rather than silently skipping protections.
if !ro_subpath.as_path().exists() {
return Err(CodexErr::UnsupportedOperation(format!(
"Sandbox expected to protect {path}, but it does not exist. Ensure the repository contains this path or create it before running Codex.",
path = ro_subpath.as_path().display()
)));
}
targets.push(ro_subpath.clone());
// Worktrees and submodules store `.git` as a pointer file; add the
// referenced gitdir as an extra read-only target.
if is_git_pointer_file(ro_subpath) {
let gitdir = resolve_gitdir_from_file(ro_subpath)?;
if !targets
.iter()
.any(|target| target.as_path() == gitdir.as_path())
{
targets.push(gitdir);
}
}
}
}
Ok(targets)
}
/// Detect a `.git` pointer file used by worktrees and submodules.
fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool {
path.as_path().is_file() && path.as_path().file_name() == Some(std::ffi::OsStr::new(".git"))
}
/// Resolve a worktree `.git` pointer file to its gitdir path.
fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Result<AbsolutePathBuf> {
let contents = std::fs::read_to_string(dot_git.as_path()).map_err(CodexErr::from)?;
let trimmed = contents.trim();
let (_, gitdir_raw) = trimmed.split_once(':').ok_or_else(|| {
CodexErr::UnsupportedOperation(format!(
"Expected {path} to contain a gitdir pointer, but it did not match `gitdir: <path>`.",
path = dot_git.as_path().display()
))
})?;
// `gitdir: <path>` may be relative to the directory containing `.git`.
let gitdir_raw = gitdir_raw.trim();
if gitdir_raw.is_empty() {
return Err(CodexErr::UnsupportedOperation(format!(
"Expected {path} to contain a gitdir pointer, but it was empty.",
path = dot_git.as_path().display()
)));
}
let base = dot_git.as_path().parent().ok_or_else(|| {
CodexErr::UnsupportedOperation(format!(
"Unable to resolve parent directory for {path}.",
path = dot_git.as_path().display()
))
})?;
let gitdir_path = AbsolutePathBuf::resolve_path_against_base(gitdir_raw, base)?;
if !gitdir_path.as_path().exists() {
return Err(CodexErr::UnsupportedOperation(format!(
"Resolved gitdir path {path} does not exist.",
path = gitdir_path.as_path().display()
)));
}
Ok(gitdir_path)
}
/// Unshare the mount namespace so mount changes are isolated to the sandboxed process.
fn unshare_mount_namespace() -> Result<()> {
let result = unsafe { libc::unshare(libc::CLONE_NEWNS) };
if result != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
/// Unshare user + mount namespaces so the process can remount read-only without privileges.
fn unshare_user_and_mount_namespaces() -> Result<()> {
let result = unsafe { libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) };
if result != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
fn is_running_as_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
#[repr(C)]
struct CapUserHeader {
version: u32,
pid: i32,
}
#[repr(C)]
struct CapUserData {
effective: u32,
permitted: u32,
inheritable: u32,
}
const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
/// Map the provided uid/gid to root inside the user namespace.
fn write_user_namespace_maps(uid: libc::uid_t, gid: libc::gid_t) -> Result<()> {
write_proc_file("/proc/self/setgroups", "deny\n")?;
write_proc_file("/proc/self/uid_map", format!("0 {uid} 1\n"))?;
write_proc_file("/proc/self/gid_map", format!("0 {gid} 1\n"))?;
Ok(())
}
/// Drop all capabilities in the current user namespace.
fn drop_caps() -> Result<()> {
let mut header = CapUserHeader {
version: LINUX_CAPABILITY_VERSION_3,
pid: 0,
};
let data = [
CapUserData {
effective: 0,
permitted: 0,
inheritable: 0,
},
CapUserData {
effective: 0,
permitted: 0,
inheritable: 0,
},
];
// Use syscall directly to avoid libc capability symbols that are missing on musl.
let result = unsafe { libc::syscall(libc::SYS_capset, &mut header, data.as_ptr()) };
if result != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
/// Write a small procfs file, returning a sandbox error on failure.
fn write_proc_file(path: &str, contents: impl AsRef<[u8]>) -> Result<()> {
std::fs::write(path, contents)?;
Ok(())
}
/// Ensure mounts are private so remounting does not propagate outside the namespace.
fn make_mounts_private() -> Result<()> {
let root = CString::new("/").map_err(|_| {
CodexErr::UnsupportedOperation("Sandbox mount path contains NUL byte: /".to_string())
})?;
let result = unsafe {
libc::mount(
std::ptr::null(),
root.as_ptr(),
std::ptr::null(),
libc::MS_REC | libc::MS_PRIVATE,
std::ptr::null(),
)
};
if result != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
/// Bind-mount a path onto itself and remount read-only.
fn bind_mount_read_only(path: &Path) -> Result<()> {
let c_path = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
CodexErr::UnsupportedOperation(format!(
"Sandbox mount path contains NUL byte: {path}",
path = path.display()
))
})?;
let bind_result = unsafe {
libc::mount(
c_path.as_ptr(),
c_path.as_ptr(),
std::ptr::null(),
libc::MS_BIND,
std::ptr::null(),
)
};
if bind_result != 0 {
return Err(std::io::Error::last_os_error().into());
}
let remount_result = unsafe {
libc::mount(
c_path.as_ptr(),
c_path.as_ptr(),
std::ptr::null(),
libc::MS_BIND | libc::MS_REMOUNT | libc::MS_RDONLY,
std::ptr::null(),
)
};
if remount_result != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn collect_read_only_mount_targets_errors_on_missing_path() {
let tempdir = tempfile::tempdir().expect("tempdir");
let missing = AbsolutePathBuf::try_from(tempdir.path().join("missing").as_path())
.expect("missing path");
let root = AbsolutePathBuf::try_from(tempdir.path()).expect("root");
let writable_root = WritableRoot {
root,
read_only_subpaths: vec![missing],
};
let err = collect_read_only_mount_targets(&[writable_root])
.expect_err("expected missing path error");
let message = match err {
CodexErr::UnsupportedOperation(message) => message,
other => panic!("unexpected error: {other:?}"),
};
assert_eq!(
message,
format!(
"Sandbox expected to protect {path}, but it does not exist. Ensure the repository contains this path or create it before running Codex.",
path = tempdir.path().join("missing").display()
)
);
}
#[test]
fn collect_read_only_mount_targets_adds_gitdir_for_pointer_file() {
let tempdir = tempfile::tempdir().expect("tempdir");
let gitdir = tempdir.path().join("actual-gitdir");
std::fs::create_dir_all(&gitdir).expect("create gitdir");
let dot_git = tempdir.path().join(".git");
std::fs::write(&dot_git, format!("gitdir: {}\n", gitdir.display()))
.expect("write gitdir pointer");
let root = AbsolutePathBuf::try_from(tempdir.path()).expect("root");
let writable_root = WritableRoot {
root,
read_only_subpaths: vec![
AbsolutePathBuf::try_from(dot_git.as_path()).expect("dot git"),
],
};
let targets = collect_read_only_mount_targets(&[writable_root]).expect("collect targets");
assert_eq!(targets.len(), 2);
assert_eq!(targets[0].as_path(), dot_git.as_path());
assert_eq!(targets[1].as_path(), gitdir.as_path());
}
#[test]
fn collect_read_only_mount_targets_errors_on_invalid_gitdir_pointer() {
let tempdir = tempfile::tempdir().expect("tempdir");
let dot_git = tempdir.path().join(".git");
std::fs::write(&dot_git, "not-a-pointer\n").expect("write invalid pointer");
let root = AbsolutePathBuf::try_from(tempdir.path()).expect("root");
let writable_root = WritableRoot {
root,
read_only_subpaths: vec![
AbsolutePathBuf::try_from(dot_git.as_path()).expect("dot git"),
],
};
let err = collect_read_only_mount_targets(&[writable_root])
.expect_err("expected invalid pointer error");
let message = match err {
CodexErr::UnsupportedOperation(message) => message,
other => panic!("unexpected error: {other:?}"),
};
assert_eq!(
message,
format!(
"Expected {path} to contain a gitdir pointer, but it did not match `gitdir: <path>`.",
path = dot_git.display()
)
);
}
}

View File

@@ -0,0 +1,74 @@
//! Build-time bubblewrap entrypoint.
//!
//! This module is intentionally behind a build-time opt-in. When enabled, the
//! build script compiles bubblewrap's C sources and exposes a `bwrap_main`
//! symbol that we can call via FFI.
#[cfg(vendored_bwrap_available)]
mod imp {
use std::ffi::CString;
use std::os::raw::c_char;
unsafe extern "C" {
fn bwrap_main(argc: libc::c_int, argv: *const *const c_char) -> libc::c_int;
}
fn argv_to_cstrings(argv: &[String]) -> Vec<CString> {
let mut cstrings: Vec<CString> = Vec::with_capacity(argv.len());
for arg in argv {
match CString::new(arg.as_str()) {
Ok(value) => cstrings.push(value),
Err(err) => panic!("failed to convert argv to CString: {err}"),
}
}
cstrings
}
/// Run the build-time bubblewrap `main` function and return its exit code.
///
/// On success, bubblewrap will `execve` into the target program and this
/// function will never return. A return value therefore implies failure.
pub(crate) fn run_vendored_bwrap_main(argv: &[String]) -> libc::c_int {
let cstrings = argv_to_cstrings(argv);
let mut argv_ptrs: Vec<*const c_char> = cstrings.iter().map(|arg| arg.as_ptr()).collect();
argv_ptrs.push(std::ptr::null());
// SAFETY: We provide a null-terminated argv vector whose pointers
// remain valid for the duration of the call.
unsafe { bwrap_main(cstrings.len() as libc::c_int, argv_ptrs.as_ptr()) }
}
/// Execute the build-time bubblewrap `main` function with the given argv.
pub(crate) fn exec_vendored_bwrap(argv: Vec<String>) -> ! {
let exit_code = run_vendored_bwrap_main(&argv);
std::process::exit(exit_code);
}
}
#[cfg(not(vendored_bwrap_available))]
mod imp {
/// Panics with a clear error when the build-time bwrap path is not enabled.
pub(crate) fn run_vendored_bwrap_main(_argv: &[String]) -> libc::c_int {
panic!(
"build-time bubblewrap is not available in this build.\n\
Rebuild codex-linux-sandbox on Linux with CODEX_BWRAP_ENABLE_FFI=1.\n\
Example:\n\
- cd codex-rs && CODEX_BWRAP_ENABLE_FFI=1 cargo build -p codex-linux-sandbox\n\
If this crate was already built without it, run:\n\
- cargo clean -p codex-linux-sandbox\n\
Notes:\n\
- libcap headers must be available via pkg-config\n\
- bubblewrap sources expected at codex-rs/vendor/bubblewrap (default)"
);
}
/// Panics with a clear error when the build-time bwrap path is not enabled.
pub(crate) fn exec_vendored_bwrap(_argv: Vec<String>) -> ! {
let _ = run_vendored_bwrap_main(&[]);
unreachable!("run_vendored_bwrap_main should always panic in this configuration")
}
}
pub(crate) use imp::exec_vendored_bwrap;
pub(crate) use imp::run_vendored_bwrap_main;

View File

@@ -2,6 +2,7 @@
#![allow(clippy::unwrap_used)]
use codex_core::config::types::ShellEnvironmentPolicy;
use codex_core::error::CodexErr;
use codex_core::error::Result;
use codex_core::error::SandboxErr;
use codex_core::exec::ExecParams;
use codex_core::exec::process_exec_tool_call;
@@ -47,12 +48,23 @@ async fn run_cmd(cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64) {
}
}
#[expect(clippy::expect_used, clippy::unwrap_used)]
#[expect(clippy::expect_used)]
async fn run_cmd_output(
cmd: &[&str],
writable_roots: &[PathBuf],
timeout_ms: u64,
) -> codex_core::exec::ExecToolCallOutput {
run_cmd_result(cmd, writable_roots, timeout_ms)
.await
.expect("sandboxed command should succeed")
}
#[expect(clippy::expect_used)]
async fn run_cmd_result(
cmd: &[&str],
writable_roots: &[PathBuf],
timeout_ms: u64,
) -> Result<codex_core::exec::ExecToolCallOutput> {
let cwd = std::env::current_dir().expect("cwd should exist");
let sandbox_cwd = cwd.clone();
let params = ExecParams {
@@ -86,10 +98,24 @@ async fn run_cmd_output(
&sandbox_policy,
sandbox_cwd.as_path(),
&codex_linux_sandbox_exe,
false,
None,
)
.await
.unwrap()
}
fn expect_denied(
result: Result<codex_core::exec::ExecToolCallOutput>,
context: &str,
) -> codex_core::exec::ExecToolCallOutput {
match result {
Ok(output) => {
assert_ne!(output.exit_code, 0, "{context}: expected nonzero exit code");
output
}
Err(CodexErr::Sandbox(SandboxErr::Denied { output })) => *output,
Err(err) => panic!("{context}: {err:?}"),
}
}
#[tokio::test]
@@ -192,6 +218,7 @@ async fn assert_network_blocked(cmd: &[&str]) {
&sandbox_policy,
sandbox_cwd.as_path(),
&codex_linux_sandbox_exe,
false,
None,
)
.await;
@@ -242,6 +269,77 @@ async fn sandbox_blocks_nc() {
assert_network_blocked(&["nc", "-z", "127.0.0.1", "80"]).await;
}
#[tokio::test]
async fn sandbox_blocks_git_and_codex_writes_inside_writable_root() {
let tmpdir = tempfile::tempdir().expect("tempdir");
let dot_git = tmpdir.path().join(".git");
let dot_codex = tmpdir.path().join(".codex");
std::fs::create_dir_all(&dot_git).expect("create .git");
std::fs::create_dir_all(&dot_codex).expect("create .codex");
let git_target = dot_git.join("config");
let codex_target = dot_codex.join("config.toml");
let git_output = expect_denied(
run_cmd_result(
&[
"bash",
"-lc",
&format!("echo denied > {}", git_target.to_string_lossy()),
],
&[tmpdir.path().to_path_buf()],
LONG_TIMEOUT_MS,
)
.await,
".git write should be denied under bubblewrap",
);
let codex_output = expect_denied(
run_cmd_result(
&[
"bash",
"-lc",
&format!("echo denied > {}", codex_target.to_string_lossy()),
],
&[tmpdir.path().to_path_buf()],
LONG_TIMEOUT_MS,
)
.await,
".codex write should be denied under bubblewrap",
);
assert_ne!(git_output.exit_code, 0);
assert_ne!(codex_output.exit_code, 0);
}
#[tokio::test]
async fn sandbox_blocks_codex_symlink_replacement_attack() {
use std::os::unix::fs::symlink;
let tmpdir = tempfile::tempdir().expect("tempdir");
let decoy = tmpdir.path().join("decoy-codex");
std::fs::create_dir_all(&decoy).expect("create decoy dir");
let dot_codex = tmpdir.path().join(".codex");
symlink(&decoy, &dot_codex).expect("create .codex symlink");
let codex_target = dot_codex.join("config.toml");
let codex_output = expect_denied(
run_cmd_result(
&[
"bash",
"-lc",
&format!("echo denied > {}", codex_target.to_string_lossy()),
],
&[tmpdir.path().to_path_buf()],
LONG_TIMEOUT_MS,
)
.await,
".codex symlink replacement should be denied",
);
assert_ne!(codex_output.exit_code, 0);
}
#[tokio::test]
async fn sandbox_blocks_ssh() {
// Force ssh to attempt a real TCP connection but fail quickly. `BatchMode`

View File

@@ -0,0 +1 @@
((c-mode . ((indent-tabs-mode . nil) (c-file-style . "gnu"))))

View File

@@ -0,0 +1,5 @@
[*.[ch]]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
indent_brace_style = gnu

View File

@@ -0,0 +1,105 @@
name: CI checks
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
meson:
name: Build with Meson and gcc, and test
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4
- name: Install build-dependencies
run: sudo ./ci/builddeps.sh
- name: Enable user namespaces
run: sudo ./ci/enable-userns.sh
- name: Create logs dir
run: mkdir test-logs
- name: setup
run: |
meson _build
env:
CFLAGS: >-
-O2
-Wp,-D_FORTIFY_SOURCE=2
-fsanitize=address
-fsanitize=undefined
- name: compile
run: ninja -C _build -v
- name: smoke-test
run: |
set -x
./_build/bwrap --bind / / --tmpfs /tmp true
env:
ASAN_OPTIONS: detect_leaks=0
- name: test
run: |
BWRAP_MUST_WORK=1 meson test -C _build
env:
ASAN_OPTIONS: detect_leaks=0
- name: Collect overall test logs on failure
if: failure()
run: mv _build/meson-logs/testlog.txt test-logs/ || true
- name: install
run: |
DESTDIR="$(pwd)/DESTDIR" meson install -C _build
( cd DESTDIR && find -ls )
- name: dist
run: |
BWRAP_MUST_WORK=1 meson dist -C _build
- name: Collect dist test logs on failure
if: failure()
run: mv _build/meson-private/dist-build/meson-logs/testlog.txt test-logs/disttestlog.txt || true
- name: use as subproject
run: |
mkdir tests/use-as-subproject/subprojects
tar -C tests/use-as-subproject/subprojects -xf _build/meson-dist/bubblewrap-*.tar.xz
mv tests/use-as-subproject/subprojects/bubblewrap-* tests/use-as-subproject/subprojects/bubblewrap
( cd tests/use-as-subproject && meson _build )
ninja -C tests/use-as-subproject/_build -v
meson test -C tests/use-as-subproject/_build
DESTDIR="$(pwd)/DESTDIR-as-subproject" meson install -C tests/use-as-subproject/_build
( cd DESTDIR-as-subproject && find -ls )
test -x DESTDIR-as-subproject/usr/local/libexec/not-flatpak-bwrap
test ! -e DESTDIR-as-subproject/usr/local/bin/bwrap
test ! -e DESTDIR-as-subproject/usr/local/libexec/bwrap
tests/use-as-subproject/assert-correct-rpath.py DESTDIR-as-subproject/usr/local/libexec/not-flatpak-bwrap
- name: Upload test logs
uses: actions/upload-artifact@v4
if: failure() || cancelled()
with:
name: test logs
path: test-logs
clang:
name: Build with clang and analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language:
- cpp
steps:
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Check out
uses: actions/checkout@v4
- name: Install build-dependencies
run: sudo ./ci/builddeps.sh --clang
- run: meson build -Dselinux=enabled
env:
CC: clang
CFLAGS: >-
-O2
-Werror=unused-variable
- run: meson compile -C build
- name: CodeQL analysis
uses: github/codeql-action/analyze@v2

View File

@@ -0,0 +1,3 @@
## The bubblewrap Project Community Code of Conduct
The bubblewrap project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/HEAD/CODE-OF-CONDUCT.md).

481
codex-rs/vendor/bubblewrap/COPYING vendored Normal file
View File

@@ -0,0 +1,481 @@
GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the library GPL. It is
numbered 2 because it goes with version 2 of the ordinary GPL.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Library General Public License, applies to some
specially designated Free Software Foundation software, and to any
other libraries whose authors decide to use it. You can use it for
your libraries, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if
you distribute copies of the library, or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link a program with the library, you must provide
complete object files to the recipients so that they can relink them
with the library, after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
Our method of protecting your rights has two steps: (1) copyright
the library, and (2) offer you this license which gives you legal
permission to copy, distribute and/or modify the library.
Also, for each distributor's protection, we want to make certain
that everyone understands that there is no warranty for this free
library. If the library is modified by someone else and passed on, we
want its recipients to know that what they have is not the original
version, so that any problems introduced by others will not reflect on
the original authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that companies distributing free
software will individually obtain patent licenses, thus in effect
transforming the program into proprietary software. To prevent this,
we have made it clear that any patent must be licensed for everyone's
free use or not licensed at all.
Most GNU software, including some libraries, is covered by the ordinary
GNU General Public License, which was designed for utility programs. This
license, the GNU Library General Public License, applies to certain
designated libraries. This license is quite different from the ordinary
one; be sure to read it in full, and don't assume that anything in it is
the same as in the ordinary license.
The reason we have a separate public license for some libraries is that
they blur the distinction we usually make between modifying or adding to a
program and simply using it. Linking a program with a library, without
changing the library, is in some sense simply using the library, and is
analogous to running a utility program or application program. However, in
a textual and legal sense, the linked executable is a combined work, a
derivative of the original library, and the ordinary General Public License
treats it as such.
Because of this blurred distinction, using the ordinary General
Public License for libraries did not effectively promote software
sharing, because most developers did not use the libraries. We
concluded that weaker conditions might promote sharing better.
However, unrestricted linking of non-free programs would deprive the
users of those programs of all benefit from the free status of the
libraries themselves. This Library General Public License is intended to
permit developers of non-free programs to use free libraries, while
preserving your freedom as a user of such programs to change the free
libraries that are incorporated in them. (We have not seen how to achieve
this as regards changes in header files, but we have achieved it as regards
changes in the actual functions of the Library.) The hope is that this
will lead to faster development of free libraries.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, while the latter only
works together with the library.
Note that it is possible for a library to be covered by the ordinary
General Public License rather than by this special one.
GNU LIBRARY GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library which
contains a notice placed by the copyright holder or other authorized
party saying it may be distributed under the terms of this Library
General Public License (also called "this License"). Each licensee is
addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also compile or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
c) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
d) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the source code distributed need not include anything that is normally
distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Library General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

1
codex-rs/vendor/bubblewrap/LICENSE vendored Symbolic link
View File

@@ -0,0 +1 @@
COPYING

51
codex-rs/vendor/bubblewrap/NEWS.md vendored Normal file
View File

@@ -0,0 +1,51 @@
bubblewrap 0.11.0
=================
Released: 2024-10-30
Dependencies:
* Remove the Autotools build system. Meson ≥ 0.49.0 is now required
at build-time. (#625, Hugo Osvaldo Barrera)
* For users of bash-completion, bash-completion ≥ 2.10 is recommended.
With older bash-completion, bubblewrap might install completions
outside its `${prefix}` unless overridden with `-Dbash_completion_dir=…`.
Enhancements:
* New `--overlay`, `--tmp-overlay`, `--ro-overlay` and `--overlay-src`
options allow creation of overlay mounts.
This feature is not available when bubblewrap is installed setuid.
(#412, #663; Ryan Hendrickson, William Manley, Simon McVittie)
* New `--level-prefix` option produces output that can be parsed by
tools like `logger --prio-prefix` and `systemd-cat --level-prefix=1`
(#646, Simon McVittie)
Bug fixes:
* Handle `EINTR` when doing I/O on files or sockets (#657, Simon McVittie)
* Don't make assumptions about alignment of socket control message data
(#637, Simon McVittie)
* Silence some Meson deprecation warnings (#647, @Sertonix)
* Update URLs in documentation to https (#566, @TotalCaesar659)
* Improve tests' compatibility with busybox (#627, @Sertonix)
* Improve compatibility with Meson < 1.3.0 (#664, Simon McVittie)
Internal changes:
* Consistently use `<stdbool.h>` for booleans (#660, Simon McVittie)
* Avoid `-Wshadow` compiler warnings (#661, Simon McVittie)
* Update Github Actions configuration (#658, Simon McVittie)
----
See also <https://github.com/containers/bubblewrap/releases>

255
codex-rs/vendor/bubblewrap/README.md vendored Normal file
View File

@@ -0,0 +1,255 @@
Bubblewrap
==========
Many container runtime tools like `systemd-nspawn`, `docker`,
etc. focus on providing infrastructure for system administrators and
orchestration tools (e.g. Kubernetes) to run containers.
These tools are not suitable to give to unprivileged users, because it
is trivial to turn such access into a fully privileged root shell
on the host.
User namespaces
---------------
There is an effort in the Linux kernel called
[user namespaces](https://www.google.com/search?q=user+namespaces+site%3Ahttps%3A%2F%2Flwn.net)
which attempts to allow unprivileged users to use container features.
While significant progress has been made, there are
[still concerns](https://lwn.net/Articles/673597/) about it, and
it is not available to unprivileged users in several production distributions
such as CentOS/Red Hat Enterprise Linux 7, Debian Jessie, etc.
See for example
[CVE-2016-3135](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-3135)
which is a local root vulnerability introduced by userns.
[This March 2016 post](https://lkml.org/lkml/2016/3/9/555) has some
more discussion.
Bubblewrap could be viewed as setuid implementation of a *subset* of
user namespaces. Emphasis on subset - specifically relevant to the
above CVE, bubblewrap does not allow control over iptables.
The original bubblewrap code existed before user namespaces - it inherits code from
[xdg-app helper](https://cgit.freedesktop.org/xdg-app/xdg-app/tree/common/xdg-app-helper.c?id=4c3bf179e2e4a2a298cd1db1d045adaf3f564532)
which in turn distantly derives from
[linux-user-chroot](https://git.gnome.org/browse/linux-user-chroot).
System security
---------------
The maintainers of this tool believe that it does not, even when used
in combination with typical software installed on that distribution,
allow privilege escalation. It may increase the ability of a logged
in user to perform denial of service attacks, however.
In particular, bubblewrap uses `PR_SET_NO_NEW_PRIVS` to turn off
setuid binaries, which is the [traditional way](https://en.wikipedia.org/wiki/Chroot#Limitations) to get out of things
like chroots.
Sandbox security
----------------
bubblewrap is a tool for constructing sandbox environments.
bubblewrap is not a complete, ready-made sandbox with a specific security
policy.
Some of bubblewrap's use-cases want a security boundary between the sandbox
and the real system; other use-cases want the ability to change the layout of
the filesystem for processes inside the sandbox, but do not aim to be a
security boundary.
As a result, the level of protection between the sandboxed processes and
the host system is entirely determined by the arguments passed to
bubblewrap.
Whatever program constructs the command-line arguments for bubblewrap
(often a larger framework like Flatpak, libgnome-desktop, sandwine
or an ad-hoc script) is responsible for defining its own security model,
and choosing appropriate bubblewrap command-line arguments to implement
that security model.
Some aspects of sandbox security that require particular care are described
in the [Limitations](#limitations) section below.
Users
-----
This program can be shared by all container tools which perform
non-root operation, such as:
- [Flatpak](https://www.flatpak.org)
- [rpm-ostree unprivileged](https://github.com/projectatomic/rpm-ostree/pull/209)
- [bwrap-oci](https://github.com/projectatomic/bwrap-oci)
We would also like to see this be available in Kubernetes/OpenShift
clusters. Having the ability for unprivileged users to use container
features would make it significantly easier to do interactive
debugging scenarios and the like.
Installation
------------
bubblewrap is available in the package repositories of the most Linux distributions
and can be installed from there.
If you need to build bubblewrap from source, you can do this with meson:
```sh
meson _builddir
meson compile -C _builddir
meson test -C _builddir
meson install -C _builddir
```
Usage
-----
bubblewrap works by creating a new, completely empty, mount
namespace where the root is on a tmpfs that is invisible from the
host, and will be automatically cleaned up when the last process
exits. You can then use commandline options to construct the root
filesystem and process environment and command to run in the
namespace.
There's a larger [demo script](./demos/bubblewrap-shell.sh) in the
source code, but here's a trimmed down version which runs
a new shell reusing the host's `/usr`.
```
bwrap \
--ro-bind /usr /usr \
--symlink usr/lib64 /lib64 \
--proc /proc \
--dev /dev \
--unshare-pid \
--new-session \
bash
```
This is an incomplete example, but useful for purposes of
illustration. More often, rather than creating a container using the
host's filesystem tree, you want to target a chroot. There, rather
than creating the symlink `lib64 -> usr/lib64` in the tmpfs, you might
have already created it in the target rootfs.
Sandboxing
----------
The goal of bubblewrap is to run an application in a sandbox, where it
has restricted access to parts of the operating system or user data
such as the home directory.
bubblewrap always creates a new mount namespace, and the user can specify
exactly what parts of the filesystem should be visible in the sandbox.
Any such directories you specify mounted `nodev` by default, and can be made readonly.
Additionally you can use these kernel features:
User namespaces ([CLONE_NEWUSER](https://linux.die.net/man/2/clone)): This hides all but the current uid and gid from the
sandbox. You can also change what the value of uid/gid should be in the sandbox.
IPC namespaces ([CLONE_NEWIPC](https://linux.die.net/man/2/clone)): The sandbox will get its own copy of all the
different forms of IPCs, like SysV shared memory and semaphores.
PID namespaces ([CLONE_NEWPID](https://linux.die.net/man/2/clone)): The sandbox will not see any processes outside the sandbox. Additionally, bubblewrap will run a trivial pid1 inside your container to handle the requirements of reaping children in the sandbox. This avoids what is known now as the [Docker pid 1 problem](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/).
Network namespaces ([CLONE_NEWNET](https://linux.die.net/man/2/clone)): The sandbox will not see the network. Instead it will have its own network namespace with only a loopback device.
UTS namespace ([CLONE_NEWUTS](https://linux.die.net/man/2/clone)): The sandbox will have its own hostname.
Seccomp filters: You can pass in seccomp filters that limit which syscalls can be done in the sandbox. For more information, see [Seccomp](https://en.wikipedia.org/wiki/Seccomp).
Limitations
-----------
As noted in the [Sandbox security](#sandbox-security) section above,
the level of protection between the sandboxed processes and the host system
is entirely determined by the arguments passed to bubblewrap.
Some aspects that require special care are noted here.
- If you are not filtering out `TIOCSTI` commands using seccomp filters,
argument `--new-session` is needed to protect against out-of-sandbox
command execution
(see [CVE-2017-5226](https://github.com/containers/bubblewrap/issues/142)).
- Everything mounted into the sandbox can potentially be used to escalate
privileges.
For example, if you bind a D-Bus socket into the sandbox, it can be used to
execute commands via systemd. You can use
[xdg-dbus-proxy](https://github.com/flatpak/xdg-dbus-proxy) to filter
D-Bus communication.
- Some applications deploy their own sandboxing mechanisms, and these can be
restricted by the constraints imposed by bubblewrap's sandboxing.
For example, some web browsers which configure their child proccesses via
seccomp to not have access to the filesystem. If you limit the syscalls and
don't allow the seccomp syscall, a browser cannot apply these restrictions.
Similarly, if these rules were compiled into a file that is not available in
the sandbox, the browser cannot load these rules from this file and cannot
apply these restrictions.
Related project comparison: Firejail
------------------------------------
[Firejail](https://github.com/netblue30/firejail/tree/HEAD/src/firejail)
is similar to Flatpak before bubblewrap was split out in that it combines
a setuid tool with a lot of desktop-specific sandboxing features. For
example, Firejail knows about Pulseaudio, whereas bubblewrap does not.
The bubblewrap authors believe it's much easier to audit a small
setuid program, and keep features such as Pulseaudio filtering as an
unprivileged process, as now occurs in Flatpak.
Also, @cgwalters thinks trying to
[whitelist file paths](https://github.com/netblue30/firejail/blob/37a5a3545ef6d8d03dad8bbd888f53e13274c9e5/src/firejail/fs_whitelist.c#L176)
is a bad idea given the myriad ways users have to manipulate paths,
and the myriad ways in which system administrators may configure a
system. The bubblewrap approach is to only retain a few specific
Linux capabilities such as `CAP_SYS_ADMIN`, but to always access the
filesystem as the invoking uid. This entirely closes
[TOCTTOU attacks](https://cwe.mitre.org/data/definitions/367.html) and
such.
Related project comparison: Sandstorm.io
----------------------------------------
[Sandstorm.io](https://sandstorm.io/) requires unprivileged user
namespaces to set up its sandbox, though it could easily be adapted
to operate in a setuid mode as well. @cgwalters believes their code is
fairly good, but it could still make sense to unify on bubblewrap.
However, @kentonv (of Sandstorm) feels that while this makes sense
in principle, the switching cost outweighs the practical benefits for
now. This decision could be re-evaluated in the future, but it is not
being actively pursued today.
Related project comparison: runc/binctr
----------------------------------------
[runC](https://github.com/opencontainers/runc) is currently working on
supporting [rootless containers](https://github.com/opencontainers/runc/pull/774),
without needing `setuid` or any other privileges during installation of
runC (using unprivileged user namespaces rather than `setuid`),
creation, and management of containers. However, the standard mode of
using runC is similar to [systemd nspawn](https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html)
in that it is tooling intended to be invoked by root.
The bubblewrap authors believe that runc and systemd-nspawn are not
designed to be made setuid, and are distant from supporting such a mode.
However with rootless containers, runC will be able to fulfill certain usecases
that bubblewrap supports (with the added benefit of being a standardised and
complete OCI runtime).
[binctr](https://github.com/jfrazelle/binctr) is just a wrapper for
runC, so inherits all of its design tradeoffs.
What's with the name?!
----------------------
The name bubblewrap was chosen to convey that this
tool runs as the parent of the application (so wraps it in some sense) and creates
a protective layer (the sandbox) around it.
![](bubblewrap.jpg)
(Bubblewrap cat by [dancing_stupidity](https://www.flickr.com/photos/27549668@N03/))

42
codex-rs/vendor/bubblewrap/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,42 @@
## Security and Disclosure Information Policy for the bubblewrap Project
The bubblewrap Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/HEAD/SECURITY.md) for the Containers Projects.
### System security
If bubblewrap is setuid root, then the goal is that it does not allow
a malicious local user to do anything that would not have been possible
on a kernel that allows unprivileged users to create new user namespaces.
For example, [CVE-2020-5291](https://github.com/containers/bubblewrap/security/advisories/GHSA-j2qp-rvxj-43vj)
was treated as a security vulnerability in bubblewrap.
If bubblewrap is not setuid root, then it is not a security boundary
between the user and the OS, because anything bubblewrap could do, a
malicious user could equally well do by writing their own tool equivalent
to bubblewrap.
### Sandbox security
bubblewrap is a toolkit for constructing sandbox environments.
bubblewrap is not a complete, ready-made sandbox with a specific security
policy.
Some of bubblewrap's use-cases want a security boundary between the sandbox
and the real system; other use-cases want the ability to change the layout of
the filesystem for processes inside the sandbox, but do not aim to be a
security boundary.
As a result, the level of protection between the sandboxed processes and
the host system is entirely determined by the arguments passed to
bubblewrap.
Whatever program constructs the command-line arguments for bubblewrap
(often a larger framework like Flatpak, libgnome-desktop, sandwine
or an ad-hoc script) is responsible for defining its own security model,
and choosing appropriate bubblewrap command-line arguments to implement
that security model.
For example,
[CVE-2017-5226](https://github.com/flatpak/flatpak/security/advisories/GHSA-7gfv-rvfx-h87x)
(in which a Flatpak app could send input to a parent terminal using the
`TIOCSTI` ioctl) is considered to be a Flatpak vulnerability, not a
bubblewrap vulnerability.

598
codex-rs/vendor/bubblewrap/bind-mount.c vendored Normal file
View File

@@ -0,0 +1,598 @@
/* bubblewrap
* Copyright (C) 2016 Alexander Larsson
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <sys/mount.h>
#include "utils.h"
#include "bind-mount.h"
static char *
skip_token (char *line, bool eat_whitespace)
{
while (*line != ' ' && *line != '\n')
line++;
if (eat_whitespace && *line == ' ')
line++;
return line;
}
static char *
unescape_inline (char *escaped)
{
char *unescaped, *res;
const char *end;
res = escaped;
end = escaped + strlen (escaped);
unescaped = escaped;
while (escaped < end)
{
if (*escaped == '\\')
{
*unescaped++ =
((escaped[1] - '0') << 6) |
((escaped[2] - '0') << 3) |
((escaped[3] - '0') << 0);
escaped += 4;
}
else
{
*unescaped++ = *escaped++;
}
}
*unescaped = 0;
return res;
}
static bool
match_token (const char *token, const char *token_end, const char *str)
{
while (token != token_end && *token == *str)
{
token++;
str++;
}
if (token == token_end)
return *str == 0;
return false;
}
static unsigned long
decode_mountoptions (const char *options)
{
const char *token, *end_token;
int i;
unsigned long flags = 0;
static const struct { int flag;
const char *name;
} flags_data[] = {
{ 0, "rw" },
{ MS_RDONLY, "ro" },
{ MS_NOSUID, "nosuid" },
{ MS_NODEV, "nodev" },
{ MS_NOEXEC, "noexec" },
{ MS_NOATIME, "noatime" },
{ MS_NODIRATIME, "nodiratime" },
{ MS_RELATIME, "relatime" },
{ 0, NULL }
};
token = options;
do
{
end_token = strchr (token, ',');
if (end_token == NULL)
end_token = token + strlen (token);
for (i = 0; flags_data[i].name != NULL; i++)
{
if (match_token (token, end_token, flags_data[i].name))
{
flags |= flags_data[i].flag;
break;
}
}
if (*end_token != 0)
token = end_token + 1;
else
token = NULL;
}
while (token != NULL);
return flags;
}
typedef struct MountInfo MountInfo;
struct MountInfo {
char *mountpoint;
unsigned long options;
};
typedef MountInfo *MountTab;
static void
mount_tab_free (MountTab tab)
{
int i;
for (i = 0; tab[i].mountpoint != NULL; i++)
free (tab[i].mountpoint);
free (tab);
}
static inline void
cleanup_mount_tabp (void *p)
{
void **pp = (void **) p;
if (*pp)
mount_tab_free ((MountTab)*pp);
}
#define cleanup_mount_tab __attribute__((cleanup (cleanup_mount_tabp)))
typedef struct MountInfoLine MountInfoLine;
struct MountInfoLine {
const char *mountpoint;
const char *options;
bool covered;
int id;
int parent_id;
MountInfoLine *first_child;
MountInfoLine *next_sibling;
};
static unsigned int
count_lines (const char *data)
{
unsigned int count = 0;
const char *p = data;
while (*p != 0)
{
if (*p == '\n')
count++;
p++;
}
/* If missing final newline, add one */
if (p > data && *(p-1) != '\n')
count++;
return count;
}
static int
count_mounts (MountInfoLine *line)
{
MountInfoLine *child;
int res = 0;
if (!line->covered)
res += 1;
child = line->first_child;
while (child != NULL)
{
res += count_mounts (child);
child = child->next_sibling;
}
return res;
}
static MountInfo *
collect_mounts (MountInfo *info, MountInfoLine *line)
{
MountInfoLine *child;
if (!line->covered)
{
info->mountpoint = xstrdup (line->mountpoint);
info->options = decode_mountoptions (line->options);
info ++;
}
child = line->first_child;
while (child != NULL)
{
info = collect_mounts (info, child);
child = child->next_sibling;
}
return info;
}
static MountTab
parse_mountinfo (int proc_fd,
const char *root_mount)
{
cleanup_free char *mountinfo = NULL;
cleanup_free MountInfoLine *lines = NULL;
cleanup_free MountInfoLine **by_id = NULL;
cleanup_mount_tab MountTab mount_tab = NULL;
MountInfo *end_tab;
int n_mounts;
char *line;
unsigned int i;
int max_id;
unsigned int n_lines;
int root;
mountinfo = load_file_at (proc_fd, "self/mountinfo");
if (mountinfo == NULL)
die_with_error ("Can't open /proc/self/mountinfo");
n_lines = count_lines (mountinfo);
lines = xcalloc (n_lines, sizeof (MountInfoLine));
max_id = 0;
line = mountinfo;
i = 0;
root = -1;
while (*line != 0)
{
int rc, consumed = 0;
unsigned int maj, min;
char *end;
char *rest;
char *mountpoint;
char *mountpoint_end;
char *options;
char *options_end;
char *next_line;
assert (i < n_lines);
end = strchr (line, '\n');
if (end != NULL)
{
*end = 0;
next_line = end + 1;
}
else
next_line = line + strlen (line);
rc = sscanf (line, "%d %d %u:%u %n", &lines[i].id, &lines[i].parent_id, &maj, &min, &consumed);
if (rc != 4)
die ("Can't parse mountinfo line");
rest = line + consumed;
rest = skip_token (rest, true); /* mountroot */
mountpoint = rest;
rest = skip_token (rest, false); /* mountpoint */
mountpoint_end = rest++;
options = rest;
rest = skip_token (rest, false); /* vfs options */
options_end = rest;
*mountpoint_end = 0;
lines[i].mountpoint = unescape_inline (mountpoint);
*options_end = 0;
lines[i].options = options;
if (lines[i].id > max_id)
max_id = lines[i].id;
if (lines[i].parent_id > max_id)
max_id = lines[i].parent_id;
if (path_equal (lines[i].mountpoint, root_mount))
root = i;
i++;
line = next_line;
}
assert (i == n_lines);
if (root == -1)
{
mount_tab = xcalloc (1, sizeof (MountInfo));
return steal_pointer (&mount_tab);
}
by_id = xcalloc (max_id + 1, sizeof (MountInfoLine*));
for (i = 0; i < n_lines; i++)
by_id[lines[i].id] = &lines[i];
for (i = 0; i < n_lines; i++)
{
MountInfoLine *this = &lines[i];
MountInfoLine *parent = by_id[this->parent_id];
MountInfoLine **to_sibling;
MountInfoLine *sibling;
bool covered = false;
if (!has_path_prefix (this->mountpoint, root_mount))
continue;
if (parent == NULL)
continue;
if (strcmp (parent->mountpoint, this->mountpoint) == 0)
parent->covered = true;
to_sibling = &parent->first_child;
sibling = parent->first_child;
while (sibling != NULL)
{
/* If this mountpoint is a path prefix of the sibling,
* say this->mp=/foo/bar and sibling->mp=/foo, then it is
* covered by the sibling, and we drop it. */
if (has_path_prefix (this->mountpoint, sibling->mountpoint))
{
covered = true;
break;
}
/* If the sibling is a path prefix of this mount point,
* say this->mp=/foo and sibling->mp=/foo/bar, then the sibling
* is covered, and we drop it.
*/
if (has_path_prefix (sibling->mountpoint, this->mountpoint))
*to_sibling = sibling->next_sibling;
else
to_sibling = &sibling->next_sibling;
sibling = sibling->next_sibling;
}
if (covered)
continue;
*to_sibling = this;
}
n_mounts = count_mounts (&lines[root]);
mount_tab = xcalloc (n_mounts + 1, sizeof (MountInfo));
end_tab = collect_mounts (&mount_tab[0], &lines[root]);
assert (end_tab == &mount_tab[n_mounts]);
return steal_pointer (&mount_tab);
}
bind_mount_result
bind_mount (int proc_fd,
const char *src,
const char *dest,
bind_option_t options,
char **failing_path)
{
bool readonly = (options & BIND_READONLY) != 0;
bool devices = (options & BIND_DEVICES) != 0;
bool recursive = (options & BIND_RECURSIVE) != 0;
unsigned long current_flags, new_flags;
cleanup_mount_tab MountTab mount_tab = NULL;
cleanup_free char *resolved_dest = NULL;
cleanup_free char *dest_proc = NULL;
cleanup_free char *oldroot_dest_proc = NULL;
cleanup_free char *kernel_case_combination = NULL;
cleanup_fd int dest_fd = -1;
int i;
if (src)
{
if (mount (src, dest, NULL, MS_SILENT | MS_BIND | (recursive ? MS_REC : 0), NULL) != 0)
return BIND_MOUNT_ERROR_MOUNT;
}
/* The mount operation will resolve any symlinks in the destination
path, so to find it in the mount table we need to do that too. */
resolved_dest = realpath (dest, NULL);
if (resolved_dest == NULL)
return BIND_MOUNT_ERROR_REALPATH_DEST;
dest_fd = TEMP_FAILURE_RETRY (open (resolved_dest, O_PATH | O_CLOEXEC));
if (dest_fd < 0)
{
if (failing_path != NULL)
*failing_path = steal_pointer (&resolved_dest);
return BIND_MOUNT_ERROR_REOPEN_DEST;
}
/* If we are in a case-insensitive filesystem, mountinfo might contain a
* different case combination of the path we requested to mount.
* This is due to the fact that the kernel, as of the beginning of 2021,
* populates mountinfo with whatever case combination first appeared in the
* dcache; kernel developers plan to change this in future so that it
* reflects the on-disk encoding instead.
* To avoid throwing an error when this happens, we use readlink() result
* instead of the provided @root_mount, so that we can compare the mountinfo
* entries with the same case combination that the kernel is expected to
* use. */
dest_proc = xasprintf ("/proc/self/fd/%d", dest_fd);
oldroot_dest_proc = get_oldroot_path (dest_proc);
kernel_case_combination = readlink_malloc (oldroot_dest_proc);
if (kernel_case_combination == NULL)
{
if (failing_path != NULL)
*failing_path = steal_pointer (&resolved_dest);
return BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD;
}
mount_tab = parse_mountinfo (proc_fd, kernel_case_combination);
if (mount_tab[0].mountpoint == NULL)
{
if (failing_path != NULL)
*failing_path = steal_pointer (&kernel_case_combination);
errno = EINVAL;
return BIND_MOUNT_ERROR_FIND_DEST_MOUNT;
}
assert (path_equal (mount_tab[0].mountpoint, kernel_case_combination));
current_flags = mount_tab[0].options;
new_flags = current_flags | (devices ? 0 : MS_NODEV) | MS_NOSUID | (readonly ? MS_RDONLY : 0);
if (new_flags != current_flags &&
mount ("none", resolved_dest,
NULL, MS_SILENT | MS_BIND | MS_REMOUNT | new_flags, NULL) != 0)
{
if (failing_path != NULL)
*failing_path = steal_pointer (&resolved_dest);
return BIND_MOUNT_ERROR_REMOUNT_DEST;
}
/* We need to work around the fact that a bind mount does not apply the flags, so we need to manually
* apply the flags to all submounts in the recursive case.
* Note: This does not apply the flags to mounts which are later propagated into this namespace.
*/
if (recursive)
{
for (i = 1; mount_tab[i].mountpoint != NULL; i++)
{
current_flags = mount_tab[i].options;
new_flags = current_flags | (devices ? 0 : MS_NODEV) | MS_NOSUID | (readonly ? MS_RDONLY : 0);
if (new_flags != current_flags &&
mount ("none", mount_tab[i].mountpoint,
NULL, MS_SILENT | MS_BIND | MS_REMOUNT | new_flags, NULL) != 0)
{
/* If we can't read the mountpoint we can't remount it, but that should
be safe to ignore because its not something the user can access. */
if (errno != EACCES)
{
if (failing_path != NULL)
*failing_path = xstrdup (mount_tab[i].mountpoint);
return BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT;
}
}
}
}
return BIND_MOUNT_SUCCESS;
}
/**
* Return a string representing bind_mount_result, like strerror().
* If want_errno_p is non-NULL, *want_errno_p is used to indicate whether
* it would make sense to print strerror(saved_errno).
*/
static char *
bind_mount_result_to_string (bind_mount_result res,
const char *failing_path,
bool *want_errno_p)
{
char *string = NULL;
bool want_errno = true;
switch (res)
{
case BIND_MOUNT_ERROR_MOUNT:
string = xstrdup ("Unable to mount source on destination");
break;
case BIND_MOUNT_ERROR_REALPATH_DEST:
string = xstrdup ("realpath(destination)");
break;
case BIND_MOUNT_ERROR_REOPEN_DEST:
string = xasprintf ("open(\"%s\", O_PATH)", failing_path);
break;
case BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD:
string = xasprintf ("readlink(/proc/self/fd/N) for \"%s\"", failing_path);
break;
case BIND_MOUNT_ERROR_FIND_DEST_MOUNT:
string = xasprintf ("Unable to find \"%s\" in mount table", failing_path);
want_errno = false;
break;
case BIND_MOUNT_ERROR_REMOUNT_DEST:
string = xasprintf ("Unable to remount destination \"%s\" with correct flags",
failing_path);
break;
case BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT:
string = xasprintf ("Unable to apply mount flags: remount \"%s\"",
failing_path);
break;
case BIND_MOUNT_SUCCESS:
string = xstrdup ("Success");
break;
default:
string = xstrdup ("(unknown/invalid bind_mount_result)");
break;
}
if (want_errno_p != NULL)
*want_errno_p = want_errno;
return string;
}
void
die_with_bind_result (bind_mount_result res,
int saved_errno,
const char *failing_path,
const char *format,
...)
{
va_list args;
bool want_errno = true;
char *message;
if (bwrap_level_prefix)
fprintf (stderr, "<%d>", LOG_ERR);
fprintf (stderr, "bwrap: ");
va_start (args, format);
vfprintf (stderr, format, args);
va_end (args);
message = bind_mount_result_to_string (res, failing_path, &want_errno);
fprintf (stderr, ": %s", message);
/* message is leaked, but we're exiting unsuccessfully anyway, so ignore */
if (want_errno)
{
switch (res)
{
case BIND_MOUNT_ERROR_MOUNT:
case BIND_MOUNT_ERROR_REMOUNT_DEST:
case BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT:
fprintf (stderr, ": %s", mount_strerror (saved_errno));
break;
case BIND_MOUNT_ERROR_REALPATH_DEST:
case BIND_MOUNT_ERROR_REOPEN_DEST:
case BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD:
case BIND_MOUNT_ERROR_FIND_DEST_MOUNT:
case BIND_MOUNT_SUCCESS:
default:
fprintf (stderr, ": %s", strerror (saved_errno));
}
}
fprintf (stderr, "\n");
exit (1);
}

54
codex-rs/vendor/bubblewrap/bind-mount.h vendored Normal file
View File

@@ -0,0 +1,54 @@
/* bubblewrap
* Copyright (C) 2016 Alexander Larsson
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
#pragma once
#include "utils.h"
typedef enum {
BIND_READONLY = (1 << 0),
BIND_DEVICES = (1 << 2),
BIND_RECURSIVE = (1 << 3),
} bind_option_t;
typedef enum
{
BIND_MOUNT_SUCCESS = 0,
BIND_MOUNT_ERROR_MOUNT,
BIND_MOUNT_ERROR_REALPATH_DEST,
BIND_MOUNT_ERROR_REOPEN_DEST,
BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD,
BIND_MOUNT_ERROR_FIND_DEST_MOUNT,
BIND_MOUNT_ERROR_REMOUNT_DEST,
BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT,
} bind_mount_result;
bind_mount_result bind_mount (int proc_fd,
const char *src,
const char *dest,
bind_option_t options,
char **failing_path);
void die_with_bind_result (bind_mount_result res,
int saved_errno,
const char *failing_path,
const char *format,
...)
__attribute__((__noreturn__))
__attribute__((format (printf, 4, 5)));

3641
codex-rs/vendor/bubblewrap/bubblewrap.c vendored Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

648
codex-rs/vendor/bubblewrap/bwrap.xml vendored Normal file
View File

@@ -0,0 +1,648 @@
<?xml version="1.0"?>
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
"http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [
]>
<refentry id="bwrap">
<refentryinfo>
<title>bwrap</title>
<productname>Containers</productname>
<authorgroup>
<author>
<contrib>Developer</contrib>
<firstname>Alexander</firstname>
<surname>Larsson</surname>
</author>
<author>
<contrib>Developer</contrib>
<firstname>Colin</firstname>
<surname>Walters</surname>
</author>
</authorgroup>
</refentryinfo>
<refmeta>
<refentrytitle>bwrap</refentrytitle>
<manvolnum>1</manvolnum>
<refmiscinfo class="manual">User Commands</refmiscinfo>
</refmeta>
<refnamediv>
<refname>bwrap</refname>
<refpurpose>container setup utility</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>bwrap</command>
<arg choice="opt" rep="repeat"><replaceable>OPTION</replaceable></arg>
<arg choice="opt"><replaceable>COMMAND</replaceable></arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1><title>Description</title>
<para>
<command>bwrap</command> is a unprivileged low-level sandboxing tool
(optionally setuid on older distributions). You
are unlikely to use it directly from the commandline, although that is possible.
</para>
<para>
It works by creating a new, completely empty, filesystem namespace where the root
is on a tmpfs that is invisible from the host, and which will be automatically
cleaned up when the last process exits. You can then use commandline options to
construct the root filesystem and process environment for the command to run in
the namespace.
</para>
<para>
By default, <command>bwrap</command> creates a new mount namespace for the sandbox.
Optionally it also sets up new user, ipc, pid, network and uts namespaces (but note the
user namespace is required if bwrap is not installed setuid root).
The application in the sandbox can be made to run with a different UID and GID.
</para>
<para>
If needed (e.g. when using a PID namespace) <command>bwrap</command>
is running a minimal pid 1 process in the sandbox that is
responsible for reaping zombies. It also detects when the initial
application process (pid 2) dies and reports its exit status back to
the original spawner. The pid 1 process exits to clean up the
sandbox when there are no other processes in the sandbox left.
</para>
</refsect1>
<refsect1><title>Options</title>
<para>
When options are used multiple times, the last option wins, unless otherwise
specified.
</para>
<para>General options:</para>
<variablelist>
<varlistentry>
<term><option>--help</option></term>
<listitem><para>Print help and exit</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--version</option></term>
<listitem><para>Print version</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--args <arg choice="plain">FD</arg></option></term>
<listitem><para>
Parse nul-separated arguments from the given file descriptor.
This option can be used multiple times to parse options from
multiple sources.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--argv0 <arg choice="plain">VALUE</arg></option></term>
<listitem><para>Set argv[0] to the value <arg choice="plain">VALUE</arg> before running the program</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--level-prefix</option></term>
<listitem>
<para>
Prefix each line of diagnostic output with a numeric severity
level enclosed in angle brackets.
The severity levels used are based on the constants used by
<citerefentry><refentrytitle>syslog</refentrytitle><manvolnum>3</manvolnum></citerefentry>:
for example, <literal>&lt;4&gt;</literal> indicates a warning,
because <literal>LOG_WARNING</literal> has numeric value 4.
Numbers smaller than 4 indicate fatal errors, and numbers larger
than 4 indicate informational messages.
These prefixes can be parsed by tools compatible with
<literal>logger --prio-prefix</literal> (see
<citerefentry><refentrytitle>logger</refentrytitle><manvolnum>1</manvolnum></citerefentry>)
or <literal>systemd-cat --level-prefix=1</literal> (see
<citerefentry><refentrytitle>systemd-cat</refentrytitle><manvolnum>1</manvolnum></citerefentry>).
</para>
</listitem>
</varlistentry>
</variablelist>
<para>Options related to kernel namespaces:</para>
<variablelist>
<varlistentry>
<term><option>--unshare-user</option></term>
<listitem><para>Create a new user namespace</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--unshare-user-try</option></term>
<listitem><para>Create a new user namespace if possible else skip it</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--unshare-ipc</option></term>
<listitem><para>Create a new ipc namespace</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--unshare-pid</option></term>
<listitem><para>Create a new pid namespace</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--unshare-net</option></term>
<listitem><para>Create a new network namespace</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--unshare-uts</option></term>
<listitem><para>Create a new uts namespace</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--unshare-cgroup</option></term>
<listitem><para>Create a new cgroup namespace</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--unshare-cgroup-try</option></term>
<listitem><para>Create a new cgroup namespace if possible else skip it</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--unshare-all</option></term>
<listitem><para>Unshare all possible namespaces. Currently equivalent with: <option>--unshare-user-try</option> <option>--unshare-ipc</option> <option>--unshare-pid</option> <option>--unshare-net</option> <option>--unshare-uts</option> <option>--unshare-cgroup-try</option></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--share-net</option></term>
<listitem><para>Retain the network namespace, overriding an earlier <option>--unshare-all</option> or <option>--unshare-net</option></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--userns <arg choice="plain">FD</arg></option></term>
<listitem><para>Use an existing user namespace instead of creating a new one. The namespace must fulfil the permission requirements for setns(), which generally means that it must be a descendant of the currently active user namespace, owned by the same user. </para>
<para>This is incompatible with --unshare-user, and doesn't work in the setuid version of bubblewrap.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--userns2 <arg choice="plain">FD</arg></option></term>
<listitem><para>After setting up the new namespace, switch into the specified namespace. For this to work the specified namespace must be a descendant of the user namespace used for the setup, so this is only useful in combination with --userns.</para>
<para>This is useful because sometimes bubblewrap itself creates nested user namespaces (to work around some kernel issues) and --userns2 can be used to enter these.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--disable-userns</option></term>
<listitem><para>
Prevent the process in the sandbox from creating further user namespaces,
so that it cannot rearrange the filesystem namespace or do other more
complex namespace modification.
This is currently implemented by setting the
<literal>user.max_user_namespaces</literal> sysctl to 1, and then
entering a nested user namespace which is unable to raise that limit
in the outer namespace.
This option requires <option>--unshare-user</option>, and doesn't work
in the setuid version of bubblewrap.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--assert-userns-disabled</option></term>
<listitem><para>
Confirm that the process in the sandbox has been prevented from
creating further user namespaces, but without taking any particular
action to prevent that. For example, this can be combined with
<option>--userns</option> to check that the given user namespace
has already been set up to prevent the creation of further user
namespaces.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--pidns <arg choice="plain">FD</arg></option></term>
<listitem><para>Use an existing pid namespace instead of creating one. This is often used with --userns, because the pid namespace must be owned by the same user namespace that bwrap uses. </para>
<para>Note that this can be combined with --unshare-pid, and in that case it means that the sandbox will be in its own pid namespace, which is a child of the passed in one.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--uid <arg choice="plain">UID</arg></option></term>
<listitem><para>Use a custom user id in the sandbox (requires <option>--unshare-user</option>)</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--gid <arg choice="plain">GID</arg></option></term>
<listitem><para>Use a custom group id in the sandbox (requires <option>--unshare-user</option>)</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--hostname <arg choice="plain">HOSTNAME</arg></option></term>
<listitem><para>Use a custom hostname in the sandbox (requires <option>--unshare-uts</option>)</para></listitem>
</varlistentry>
</variablelist>
<para>Options about environment setup:</para>
<variablelist>
<varlistentry>
<term><option>--chdir <arg choice="plain">DIR</arg></option></term>
<listitem><para>Change directory to <arg choice="plain">DIR</arg></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--setenv <arg choice="plain">VAR</arg> <arg choice="plain">VALUE</arg></option></term>
<listitem><para>Set an environment variable</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--unsetenv <arg choice="plain">VAR</arg></option></term>
<listitem><para>Unset an environment variable</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--clearenv</option></term>
<listitem><para>Unset all environment variables, except for
<envar>PWD</envar> and any that are subsequently set by
<option>--setenv</option></para></listitem>
</varlistentry>
</variablelist>
<para>Options for monitoring the sandbox from the outside:</para>
<variablelist>
<varlistentry>
<term><option>--lock-file <arg choice="plain">DEST</arg></option></term>
<listitem><para>
Take a lock on <arg choice="plain">DEST</arg> while the sandbox is running.
This option can be used multiple times to take locks on multiple files.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--sync-fd <arg choice="plain">FD</arg></option></term>
<listitem><para>Keep this file descriptor open while the sandbox is running</para></listitem>
</varlistentry>
</variablelist>
<para>
Filesystem related options. These are all operations that modify the filesystem directly, or
mounts stuff in the filesystem. These are applied in the order they are given as arguments.
</para>
<para>
Any missing parent directories that are required to create a specified destination are
automatically created as needed. Their permissions are normally set to 0755
(rwxr-xr-x). However, if a <option>--perms</option> option is in effect, and
it sets the permissions for group or other to zero, then newly-created
parent directories will also have their corresponding permission set to zero.
<option>--size</option> modifies the size of the created mount when preceding a
<option>--tmpfs</option> action; <option>--perms</option> and <option>--size</option>
can be combined.
</para>
<variablelist>
<varlistentry>
<term><option>--perms <arg choice="plain">OCTAL</arg></option></term>
<listitem><para>This option does nothing on its own, and must be followed
by one of the options that it affects. It sets the permissions
for the next operation to <arg choice="plain">OCTAL</arg>.
Subsequent operations are not affected: for example,
<literal>--perms 0700 --tmpfs /a --tmpfs /b</literal> will mount
<filename>/a</filename> with permissions 0700, then return to
the default permissions for <filename>/b</filename>.
Note that <option>--perms</option> and <option>--size</option> can be
combined: <literal>--perms 0700 --size 10485760 --tmpfs /s</literal> will apply
permissions as well as a maximum size to the created tmpfs.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--size <arg choice="plain">BYTES</arg></option></term>
<listitem><para>This option does nothing on its own, and must be followed
by <literal>--tmpfs</literal>. It sets the size in bytes for the next tmpfs.
For example, <literal>--size 10485760 --tmpfs /tmp</literal> will create a tmpfs
at <filename>/tmp</filename> of size 10MiB. Subsequent operations are not
affected: for example,
<literal>--size 10485760 --tmpfs /a --tmpfs /b</literal> will mount
<filename>/a</filename> with size 10MiB, then return to the default size for
<filename>/b</filename>.
Note that <option>--perms</option> and <option>--size</option> can be
combined: <literal>--size 10485760 --perms 0700 --tmpfs /s</literal> will apply
permissions as well as a maximum size to the created tmpfs.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--bind <arg choice="plain">SRC</arg> <arg choice="plain">DEST</arg></option></term>
<listitem><para>Bind mount the host path <arg choice="plain">SRC</arg> on <arg choice="plain">DEST</arg></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--bind-try <arg choice="plain">SRC</arg> <arg choice="plain">DEST</arg></option></term>
<listitem><para>Equal to <option>--bind</option> but ignores non-existent <arg choice="plain">SRC</arg></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--dev-bind <arg choice="plain">SRC</arg> <arg choice="plain">DEST</arg></option></term>
<listitem><para>Bind mount the host path <arg choice="plain">SRC</arg> on <arg choice="plain">DEST</arg>, allowing device access</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--dev-bind-try <arg choice="plain">SRC</arg> <arg choice="plain">DEST</arg></option></term>
<listitem><para>Equal to <option>--dev-bind</option> but ignores non-existent <arg choice="plain">SRC</arg></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--ro-bind <arg choice="plain">SRC</arg> <arg choice="plain">DEST</arg></option></term>
<listitem><para>Bind mount the host path <arg choice="plain">SRC</arg> readonly on <arg choice="plain">DEST</arg></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--ro-bind-try <arg choice="plain">SRC</arg> <arg choice="plain">DEST</arg></option></term>
<listitem><para>Equal to <option>--ro-bind</option> but ignores non-existent <arg choice="plain">SRC</arg></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--remount-ro <arg choice="plain">DEST</arg></option></term>
<listitem><para>Remount the path <arg choice="plain">DEST</arg> as readonly. It works only on the specified mount point, without changing any other mount point under the specified path</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--overlay-src <arg choice="plain">SRC</arg></option></term>
<listitem>
<para>
This option does nothing on its own, and must be followed by one of
the other <literal>overlay</literal> options. It specifies a host
path from which files should be read if they aren't present in a
higher layer.
</para>
<para>
This option can be used multiple times to provide multiple sources.
The sources are overlaid in the order given, with the first source on
the command line at the bottom of the stack: if a given path to be
read exists in more than one source, the file is read from the last
such source specified.
</para>
<para>
(For readers familiar with overlayfs, note that this is the
reverse of the order used by the kernel's <literal>lowerdir</literal>
mount option.)
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--overlay <arg choice="plain">RWSRC</arg> <arg choice="plain">WORKDIR</arg> <arg choice="plain">DEST</arg></option></term>
</varlistentry>
<varlistentry>
<term><option>--tmp-overlay <arg choice="plain">DEST</arg></option></term>
</varlistentry>
<varlistentry>
<term><option>--ro-overlay <arg choice="plain">DEST</arg></option></term>
<listitem>
<para>
Use overlayfs to mount the host paths specified by
<arg choice="plain">RWSRC</arg> and all immediately preceding
<option>--overlay-src</option> on <arg choice="plain">DEST</arg>.
<arg choice="plain">DEST</arg> will contain the union of all the files
in all the layers.
</para>
<para>
With <arg choice="plain">--overlay</arg> all writes will go to
<arg choice="plain">RWSRC</arg>. Reads will come preferentially from
<arg choice="plain">RWSRC</arg>, and then from any
<option>--overlay-src</option> paths.
<arg choice="plain">WORKDIR</arg> must be an empty directory on the
same filesystem as <arg choice="plain">RWSRC</arg>, and is used
internally by the kernel.
</para>
<para>
With <arg choice="plain">--tmp-overlay</arg> all writes will go to
the tmpfs that hosts the sandbox root, in a location not accessible
from either the host or the child process. Writes will therefore not
be persisted across multiple runs.
</para>
<para>
With <arg choice="plain">--ro-overlay</arg> the filesystem will be
mounted read-only. This option requires at least two
<option>--overlay-src</option> to precede it.
</para>
<para>
None of these options are available in the setuid version of
bubblewrap. Using <arg choice="plain">--ro-overlay</arg> or providing
more than one <option>--overlay-src</option> requires a Linux kernel
version of 4.0 or later.
</para>
<para>
Due to limitations of overlayfs, no host directory given via
<arg choice="plain">--overlay-src</arg> or
<arg choice="plain">--overlay</arg> may be an ancestor of another,
after resolving symlinks. Depending on version, the Linux kernel may
or may not enforce this, but if not then overlayfs's behavior is
undefined.
</para>
<para>
For more information see the Overlay Filesystem documentation in the
Linux kernel at
https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--proc <arg choice="plain">DEST</arg></option></term>
<listitem><para>Mount procfs on <arg choice="plain">DEST</arg></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--dev <arg choice="plain">DEST</arg></option></term>
<listitem><para>Mount new devtmpfs on <arg choice="plain">DEST</arg></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--tmpfs <arg choice="plain">DEST</arg></option></term>
<listitem>
<para>Mount new tmpfs on <arg choice="plain">DEST</arg>.
If the previous option was <option>--perms</option>, it sets the
mode of the tmpfs. Otherwise, the tmpfs has mode 0755.
If the previous option was <option>--size</option>, it sets the
size in bytes of the tmpfs. Otherwise, the tmpfs has the default size.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--mqueue <arg choice="plain">DEST</arg></option></term>
<listitem><para>Mount new mqueue on <arg choice="plain">DEST</arg></para></listitem>
</varlistentry>
<varlistentry>
<term><option>--dir <arg choice="plain">DEST</arg></option></term>
<listitem>
<para>Create a directory at <arg choice="plain">DEST</arg>.
If the directory already exists, its permissions are unmodified,
ignoring <option>--perms</option> (use <option>--chmod</option>
if the permissions of an existing directory need to be changed).
If the directory is newly created and the previous option was
<option>--perms</option>, it sets the mode of the directory.
Otherwise, newly-created directories have mode 0755.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--file <arg choice="plain">FD</arg> <arg choice="plain">DEST</arg></option></term>
<listitem>
<para>Copy from the file descriptor <arg choice="plain">FD</arg> to
<arg choice="plain">DEST</arg>.
If the previous option was <option>--perms</option>, it sets the
mode of the new file. Otherwise, the file has mode 0666
(note that this is not the same as <option>--bind-data</option>).</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--bind-data <arg choice="plain">FD</arg> <arg choice="plain">DEST</arg></option></term>
<listitem>
<para>Copy from the file descriptor <arg choice="plain">FD</arg> to
a file which is bind-mounted on <arg choice="plain">DEST</arg>.
If the previous option was <option>--perms</option>, it sets the
mode of the new file. Otherwise, the file has mode 0600
(note that this is not the same as <option>--file</option>).</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--ro-bind-data <arg choice="plain">FD</arg> <arg choice="plain">DEST</arg></option></term>
<listitem>
<para>Copy from the file descriptor <arg choice="plain">FD</arg> to
a file which is bind-mounted read-only on
<arg choice="plain">DEST</arg>.
If the previous option was <option>--perms</option>, it sets the
mode of the new file. Otherwise, the file has mode 0600
(note that this is not the same as <option>--file</option>).</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--symlink <arg choice="plain">SRC</arg> <arg choice="plain">DEST</arg></option></term>
<listitem>
<para>Create a symlink at <arg choice="plain">DEST</arg> with target
<arg choice="plain">SRC</arg>.</para>
<para>Since version 0.9.0, it is not considered to be an error if
<arg choice="plain">DEST</arg> already exists as a symbolic link and its
target is exactly <arg choice="plain">SRC</arg>.</para>
<para>Before version 0.9.0, if <arg choice="plain">DEST</arg> already
existed, this would be treated as an error (even if its target
was identical to <arg choice="plain">SRC</arg>).</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--chmod <arg choice="plain">OCTAL</arg> <arg choice="plain">PATH</arg></option></term>
<listitem>
<para>
Set the permissions of <arg choice="plain">PATH</arg>, which
must already exist, to <arg choice="plain">OCTAL</arg>.
</para>
</listitem>
</varlistentry>
</variablelist>
<para>Lockdown options:</para>
<variablelist>
<varlistentry>
<term><option>--seccomp <arg choice="plain">FD</arg></option></term>
<listitem><para>
Load and use seccomp rules from <arg choice="plain">FD</arg>.
The rules need to be in the form of a compiled cBPF program,
as generated by seccomp_export_bpf.
If this option is given more than once, only the last one is used.
Use <option>--add-seccomp-fd</option> if multiple seccomp programs
are needed.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--add-seccomp-fd <arg choice="plain">FD</arg></option></term>
<listitem><para>
Load and use seccomp rules from <arg choice="plain">FD</arg>.
The rules need to be in the form of a compiled cBPF program,
as generated by seccomp_export_bpf.
This option can be repeated, in which case all the seccomp
programs will be loaded in the order given (note that the kernel
will evaluate them in reverse order, so the last program on the
bwrap command-line is evaluated first). All of them, except
possibly the last, must allow use of the PR_SET_SECCOMP prctl.
This option cannot be combined with <option>--seccomp</option>.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--exec-label <arg choice="plain">LABEL</arg></option></term>
<listitem><para>
Exec Label from the sandbox. On an SELinux system you can specify the SELinux
context for the sandbox process(s).
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--file-label <arg choice="plain">LABEL</arg></option></term>
<listitem><para>
File label for temporary sandbox content. On an SELinux system you can specify
the SELinux context for the sandbox content.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--block-fd <arg choice="plain">FD</arg></option></term>
<listitem><para>
Block the sandbox on reading from FD until some data is available.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--userns-block-fd <arg choice="plain">FD</arg></option></term>
<listitem><para>
Do not initialize the user namespace but wait on FD until it is ready. This allow
external processes (like newuidmap/newgidmap) to setup the user namespace before it
is used by the sandbox process.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--info-fd <arg choice="plain">FD</arg></option></term>
<listitem><para>
Write information in JSON format about the sandbox to FD.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--json-status-fd <arg choice="plain">FD</arg></option></term>
<listitem><para>
Multiple JSON documents are written to <arg choice="plain">FD</arg>,
one per line (<ulink url="https://jsonlines.org/">"JSON lines" format</ulink>).
Each line is a single JSON object.
After <command>bwrap</command> has started the child process inside the sandbox,
it writes an object with a <literal>child-pid</literal> member to the
<option>--json-status-fd</option> (this duplicates the older <option>--info-fd</option>).
The corresponding value is the process ID of the child process in the pid namespace from
which <command>bwrap</command> was run.
If available, the namespace IDs are also included in the object with the <literal>child-pid</literal>;
again, this duplicates the older <option>--info-fd</option>.
When the child process inside the sandbox exits, <command>bwrap</command> writes an object
with an exit-code member, and then closes the <option>--json-status-fd</option>. The value
corresponding to <literal>exit-code</literal> is the exit status of the child, in the usual
shell encoding (n if it exited normally with status n, or 128+n if it was killed by signal n).
Other members may be added to those objects in future versions of <command>bwrap</command>,
and other JSON objects may be added before or after the current objects, so readers must
ignore members and objects that they do not understand.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--new-session</option></term>
<listitem><para>
Create a new terminal session for the sandbox (calls setsid()). This
disconnects the sandbox from the controlling terminal which means
the sandbox can't for instance inject input into the terminal.
</para><para>
Note: In a general sandbox, if you don't use --new-session, it is
recommended to use seccomp to disallow the TIOCSTI ioctl, otherwise
the application can feed keyboard input to the terminal
which can e.g. lead to out-of-sandbox command execution
(see CVE-2017-5226).
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--die-with-parent</option></term>
<listitem><para>
Ensures child process (COMMAND) dies when bwrap's parent dies. Kills (SIGKILL)
all bwrap sandbox processes in sequence from parent to child
including COMMAND process when bwrap or bwrap's parent dies.
See prctl, PR_SET_PDEATHSIG.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--as-pid-1</option></term>
<listitem><para>
Do not create a process with PID=1 in the sandbox to reap child processes.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--cap-add <arg choice="plain">CAP</arg></option></term>
<listitem><para>
Add the specified capability <arg choice="plain">CAP</arg>, e.g.
CAP_DAC_READ_SEARCH, when running as privileged user. It accepts
the special value ALL to add all the permitted caps.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--cap-drop <arg choice="plain">CAP</arg></option></term>
<listitem><para>
Drop the specified capability when running as privileged user. It accepts
the special value ALL to drop all the caps.
By default no caps are left in the sandboxed process. The
<option>--cap-add</option> and <option>--cap-drop</option>
options are processed in the order they are specified on the
command line. Please be careful to the order they are specified.
</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Environment</title>
<variablelist>
<varlistentry>
<term><envar>HOME</envar></term>
<listitem><para>
Used as the cwd in the sandbox if <option>--chdir</option> has not been
explicitly specified and the current cwd is not present inside the sandbox.
The <option>--setenv</option> option can be used to override the value
that is used here.
</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Exit status</title>
<para>
The <command>bwrap</command> command returns the exit status of the
initial application process (pid 2 in the sandbox).
</para>
</refsect1>
</refentry>

107
codex-rs/vendor/bubblewrap/ci/builddeps.sh vendored Executable file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Copyright 2021 Simon McVittie
# SPDX-License-Identifier: LGPL-2.0-or-later
set -eux
set -o pipefail
usage() {
if [ "${1-2}" -ne 0 ]; then
exec >&2
fi
cat <<EOF
Usage: see source code
EOF
exit "${1-2}"
}
opt_clang=
getopt_temp="help"
getopt_temp="$getopt_temp,clang"
getopt_temp="$(getopt -o '' --long "${getopt_temp}" -n "$0" -- "$@")"
eval set -- "$getopt_temp"
unset getopt_temp
while true; do
case "$1" in
(--clang)
clang=yes
shift
;;
(--help)
usage 0
# not reached
;;
(--)
shift
break
;;
(*)
echo 'Error parsing options' >&2
usage 2
;;
esac
done
# No more arguments please
for arg in "$@"; do
usage 2
done
if dpkg-vendor --derives-from Debian; then
apt-get -y update
apt-get -q -y install \
build-essential \
docbook-xml \
docbook-xsl \
libcap-dev \
libselinux1-dev \
libtool \
meson \
pkg-config \
python3 \
xsltproc \
${NULL+}
if [ -n "${opt_clang}" ]; then
apt-get -y install clang
fi
exit 0
fi
if command -v yum; then
yum -y install \
'pkgconfig(libselinux)' \
/usr/bin/eu-readelf \
docbook-style-xsl \
gcc \
git \
libasan \
libcap-devel \
libtool \
libtsan \
libubsan \
libxslt \
make \
meson \
redhat-rpm-config \
rsync \
${NULL+}
if [ -n "${opt_clang}" ]; then
yum -y install clang
fi
exit 0
fi
echo "Unknown distribution" >&2
exit 1
# vim:set sw=4 sts=4 et:

View File

@@ -0,0 +1,6 @@
#!/bin/bash
set -e
echo "kernel.apparmor_restrict_unprivileged_userns = 0" > /etc/sysctl.d/99-userns.conf
sysctl --system

View File

@@ -0,0 +1,80 @@
# shellcheck shell=bash
# bash completion file for bubblewrap commands
#
_bwrap() {
local cur prev words cword
_init_completion || return
# Please keep sorted in LC_ALL=C order
local boolean_options="
--as-pid-1
--assert-userns-disabled
--clearenv
--disable-userns
--help
--new-session
--unshare-all
--unshare-cgroup
--unshare-cgroup-try
--unshare-ipc
--unshare-net
--unshare-pid
--unshare-user
--unshare-user-try
--unshare-uts
--version
"
# Please keep sorted in LC_ALL=C order
local options_with_args="
$boolean_optons
--add-seccomp-fd
--args
--argv0
--bind
--bind-data
--block-fd
--cap-add
--cap-drop
--chdir
--chmod
--dev
--dev-bind
--die-with-parent
--dir
--exec-label
--file
--file-label
--gid
--hostname
--info-fd
--lock-file
--overlay
--overlay-src
--perms
--proc
--remount-ro
--ro-bind
--ro-overlay
--seccomp
--setenv
--size
--symlink
--sync-fd
--tmp-overlay
--uid
--unsetenv
--userns-block-fd
"
if [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W "$boolean_options $options_with_args" -- "$cur" ) )
fi
return 0
}
complete -F _bwrap bwrap
# vim:set ft=bash:

View File

@@ -0,0 +1,35 @@
bash_completion_dir = get_option('bash_completion_dir')
if bash_completion_dir == ''
bash_completion = dependency(
'bash-completion',
version : '>=2.0',
required : false,
)
if bash_completion.found()
if meson.version().version_compare('>=0.51.0')
bash_completion_dir = bash_completion.get_variable(
default_value: '',
pkgconfig: 'completionsdir',
pkgconfig_define: [
'datadir', get_option('prefix') / get_option('datadir'),
],
)
else
bash_completion_dir = bash_completion.get_pkgconfig_variable(
'completionsdir',
default: '',
define_variable: [
'datadir', get_option('prefix') / get_option('datadir'),
],
)
endif
endif
endif
if bash_completion_dir == ''
bash_completion_dir = get_option('datadir') / 'bash-completion' / 'completions'
endif
install_data('bwrap', install_dir : bash_completion_dir)

View File

@@ -0,0 +1,7 @@
if get_option('bash_completion').enabled()
subdir('bash')
endif
if get_option('zsh_completion').enabled()
subdir('zsh')
endif

View File

@@ -0,0 +1,115 @@
#compdef bwrap
_bwrap_args_after_perms_size=(
# Please sort alphabetically (in LC_ALL=C order) by option name
'--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/'
)
_bwrap_args_after_perms=(
# Please sort alphabetically (in LC_ALL=C order) by option name
'--bind-data[Copy from FD to file which is bind-mounted on DEST]: :_guard "[0-9]#" "file descriptor to read content":destination:_files'
'--dir[Create dir at DEST]:directory to create:_files -/'
'--file[Copy from FD to destination DEST]: :_guard "[0-9]#" "file descriptor to read content from":destination:_files'
'--ro-bind-data[Copy from FD to file which is readonly bind-mounted on DEST]: :_guard "[0-9]#" "file descriptor to read content from":destination:_files'
'--size[Set size in bytes for next action argument]: :->after_perms_size'
'--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/'
)
_bwrap_args_after_size=(
# Please sort alphabetically (in LC_ALL=C order) by option name
'--perms[Set permissions for next action argument]: :_guard "[0-7]#" "permissions in octal": :->after_perms_size'
'--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/'
)
_bwrap_args=(
'*::arguments:_normal'
$_bwrap_args_after_perms
# Please sort alphabetically (in LC_ALL=C order) by option name
'--add-seccomp-fd[Load and use seccomp rules from FD]: :_guard "[0-9]#" "file descriptor to read seccomp rules from"'
'--assert-userns-disabled[Fail unless further use of user namespace inside sandbox is disabled]'
'--args[Parse NUL-separated args from FD]: :_guard "[0-9]#" "file descriptor with NUL-separated arguments"'
'--argv0[Set argv0 to the value VALUE before running the program]:value:'
'--as-pid-1[Do not install a reaper process with PID=1]'
'--bind-try[Equal to --bind but ignores non-existent SRC]:source:_files:destination:_files'
'--bind[Bind mount the host path SRC on DEST]:source:_files:destination:_files'
'--block-fd[Block on FD until some data to read is available]: :_guard "[0-9]#" "file descriptor to block on"'
'--cap-add[Add cap CAP when running as privileged user]:capability to add:->caps'
'--cap-drop[Drop cap CAP when running as privileged user]:capability to add:->caps'
'--chdir[Change directory to DIR]:working directory for sandbox: _files -/'
'--chmod[Set permissions]: :_guard "[0-7]#" "permissions in octal":path to set permissions:_files'
'--clearenv[Unset all environment variables]'
'--dev-bind-try[Equal to --dev-bind but ignores non-existent SRC]:source:_files:destination:_files'
'--dev-bind[Bind mount the host path SRC on DEST, allowing device access]:source:_files:destination:_files'
'--dev[Mount new dev on DEST]:mount point for /dev:_files -/'
"--die-with-parent[Kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.]"
'--disable-userns[Disable further use of user namespaces inside sandbox]'
'--exec-label[Exec label for the sandbox]:SELinux label:_selinux_contexts'
'--file-label[File label for temporary sandbox content]:SELinux label:_selinux_contexts'
'--gid[Custom gid in the sandbox (requires --unshare-user or --userns)]: :_guard "[0-9]#" "numeric group ID"'
'--help[Print help and exit]'
'--hostname[Custom hostname in the sandbox (requires --unshare-uts)]:hostname:'
'--info-fd[Write information about the running container to FD]: :_guard "[0-9]#" "file descriptor to write to"'
'--json-status-fd[Write container status to FD as multiple JSON documents]: :_guard "[0-9]#" "file descriptor to write to"'
'--lock-file[Take a lock on DEST while sandbox is running]:lock file:_files'
'--mqueue[Mount new mqueue on DEST]:mount point for mqueue:_files -/'
'--new-session[Create a new terminal session]'
'--perms[Set permissions for next action argument]: :_guard "[0-7]#" "permissions in octal": :->after_perms'
'--pidns[Use this user namespace (as parent namespace if using --unshare-pid)]: :'
'--proc[Mount new procfs on DEST]:mount point for procfs:_files -/'
'--remount-ro[Remount DEST as readonly; does not recursively remount]:mount point to remount read-only:_files'
'--ro-bind-try[Equal to --ro-bind but ignores non-existent SRC]:source:_files:destination:_files'
'--ro-bind[Bind mount the host path SRC readonly on DEST]:source:_files:destination:_files'
'--seccomp[Load and use seccomp rules from FD]: :_guard "[0-9]#" "file descriptor to read seccomp rules from"'
'--setenv[Set an environment variable]:variable to set:_parameters -g "*export*":value of variable: :'
'--size[Set size in bytes for next action argument]: :->after_size'
'--symlink[Create symlink at DEST with target SRC]:symlink target:_files:symlink to create:_files:'
'--sync-fd[Keep this fd open while sandbox is running]: :_guard "[0-9]#" "file descriptor to keep open"'
'--uid[Custom uid in the sandbox (requires --unshare-user or --userns)]: :_guard "[0-9]#" "numeric group ID"'
'(--clearenv)--unsetenv[Unset an environment variable]:variable to unset:_parameters -g "*export*"'
'--unshare-all[Unshare every namespace we support by default]'
'--unshare-cgroup-try[Create new cgroup namespace if possible else continue by skipping it]'
'--unshare-cgroup[Create new cgroup namespace]'
'--unshare-ipc[Create new ipc namespace]'
'--unshare-net[Create new network namespace]'
'--unshare-pid[Create new pid namespace]'
'(--userns --userns2)--unshare-user[Create new user namespace (may be automatically implied if not setuid)]'
'--unshare-user-try[Create new user namespace if possible else continue by skipping it]'
'--unshare-uts[Create new uts namespace]'
'(--unshare-user)--userns[Use this user namespace (cannot combine with --unshare-user)]: :'
'--userns-block-fd[Block on FD until the user namespace is ready]: :_guard "[0-9]#" "file descriptor to block on"'
'(--unshare-user)--userns2[After setup switch to this user namespace, only useful with --userns]: :'
'--version[Print version]'
)
_bwrap() {
_arguments -S $_bwrap_args
case "$state" in
after_perms)
_values -S ' ' 'option' $_bwrap_args_after_perms
;;
after_size)
_values -S ' ' 'option' $_bwrap_args_after_size
;;
after_perms_size)
_values -S ' ' 'option' $_bwrap_args_after_perms_size
;;
caps)
# $ grep -E '#define\sCAP_\w+\s+[0-9]+' /usr/include/linux/capability.h | awk '{print $2}' | xargs echo
local all_caps=(
CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_FSETID \
CAP_KILL CAP_SETGID CAP_SETUID CAP_SETPCAP CAP_LINUX_IMMUTABLE \
CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_ADMIN CAP_NET_RAW \
CAP_IPC_LOCK CAP_IPC_OWNER CAP_SYS_MODULE CAP_SYS_RAWIO CAP_SYS_CHROOT \
CAP_SYS_PTRACE CAP_SYS_PACCT CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_NICE \
CAP_SYS_RESOURCE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_MKNOD CAP_LEASE \
CAP_AUDIT_WRITE CAP_AUDIT_CONTROL CAP_SETFCAP CAP_MAC_OVERRIDE \
CAP_MAC_ADMIN CAP_SYSLOG CAP_WAKE_ALARM CAP_BLOCK_SUSPEND CAP_AUDIT_READ
)
_values 'caps' $all_caps
;;
esac
}

View File

@@ -0,0 +1,7 @@
zsh_completion_dir = get_option('zsh_completion_dir')
if zsh_completion_dir == ''
zsh_completion_dir = get_option('datadir') / 'zsh' / 'site-functions'
endif
install_data('_bwrap', install_dir : zsh_completion_dir)

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Use bubblewrap to run /bin/sh reusing the host OS binaries (/usr), but with
# separate /tmp, /home, /var, /run, and /etc. For /etc we just inherit the
# host's resolv.conf, and set up "stub" passwd/group files. Not sharing
# /home for example is intentional. If you wanted to, you could design
# a bwrap-using program that shared individual parts of /home, perhaps
# public content.
#
# Another way to build on this example is to remove --share-net to disable
# networking.
set -euo pipefail
(exec bwrap --ro-bind /usr /usr \
--dir /tmp \
--dir /var \
--symlink ../tmp var/tmp \
--proc /proc \
--dev /dev \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--symlink usr/bin /bin \
--symlink usr/sbin /sbin \
--chdir / \
--unshare-all \
--share-net \
--die-with-parent \
--dir /run/user/$(id -u) \
--setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \
--setenv PS1 "bwrap-demo$ " \
--file 11 /etc/passwd \
--file 12 /etc/group \
/bin/sh) \
11< <(getent passwd $UID 65534) \
12< <(getent group $(id -g) 65534)

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# For this to work you first have to run these commands:
# curl -O http://sdk.gnome.org/nightly/keys/nightly.gpg
# flatpak --user remote-add --gpg-key=nightly.gpg gnome-nightly http://sdk.gnome.org/nightly/repo/
# flatpak --user install gnome-nightly org.gnome.Platform
# flatpak --user install gnome-nightly org.gnome.Weather
mkdir -p ~/.var/app/org.gnome.Weather/cache ~/.var/app/org.gnome.Weather/config ~/.var/app/org.gnome.Weather/data
(
exec bwrap \
--ro-bind ~/.local/share/flatpak/runtime/org.gnome.Platform/x86_64/master/active/files /usr \
--lock-file /usr/.ref \
--ro-bind ~/.local/share/flatpak/app/org.gnome.Weather/x86_64/master/active/files/ /app \
--lock-file /app/.ref \
--dev /dev \
--proc /proc \
--dir /tmp \
--symlink /tmp /var/tmp \
--symlink /run /var/run \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--symlink usr/bin /bin \
--symlink usr/sbin /sbin \
--symlink usr/etc /etc \
--dir /run/user/`id -u` \
--ro-bind /etc/machine-id /usr/etc/machine-id \
--ro-bind /etc/resolv.conf /run/host/monitor/resolv.conf \
--ro-bind /sys/block /sys/block \
--ro-bind /sys/bus /sys/bus \
--ro-bind /sys/class /sys/class \
--ro-bind /sys/dev /sys/dev \
--ro-bind /sys/devices /sys/devices \
--dev-bind /dev/dri /dev/dri \
--bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X99 \
--bind ~/.var/app/org.gnome.Weather ~/.var/app/org.gnome.Weather \
--bind ~/.config/dconf ~/.config/dconf \
--bind /run/user/`id -u`/dconf /run/user/`id -u`/dconf \
--unshare-pid \
--setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \
--setenv DISPLAY :99 \
--setenv GI_TYPELIB_PATH /app/lib/girepository-1.0 \
--setenv GST_PLUGIN_PATH /app/lib/gstreamer-1.0 \
--setenv LD_LIBRARY_PATH /app/lib:/usr/lib/GL \
--setenv DCONF_USER_CONFIG_DIR .config/dconf \
--setenv PATH /app/bin:/usr/bin \
--setenv XDG_CONFIG_DIRS /app/etc/xdg:/etc/xdg \
--setenv XDG_DATA_DIRS /app/share:/usr/share \
--setenv SHELL /bin/sh \
--setenv XDG_CACHE_HOME ~/.var/app/org.gnome.Weather/cache \
--setenv XDG_CONFIG_HOME ~/.var/app/org.gnome.Weather/config \
--setenv XDG_DATA_HOME ~/.var/app/org.gnome.Weather/data \
--file 10 /run/user/`id -u`/flatpak-info \
--bind-data 11 /usr/etc/passwd \
--bind-data 12 /usr/etc/group \
--seccomp 13 \
/bin/sh) \
11< <(getent passwd $UID 65534 ) \
12< <(getent group $(id -g) 65534) \
13< `dirname $0`/flatpak.bpf \
10<<EOF
[Application]
name=org.gnome.Weather
runtime=runtime/org.gnome.Platform/x86_64/master
EOF

Binary file not shown.

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import os, select, subprocess, sys, json
pipe_info = os.pipe()
userns_block = os.pipe()
pid = os.fork()
if pid != 0:
os.close(pipe_info[1])
os.close(userns_block[0])
select.select([pipe_info[0]], [], [])
data = json.load(os.fdopen(pipe_info[0]))
child_pid = str(data['child-pid'])
subprocess.call(["newuidmap", child_pid, "0", str(os.getuid()), "1"])
subprocess.call(["newgidmap", child_pid, "0", str(os.getgid()), "1"])
os.write(userns_block[1], b'1')
else:
os.close(pipe_info[0])
os.close(userns_block[1])
os.set_inheritable(pipe_info[1], True)
os.set_inheritable(userns_block[0], True)
args = ["bwrap",
"bwrap",
"--unshare-all",
"--unshare-user",
"--userns-block-fd", "%i" % userns_block[0],
"--info-fd", "%i" % pipe_info[1],
"--bind", "/", "/",
"cat", "/proc/self/uid_map"]
os.execlp(*args)

171
codex-rs/vendor/bubblewrap/meson.build vendored Normal file
View File

@@ -0,0 +1,171 @@
project(
'bubblewrap',
'c',
version : '0.11.0',
meson_version : '>=0.49.0',
default_options : [
'warning_level=2',
],
)
cc = meson.get_compiler('c')
add_project_arguments('-D_GNU_SOURCE', language : 'c')
common_include_directories = include_directories('.')
# Keep this in sync with ostree, except remove -Wall (part of Meson
# warning_level 2) and -Werror=declaration-after-statement
add_project_arguments(
cc.get_supported_arguments([
'-Werror=shadow',
'-Werror=empty-body',
'-Werror=strict-prototypes',
'-Werror=missing-prototypes',
'-Werror=implicit-function-declaration',
'-Werror=pointer-arith',
'-Werror=init-self',
'-Werror=missing-declarations',
'-Werror=return-type',
'-Werror=overflow',
'-Werror=int-conversion',
'-Werror=parenthesis',
'-Werror=incompatible-pointer-types',
'-Werror=misleading-indentation',
'-Werror=missing-include-dirs',
'-Werror=aggregate-return',
# Extra warnings specific to bubblewrap
'-Werror=switch-default',
'-Wswitch-enum',
# Deliberately not warning about these, ability to zero-initialize
# a struct is a feature
'-Wno-missing-field-initializers',
'-Wno-error=missing-field-initializers',
]),
language : 'c',
)
if (
cc.has_argument('-Werror=format=2')
and cc.has_argument('-Werror=format-security')
and cc.has_argument('-Werror=format-nonliteral')
)
add_project_arguments([
'-Werror=format=2',
'-Werror=format-security',
'-Werror=format-nonliteral',
], language : 'c')
endif
bash = find_program('bash', required : false)
if get_option('python') == ''
python = find_program('python3')
else
python = find_program(get_option('python'))
endif
libcap_dep = dependency('libcap', required : true)
selinux_dep = dependency(
'libselinux',
version : '>=2.1.9',
# if disabled, Meson will behave as though libselinux was not found
required : get_option('selinux'),
)
cdata = configuration_data()
cdata.set_quoted(
'PACKAGE_STRING',
'@0@ @1@'.format(meson.project_name(), meson.project_version()),
)
if selinux_dep.found()
cdata.set('HAVE_SELINUX', 1)
if selinux_dep.version().version_compare('>=2.3')
cdata.set('HAVE_SELINUX_2_3', 1)
endif
endif
if get_option('require_userns')
cdata.set('ENABLE_REQUIRE_USERNS', 1)
endif
configure_file(
output : 'config.h',
configuration : cdata,
)
if meson.is_subproject() and get_option('program_prefix') == ''
error('program_prefix option must be set when bwrap is a subproject')
endif
if get_option('bwrapdir') != ''
bwrapdir = get_option('bwrapdir')
elif meson.is_subproject()
bwrapdir = get_option('libexecdir')
else
bwrapdir = get_option('bindir')
endif
bwrap = executable(
get_option('program_prefix') + 'bwrap',
[
'bubblewrap.c',
'bind-mount.c',
'network.c',
'utils.c',
],
build_rpath : get_option('build_rpath'),
install : true,
install_dir : bwrapdir,
install_rpath : get_option('install_rpath'),
dependencies : [selinux_dep, libcap_dep],
)
manpages_xsl = 'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl'
xsltproc = find_program('xsltproc', required : get_option('man'))
build_man_page = false
if xsltproc.found() and not meson.is_subproject()
if run_command([
xsltproc, '--nonet', manpages_xsl,
], check : false).returncode() == 0
message('Docbook XSL found, man page enabled')
build_man_page = true
elif get_option('man').enabled()
error('Man page requested, but Docbook XSL stylesheets not found')
else
message('Docbook XSL not found, man page disabled automatically')
endif
endif
if build_man_page
custom_target(
'bwrap.1',
output : 'bwrap.1',
input : 'bwrap.xml',
command : [
xsltproc,
'--nonet',
'--stringparam', 'man.output.quietly', '1',
'--stringparam', 'funcsynopsis.style', 'ansi',
'--stringparam', 'man.th.extra1.suppress', '1',
'--stringparam', 'man.authors.section.enabled', '0',
'--stringparam', 'man.copyright.section.enabled', '0',
'-o', '@OUTPUT@',
manpages_xsl,
'@INPUT@',
],
install : true,
install_dir : get_option('mandir') / 'man1',
)
endif
if not meson.is_subproject()
subdir('completions')
endif
if get_option('tests')
subdir('tests')
endif

View File

@@ -0,0 +1,73 @@
option(
'bash_completion',
type : 'feature',
description : 'install bash completion script',
value : 'enabled',
)
option(
'bash_completion_dir',
type : 'string',
description : 'install bash completion script in this directory',
value : '',
)
option(
'bwrapdir',
type : 'string',
description : 'install bwrap in this directory [default: bindir, or libexecdir in subprojects]',
)
option(
'build_rpath',
type : 'string',
description : 'set a RUNPATH or RPATH on the bwrap executable',
)
option(
'install_rpath',
type : 'string',
description : 'set a RUNPATH or RPATH on the bwrap executable',
)
option(
'man',
type : 'feature',
description : 'generate man pages',
value : 'auto',
)
option(
'program_prefix',
type : 'string',
description : 'Prepend string to bwrap executable name, for use with subprojects',
)
option(
'python',
type : 'string',
description : 'Path to Python 3, or empty to use python3',
)
option(
'require_userns',
type : 'boolean',
description : 'require user namespaces by default when installed setuid',
value : false,
)
option(
'selinux',
type : 'feature',
description : 'enable optional SELINUX support',
value : 'auto',
)
option(
'tests',
type : 'boolean',
description : 'build tests',
value : true,
)
option(
'zsh_completion',
type : 'feature',
description : 'install zsh completion script',
value : 'enabled',
)
option(
'zsh_completion_dir',
type : 'string',
description : 'install zsh completion script in this directory',
value : '',
)

199
codex-rs/vendor/bubblewrap/network.c vendored Normal file
View File

@@ -0,0 +1,199 @@
/* bubblewrap
* Copyright (C) 2016 Alexander Larsson
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <linux/loop.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include "utils.h"
#include "network.h"
static void *
add_rta (struct nlmsghdr *header,
int type,
size_t size)
{
struct rtattr *rta;
size_t rta_size = RTA_LENGTH (size);
rta = (struct rtattr *) ((char *) header + NLMSG_ALIGN (header->nlmsg_len));
rta->rta_type = type;
rta->rta_len = rta_size;
header->nlmsg_len = NLMSG_ALIGN (header->nlmsg_len) + rta_size;
return RTA_DATA (rta);
}
static int
rtnl_send_request (int rtnl_fd,
struct nlmsghdr *header)
{
struct sockaddr_nl dst_addr = { .nl_family = AF_NETLINK, .nl_pid = 0, .nl_groups = 0 };
ssize_t sent;
sent = TEMP_FAILURE_RETRY (sendto (rtnl_fd, (void *) header, header->nlmsg_len, 0,
(struct sockaddr *) &dst_addr, sizeof (dst_addr)));
if (sent < 0)
return -1;
return 0;
}
static int
rtnl_read_reply (int rtnl_fd,
unsigned int seq_nr)
{
char buffer[1024];
ssize_t received;
struct nlmsghdr *rheader;
while (1)
{
received = TEMP_FAILURE_RETRY (recv (rtnl_fd, buffer, sizeof (buffer), 0));
if (received < 0)
return -1;
rheader = (struct nlmsghdr *) buffer;
while (received >= NLMSG_HDRLEN)
{
if (rheader->nlmsg_seq != seq_nr)
return -1;
if ((pid_t)rheader->nlmsg_pid != getpid ())
return -1;
if (rheader->nlmsg_type == NLMSG_ERROR)
{
uint32_t *err = NLMSG_DATA (rheader);
if (*err == 0)
return 0;
return -1;
}
if (rheader->nlmsg_type == NLMSG_DONE)
return 0;
rheader = NLMSG_NEXT (rheader, received);
}
}
}
static int
rtnl_do_request (int rtnl_fd,
struct nlmsghdr *header)
{
if (rtnl_send_request (rtnl_fd, header) != 0)
return -1;
if (rtnl_read_reply (rtnl_fd, header->nlmsg_seq) != 0)
return -1;
return 0;
}
static struct nlmsghdr *
rtnl_setup_request (char *buffer,
int type,
int flags,
size_t size)
{
struct nlmsghdr *header;
size_t len = NLMSG_LENGTH (size);
static uint32_t counter = 0;
memset (buffer, 0, len);
header = (struct nlmsghdr *) buffer;
header->nlmsg_len = len;
header->nlmsg_type = type;
header->nlmsg_flags = flags | NLM_F_REQUEST;
header->nlmsg_seq = counter++;
header->nlmsg_pid = getpid ();
return header;
}
void
loopback_setup (void)
{
int r, if_loopback;
cleanup_fd int rtnl_fd = -1;
char buffer[1024];
struct sockaddr_nl src_addr = { .nl_family = AF_NETLINK, .nl_pid = 0, .nl_groups = 0 };
struct nlmsghdr *header;
struct ifaddrmsg *addmsg;
struct ifinfomsg *infomsg;
struct in_addr *ip_addr;
src_addr.nl_pid = getpid ();
if_loopback = (int) if_nametoindex ("lo");
if (if_loopback <= 0)
die_with_error ("loopback: Failed to look up lo");
rtnl_fd = socket (PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
if (rtnl_fd < 0)
die_with_error ("loopback: Failed to create NETLINK_ROUTE socket");
r = bind (rtnl_fd, (struct sockaddr *) &src_addr, sizeof (src_addr));
if (r < 0)
die_with_error ("loopback: Failed to bind NETLINK_ROUTE socket");
header = rtnl_setup_request (buffer, RTM_NEWADDR,
NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK,
sizeof (struct ifaddrmsg));
addmsg = NLMSG_DATA (header);
addmsg->ifa_family = AF_INET;
addmsg->ifa_prefixlen = 8;
addmsg->ifa_flags = IFA_F_PERMANENT;
addmsg->ifa_scope = RT_SCOPE_HOST;
addmsg->ifa_index = if_loopback;
ip_addr = add_rta (header, IFA_LOCAL, sizeof (*ip_addr));
ip_addr->s_addr = htonl (INADDR_LOOPBACK);
ip_addr = add_rta (header, IFA_ADDRESS, sizeof (*ip_addr));
ip_addr->s_addr = htonl (INADDR_LOOPBACK);
assert (header->nlmsg_len < sizeof (buffer));
if (rtnl_do_request (rtnl_fd, header) != 0)
die_with_error ("loopback: Failed RTM_NEWADDR");
header = rtnl_setup_request (buffer, RTM_NEWLINK,
NLM_F_ACK,
sizeof (struct ifinfomsg));
infomsg = NLMSG_DATA (header);
infomsg->ifi_family = AF_UNSPEC;
infomsg->ifi_type = 0;
infomsg->ifi_index = if_loopback;
infomsg->ifi_flags = IFF_UP;
infomsg->ifi_change = IFF_UP;
assert (header->nlmsg_len < sizeof (buffer));
if (rtnl_do_request (rtnl_fd, header) != 0)
die_with_error ("loopback: Failed RTM_NEWLINK");
}

22
codex-rs/vendor/bubblewrap/network.h vendored Normal file
View File

@@ -0,0 +1,22 @@
/* bubblewrap
* Copyright (C) 2016 Alexander Larsson
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
#pragma once
void loopback_setup (void);

View File

@@ -0,0 +1,48 @@
%global commit0 66d12bb23b04e201c5846e325f0b10930ed802f8
%global shortcommit0 %(c=%{commit0}; echo ${c:0:7})
Summary: Core execution tool for unprivileged containers
Name: bubblewrap
Version: 0
Release: 1%{?dist}
#VCS: git:https://github.com/projectatomic/bubblewrap
Source0: https://github.com/projectatomic/%{name}/archive/%{commit0}.tar.gz#/%{name}-%{shortcommit0}.tar.gz
License: LGPLv2+
URL: https://github.com/projectatomic/bubblewrap
BuildRequires: git
# We always run autogen.sh
BuildRequires: autoconf automake libtool
BuildRequires: libcap-devel
BuildRequires: pkgconfig(libselinux)
BuildRequires: libxslt
BuildRequires: docbook-style-xsl
%description
Bubblewrap (/usr/bin/bwrap) is a core execution engine for unprivileged
containers that works as a setuid binary on kernels without
user namespaces.
%prep
%autosetup -Sgit -n %{name}-%{version}
%build
env NOCONFIGURE=1 ./autogen.sh
%configure --disable-silent-rules --with-priv-mode=none
make %{?_smp_mflags}
%install
make install DESTDIR=$RPM_BUILD_ROOT INSTALL="install -p -c"
find $RPM_BUILD_ROOT -name '*.la' -delete
%files
%license COPYING
%doc README.md
%{_datadir}/bash-completion/completions/bwrap
%if (0%{?rhel} != 0 && 0%{?rhel} <= 7)
%attr(4755,root,root) %{_bindir}/bwrap
%else
%{_bindir}/bwrap
%endif
%{_mandir}/man1/*

View File

@@ -0,0 +1,18 @@
bubblewrap release checklist
============================
* Collect release notes in `NEWS`
* Update version number in `meson.build` and release date in `NEWS`
* Commit the changes
* `meson dist -C ${builddir}`
* Do any final smoke-testing, e.g. update a package, install and test it
* `git evtag sign v$VERSION`
* Include the release notes from `NEWS` in the tag message
* `git push --atomic origin main v$VERSION`
* https://github.com/containers/bubblewrap/releases/new
* Fill in the new version's tag in the "Tag version" box
* Title: `$VERSION`
* Copy the release notes into the description
* Upload the tarball that you built with `meson dist`
* Get the `sha256sum` of the tarball and append it to the description
* `Publish release`

View File

@@ -0,0 +1,190 @@
# Core source library for shell script tests; the
# canonical version lives in:
#
# https://github.com/ostreedev/ostree
#
# Known copies are in the following repos:
#
# - https://github.com/containers/bubblewrap
# - https://github.com/coreos/rpm-ostree
#
# Copyright (C) 2017 Colin Walters <walters@verbum.org>
#
# SPDX-License-Identifier: LGPL-2.0-or-later
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
fatal() {
echo $@ 1>&2; exit 1
}
# fatal() is shorter to type, but retain this alias
assert_not_reached () {
fatal "$@"
}
# Some tests look for specific English strings. Use a UTF-8 version
# of the C (POSIX) locale if we have one, or fall back to en_US.UTF-8
# (https://sourceware.org/glibc/wiki/Proposals/C.UTF-8)
#
# If we can't find the locale command assume we have support for C.UTF-8
# (e.g. musl based systems)
if type -p locale >/dev/null; then
export LC_ALL=$(locale -a | grep -iEe '^(C|en_US)\.(UTF-8|utf8)$' | head -n1 || true)
if [ -z "${LC_ALL}" ]; then fatal "Can't find suitable UTF-8 locale"; fi
else
export LC_ALL=C.UTF-8
fi
# A GNU extension, used whenever LC_ALL is not C
unset LANGUAGE
# This should really be the default IMO
export G_DEBUG=fatal-warnings
assert_streq () {
test "$1" = "$2" || fatal "$1 != $2"
}
assert_str_match () {
if ! echo "$1" | grep -E -q "$2"; then
fatal "$1 does not match regexp $2"
fi
}
assert_not_streq () {
(! test "$1" = "$2") || fatal "$1 == $2"
}
assert_has_file () {
test -f "$1" || fatal "Couldn't find '$1'"
}
assert_has_dir () {
test -d "$1" || fatal "Couldn't find '$1'"
}
# Dump ls -al + file contents to stderr, then fatal()
_fatal_print_file() {
file="$1"
shift
ls -al "$file" >&2
sed -e 's/^/# /' < "$file" >&2
fatal "$@"
}
_fatal_print_files() {
file1="$1"
shift
file2="$1"
shift
ls -al "$file1" >&2
sed -e 's/^/# /' < "$file1" >&2
ls -al "$file2" >&2
sed -e 's/^/# /' < "$file2" >&2
fatal "$@"
}
assert_not_has_file () {
if test -f "$1"; then
_fatal_print_file "$1" "File '$1' exists"
fi
}
assert_not_file_has_content () {
fpath=$1
shift
for re in "$@"; do
if grep -q -e "$re" "$fpath"; then
_fatal_print_file "$fpath" "File '$fpath' matches regexp '$re'"
fi
done
}
assert_not_has_dir () {
if test -d "$1"; then
fatal "Directory '$1' exists"
fi
}
assert_file_has_content () {
fpath=$1
shift
for re in "$@"; do
if ! grep -q -e "$re" "$fpath"; then
_fatal_print_file "$fpath" "File '$fpath' doesn't match regexp '$re'"
fi
done
}
assert_file_has_content_once () {
fpath=$1
shift
for re in "$@"; do
if ! test $(grep -e "$re" "$fpath" | wc -l) = "1"; then
_fatal_print_file "$fpath" "File '$fpath' doesn't match regexp '$re' exactly once"
fi
done
}
assert_file_has_content_literal () {
fpath=$1; shift
for s in "$@"; do
if ! grep -q -F -e "$s" "$fpath"; then
_fatal_print_file "$fpath" "File '$fpath' doesn't match fixed string list '$s'"
fi
done
}
assert_file_has_mode () {
mode=$(stat -c '%a' $1)
if [ "$mode" != "$2" ]; then
fatal "File '$1' has wrong mode: expected $2, but got $mode"
fi
}
assert_symlink_has_content () {
if ! test -L "$1"; then
fatal "File '$1' is not a symbolic link"
fi
if ! readlink "$1" | grep -q -e "$2"; then
_fatal_print_file "$1" "Symbolic link '$1' doesn't match regexp '$2'"
fi
}
assert_file_empty() {
if test -s "$1"; then
_fatal_print_file "$1" "File '$1' is not empty"
fi
}
assert_files_equal() {
if ! cmp "$1" "$2"; then
_fatal_print_files "$1" "$2" "File '$1' and '$2' is not equal"
fi
}
# Use to skip all of these tests
skip() {
echo "1..0 # SKIP" "$@"
exit 0
}
report_err () {
local exit_status="$?"
{ { local BASH_XTRACEFD=3; } 2> /dev/null
echo "Unexpected nonzero exit status $exit_status while running: $BASH_COMMAND" >&2
} 3> /dev/null
}
trap report_err ERR

View File

@@ -0,0 +1,115 @@
# shellcheck shell=bash
# Source library for shell script tests.
# Add non-bubblewrap-specific code to libtest-core.sh instead.
#
# Copyright (C) 2017 Colin Walters <walters@verbum.org>
# SPDX-License-Identifier: LGPL-2.0-or-later
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
set -e
if [ -n "${G_TEST_SRCDIR:-}" ]; then
test_srcdir="${G_TEST_SRCDIR}/tests"
else
test_srcdir=$(dirname "$0")
fi
if [ -n "${G_TEST_BUILDDIR:-}" ]; then
test_builddir="${G_TEST_BUILDDIR}/tests"
else
test_builddir=$(dirname "$0")
fi
. "${test_srcdir}/libtest-core.sh"
# Make sure /sbin/getpcaps etc. are in our PATH even if non-root
PATH="$PATH:/usr/sbin:/sbin"
tempdir=$(mktemp -d /var/tmp/tap-test.XXXXXX)
touch "${tempdir}/.testtmp"
cleanup() {
if test -n "${TEST_SKIP_CLEANUP:-}"; then
echo "Skipping cleanup of ${tempdir}"
elif test -f "${tempdir}/.testtmp"; then
rm -rf "${tempdir}"
fi
}
trap cleanup EXIT
cd "${tempdir}"
: "${BWRAP:=bwrap}"
if test -u "$(type -p ${BWRAP})"; then
bwrap_is_suid=true
fi
FUSE_DIR=
for mp in $(grep " fuse[. ]" /proc/self/mounts | grep "user_id=$(id -u)" | awk '{print $2}'); do
if test -d "$mp"; then
echo "# Using $mp as test fuse mount"
FUSE_DIR="$mp"
break
fi
done
if test "$(id -u)" = "0"; then
is_uidzero=true
else
is_uidzero=false
fi
# This is supposed to be an otherwise readable file in an unreadable (by the user) dir
UNREADABLE=/root/.bashrc
if "${is_uidzero}" || test -x "$(dirname "$UNREADABLE")"; then
UNREADABLE=
fi
# https://github.com/projectatomic/bubblewrap/issues/217
# are we on a merged-/usr system?
if [ /lib -ef /usr/lib ]; then
BWRAP_RO_HOST_ARGS="--ro-bind /usr /usr
--ro-bind /etc /etc
--dir /var/tmp
--symlink usr/lib /lib
--symlink usr/lib64 /lib64
--symlink usr/bin /bin
--symlink usr/sbin /sbin
--proc /proc
--dev /dev"
else
BWRAP_RO_HOST_ARGS="--ro-bind /usr /usr
--ro-bind /etc /etc
--ro-bind /bin /bin
--ro-bind-try /lib /lib
--ro-bind-try /lib64 /lib64
--ro-bind-try /sbin /sbin
--ro-bind-try /nix/store /nix/store
--dir /var/tmp
--proc /proc
--dev /dev"
fi
# Default arg, bind whole host fs to /, tmpfs on /tmp
RUN="${BWRAP} --bind / / --tmpfs /tmp"
if [ -z "${BWRAP_MUST_WORK-}" ] && ! $RUN true; then
skip Seems like bwrap is not working at all. Maybe setuid is not working
fi
extract_child_pid() {
grep child-pid "$1" | sed "s/^.*: \([0-9]*\).*/\1/"
}

View File

@@ -0,0 +1,72 @@
test_programs = [
['test-utils', executable(
'test-utils',
'test-utils.c',
'../utils.c',
'../utils.h',
dependencies : [selinux_dep],
include_directories : common_include_directories,
)],
]
executable(
'try-syscall',
'try-syscall.c',
override_options: ['b_sanitize=none'],
)
test_scripts = [
'test-run.sh',
'test-seccomp.py',
'test-specifying-pidns.sh',
'test-specifying-userns.sh',
]
test_env = environment()
test_env.set('BWRAP', bwrap.full_path())
test_env.set('G_TEST_BUILDDIR', meson.current_build_dir() / '..')
test_env.set('G_TEST_SRCDIR', meson.current_source_dir() / '..')
foreach pair : test_programs
name = pair[0]
test_program = pair[1]
if meson.version().version_compare('>=0.50.0')
test(
name,
test_program,
env : test_env,
protocol : 'tap',
)
else
test(
name,
test_program,
env : test_env,
)
endif
endforeach
foreach test_script : test_scripts
if test_script.endswith('.py')
interpreter = python
else
interpreter = bash
endif
if meson.version().version_compare('>=0.50.0')
test(
test_script,
interpreter,
args : [files(test_script)],
env : test_env,
protocol : 'tap',
)
else
test(
test_script,
interpreter,
args : [files(test_script)],
env : test_env,
)
endif
endforeach

692
codex-rs/vendor/bubblewrap/tests/test-run.sh vendored Executable file
View File

@@ -0,0 +1,692 @@
#!/usr/bin/env bash
set -xeuo pipefail
srcd=$(cd $(dirname "$0") && pwd)
. ${srcd}/libtest.sh
bn=$(basename "$0")
test_count=0
ok () {
test_count=$((test_count + 1))
echo ok $test_count "$@"
}
ok_skip () {
ok "# SKIP" "$@"
}
done_testing () {
echo "1..$test_count"
}
# Test help
${BWRAP} --help > help.txt
assert_file_has_content help.txt "usage: ${BWRAP}"
ok "Help works"
for ALT in "" "--unshare-user-try" "--unshare-pid" "--unshare-user-try --unshare-pid"; do
# Test fuse fs as bind source
if [ "x$FUSE_DIR" != "x" ]; then
$RUN $ALT --proc /proc --dev /dev --bind $FUSE_DIR /tmp/foo true
ok "can bind-mount a FUSE directory with $ALT"
else
ok_skip "no FUSE support"
fi
# no --dev => no devpts => no map_root workaround
$RUN $ALT --proc /proc true
ok "can mount /proc with $ALT"
# No network
$RUN $ALT --unshare-net --proc /proc --dev /dev true
ok "can unshare network, create new /dev with $ALT"
# Unreadable file
echo -n "expect EPERM: " >&2
# Test caps when bwrap is not setuid
if test -n "${bwrap_is_suid:-}"; then
CAP="--cap-add ALL"
else
CAP=""
fi
if ! cat /etc/shadow >/dev/null &&
$RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /tmp/foo; then
assert_not_reached Could read /etc/shadow via /tmp/foo bind-mount
fi
if ! cat /etc/shadow >/dev/null &&
$RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /etc/shadow; then
assert_not_reached Could read /etc/shadow
fi
ok "cannot read /etc/shadow with $ALT"
# Unreadable dir
if [ "x$UNREADABLE" != "x" ]; then
echo -n "expect EPERM: " >&2
if $RUN $ALT --unshare-net --proc /proc --dev /dev --bind $UNREADABLE /tmp/foo cat /tmp/foo; then
assert_not_reached Could read $UNREADABLE
fi
ok "cannot read $UNREADABLE with $ALT"
else
ok_skip "not sure what unreadable file to use"
fi
# bind dest in symlink (https://github.com/projectatomic/bubblewrap/pull/119)
$RUN $ALT --dir /tmp/dir --symlink dir /tmp/link --bind /etc /tmp/link true
ok "can bind a destination over a symlink"
done
# Test symlink behaviour
rm -f ./symlink
$RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/null "$(pwd)/symlink" true >&2
readlink ./symlink > target.txt
assert_file_has_content target.txt /dev/null
ok "--symlink works"
$RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/null "$(pwd)/symlink" true >&2
ok "--symlink is idempotent"
if $RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/full "$(pwd)/symlink" true 2>err.txt; then
fatal "creating a conflicting symlink should have failed"
else
assert_file_has_content err.txt "Can't make symlink .*: existing destination is /dev/null"
fi
ok "--symlink doesn't overwrite a conflicting symlink"
# Test devices
$RUN --unshare-pid --dev /dev ls -al /dev/{stdin,stdout,stderr,null,random,urandom,fd,core} >/dev/null
ok "all expected devices were created"
# Test --as-pid-1
$RUN --unshare-pid --as-pid-1 --bind / / bash -c 'echo $$' > as_pid_1.txt
assert_file_has_content as_pid_1.txt "1"
ok "can run as pid 1"
# Test --info-fd and --json-status-fd
if $RUN --unshare-all --info-fd 42 --json-status-fd 43 -- bash -c 'exit 42' 42>info.json 43>json-status.json 2>err.txt; then
fatal "should have been exit 42"
fi
assert_file_has_content info.json '"child-pid": [0-9]'
assert_file_has_content json-status.json '"child-pid": [0-9]'
assert_file_has_content_literal json-status.json '"exit-code": 42'
ok "info and json-status fd"
DATA=$($RUN --proc /proc --unshare-all --info-fd 42 --json-status-fd 43 -- bash -c 'stat -L -c "%n %i" /proc/self/ns/*' 42>info.json 43>json-status.json 2>err.txt)
for NS in "ipc" "mnt" "net" "pid" "uts"; do
want=$(echo "$DATA" | grep "/proc/self/ns/$NS" | awk '{print $2}')
assert_file_has_content info.json "$want"
assert_file_has_content json-status.json "$want"
done
ok "namespace id info in info and json-status fd"
if ! command -v strace >/dev/null || ! strace -h | grep -v -e default | grep -e fault >/dev/null; then
ok_skip "no strace fault injection"
else
! strace -o /dev/null -f -e trace=prctl -e fault=prctl:when=39 $RUN --die-with-parent --json-status-fd 42 true 42>json-status.json
assert_not_file_has_content json-status.json '"exit-code": [0-9]'
ok "pre-exec failure doesn't include exit-code in json-status"
fi
notanexecutable=/
$RUN --json-status-fd 42 $notanexecutable 42>json-status.json || true
assert_not_file_has_content json-status.json '"exit-code": [0-9]'
ok "exec failure doesn't include exit-code in json-status"
# These tests require --unshare-user
if test -n "${bwrap_is_suid:-}"; then
ok_skip "no --cap-add support"
ok_skip "no --cap-add support"
ok_skip "no --disable-userns"
else
BWRAP_RECURSE="$BWRAP --unshare-user --uid 0 --gid 0 --cap-add ALL --bind / / --bind /proc /proc"
# $BWRAP May be inaccessible due to the user namespace so use /proc/self/exe
$BWRAP_RECURSE -- /proc/self/exe --unshare-all --bind / / --bind /proc /proc echo hello > recursive_proc.txt
assert_file_has_content recursive_proc.txt "hello"
ok "can mount /proc recursively"
$BWRAP_RECURSE -- /proc/self/exe --unshare-all ${BWRAP_RO_HOST_ARGS} findmnt > recursive-newroot.txt
assert_file_has_content recursive-newroot.txt "/usr"
ok "can pivot to new rootfs recursively"
$BWRAP --dev-bind / / -- true
! $BWRAP --assert-userns-disabled --dev-bind / / -- true
$BWRAP --unshare-user --disable-userns --dev-bind / / -- true
! $BWRAP --unshare-user --disable-userns --dev-bind / / -- $BWRAP --dev-bind / / -- true
$BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 2 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true"
$BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 100 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true"
$BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "! $BWRAP --unshare-user --dev-bind / / --assert-userns-disabled -- true"
$BWRAP_RECURSE --dev-bind / / -- true
! $BWRAP_RECURSE --assert-userns-disabled --dev-bind / / -- true
$BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- true
! $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- /proc/self/exe --dev-bind / / -- true
$BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 2 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true"
$BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 100 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true"
$BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "! $BWRAP --unshare-user --dev-bind / / --assert-userns-disabled -- true"
ok "can disable nested userns"
fi
# Test error prefixing
if $RUN --unshare-pid --bind /source-enoent /dest true 2>err.txt; then
assert_not_reached "bound nonexistent source"
fi
assert_file_has_content err.txt "^bwrap: Can't find source path.*source-enoent"
ok "error prefixing"
if ! ${is_uidzero}; then
# When invoked as non-root, check that by default we have no caps left
for OPT in "" "--unshare-user-try --as-pid-1" "--unshare-user-try" "--as-pid-1"; do
e=0
$RUN $OPT --unshare-pid getpcaps 1 >&2 2> caps.test || e=$?
sed -e 's/^/# /' < caps.test >&2
test "$e" = 0
assert_not_file_has_content caps.test ': =.*cap'
done
ok "we have no caps as uid != 0"
else
capsh --print | sed -e 's/no-new-privs=0/no-new-privs=1/' > caps.expected
for OPT in "" "--as-pid-1"; do
$RUN $OPT --unshare-pid capsh --print >caps.test
diff -u caps.expected caps.test
done
# And test that we can drop all, as well as specific caps
$RUN $OPT --cap-drop ALL --unshare-pid capsh --print >caps.test
assert_file_has_content caps.test 'Current: =$'
# Check for dropping kill/fowner (we assume all uid 0 callers have this)
# But we should still have net_bind_service for example
$RUN $OPT --cap-drop CAP_KILL --cap-drop CAP_FOWNER --unshare-pid capsh --print >caps.test
# capsh's output format changed from v2.29 -> drops are now indicated with -eip
if grep 'Current: =.*+eip$' caps.test; then
assert_not_file_has_content caps.test '^Current: =.*cap_kill.*+eip$'
assert_not_file_has_content caps.test '^Current: =.*cap_fowner.*+eip$'
assert_file_has_content caps.test '^Current: =.*cap_net_bind_service.*+eip$'
else
assert_file_has_content caps.test '^Current: =eip.*cap_kill.*-eip$'
assert_file_has_content caps.test '^Current: =eip.*cap_fowner.*-eip$'
assert_not_file_has_content caps.test '^Current: =.*cap_net_bind_service.*-eip$'
fi
ok "we have the expected caps as uid 0"
fi
# Test --die-with-parent
cat >lockf-n.py <<EOF
#!/usr/bin/env python3
import struct,fcntl,sys
path = sys.argv[1]
if sys.argv[2] == 'wait':
locktype = fcntl.F_SETLKW
else:
locktype = fcntl.F_SETLK
lockdata = struct.pack("hhqqhh", fcntl.F_WRLCK, 0, 0, 0, 0, 0)
fd=open(sys.argv[1], 'a')
try:
fcntl.fcntl(fd.fileno(), locktype, lockdata)
except IOError as e:
sys.exit(1)
sys.exit(0)
EOF
chmod a+x lockf-n.py
touch lock
for die_with_parent_argv in "--die-with-parent" "--die-with-parent --unshare-pid"; do
# We have to loop here, because bwrap doesn't wait for the lock if
# another process is holding it. If we're unlucky, lockf-n.py will
# be holding it.
bash -c "while true; do $RUN ${die_with_parent_argv} --lock-file $(pwd)/lock sleep 1h; done" &
childshellpid=$!
# Wait for lock to be taken (yes hacky)
for x in $(seq 10); do
if ./lockf-n.py ./lock nowait; then
sleep 1
else
break
fi
done
if ./lockf-n.py ./lock nowait; then
assert_not_reached "timed out waiting for lock"
fi
# Kill the shell, which should kill bwrap (and the sleep)
kill -9 ${childshellpid}
# Lock file should be unlocked
./lockf-n.py ./lock wait
ok "die with parent ${die_with_parent_argv}"
done
printf '%s--dir\0/tmp/hello/world\0' '' > test.args
printf '%s--dir\0/tmp/hello/world2\0' '' > test.args2
printf '%s--dir\0/tmp/hello/world3\0' '' > test.args3
$RUN --args 3 --args 4 --args 5 /bin/sh -c 'test -d /tmp/hello/world && test -d /tmp/hello/world2 && test -d /tmp/hello/world3' 3<test.args 4<test.args2 5<test.args3
ok "we can parse arguments from a fd"
mkdir bin
echo "#!/bin/sh" > bin/--inadvisable-executable-name--
echo "echo hello" >> bin/--inadvisable-executable-name--
chmod +x bin/--inadvisable-executable-name--
PATH="${srcd}:$PATH" $RUN -- sh -c "echo hello" > stdout
assert_file_has_content stdout hello
ok "we can run with --"
PATH="$(pwd)/bin:$PATH" $RUN -- --inadvisable-executable-name-- > stdout
assert_file_has_content stdout hello
ok "we can run an inadvisable executable name with --"
if $RUN -- --dev-bind /dev /dev sh -c 'echo should not have run'; then
assert_not_reached "'--dev-bind' should have been interpreted as a (silly) executable name"
fi
ok "options like --dev-bind are defanged by --"
if command -v mktemp > /dev/null; then
tempfile="$(mktemp /tmp/bwrap-test-XXXXXXXX)"
echo "hello" > "$tempfile"
$BWRAP --bind / / cat "$tempfile" > stdout
assert_file_has_content stdout hello
ok "bind-mount of / exposes real /tmp"
$BWRAP --bind / / --bind /tmp /tmp cat "$tempfile" > stdout
assert_file_has_content stdout hello
ok "bind-mount of /tmp exposes real /tmp"
if [ -d /mnt ] && [ ! -L /mnt ]; then
$BWRAP --bind / / --bind /tmp /mnt cat "/mnt/${tempfile#/tmp/}" > stdout
assert_file_has_content stdout hello
ok "bind-mount of /tmp onto /mnt exposes real /tmp"
else
ok_skip "/mnt does not exist or is a symlink"
fi
else
ok_skip "mktemp not found"
ok_skip "mktemp not found"
ok_skip "mktemp not found"
fi
if $RUN test -d /tmp/oldroot; then
assert_not_reached "/tmp/oldroot should not be visible"
fi
if $RUN test -d /tmp/newroot; then
assert_not_reached "/tmp/newroot should not be visible"
fi
echo "hello" > input.$$
$BWRAP --bind / / --bind "$(pwd)" /tmp cat /tmp/input.$$ > stdout
assert_file_has_content stdout hello
if $BWRAP --bind / / --bind "$(pwd)" /tmp test -d /tmp/oldroot; then
assert_not_reached "/tmp/oldroot should not be visible"
fi
if $BWRAP --bind / / --bind "$(pwd)" /tmp test -d /tmp/newroot; then
assert_not_reached "/tmp/newroot should not be visible"
fi
ok "we can mount another directory onto /tmp"
echo "hello" > input.$$
$RUN --bind "$(pwd)" /tmp/here cat /tmp/here/input.$$ > stdout
assert_file_has_content stdout hello
if $RUN --bind "$(pwd)" /tmp/here test -d /tmp/oldroot; then
assert_not_reached "/tmp/oldroot should not be visible"
fi
if $RUN --bind "$(pwd)" /tmp/here test -d /tmp/newroot; then
assert_not_reached "/tmp/newroot should not be visible"
fi
ok "we can mount another directory inside /tmp"
touch some-file
mkdir -p some-dir
rm -fr new-dir-mountpoint
rm -fr new-file-mountpoint
$RUN \
--bind "$(pwd -P)/some-dir" "$(pwd -P)/new-dir-mountpoint" \
--bind "$(pwd -P)/some-file" "$(pwd -P)/new-file-mountpoint" \
true
command stat -c '%a' new-dir-mountpoint > new-dir-permissions
assert_file_has_content new-dir-permissions 755
command stat -c '%a' new-file-mountpoint > new-file-permissions
assert_file_has_content new-file-permissions 444
ok "Files and directories created as mount points have expected permissions"
if [ -S /dev/log ]; then
$RUN --bind / / --bind "$(realpath /dev/log)" "$(realpath /dev/log)" true
ok "Can bind-mount a socket (/dev/log) onto a socket"
else
ok_skip "- /dev/log is not a socket, cannot test bubblewrap#409"
fi
mkdir -p dir-already-existed
chmod 0710 dir-already-existed
mkdir -p dir-already-existed2
chmod 0754 dir-already-existed2
rm -fr new-dir-default-perms
rm -fr new-dir-set-perms
$RUN \
--perms 1741 --dir "$(pwd -P)/new-dir-set-perms" \
--dir "$(pwd -P)/dir-already-existed" \
--perms 0741 --dir "$(pwd -P)/dir-already-existed2" \
--dir "$(pwd -P)/dir-chmod" \
--chmod 1755 "$(pwd -P)/dir-chmod" \
--dir "$(pwd -P)/new-dir-default-perms" \
true
command stat -c '%a' new-dir-default-perms > new-dir-permissions
assert_file_has_content new-dir-permissions '^755$'
command stat -c '%a' new-dir-set-perms > new-dir-permissions
assert_file_has_content new-dir-permissions '^1741$'
command stat -c '%a' dir-already-existed > dir-permissions
assert_file_has_content dir-permissions '^710$'
command stat -c '%a' dir-already-existed2 > dir-permissions
assert_file_has_content dir-permissions '^754$'
command stat -c '%a' dir-chmod > dir-permissions
assert_file_has_content dir-permissions '^1755$'
ok "Directories created explicitly have expected permissions"
rm -fr parent
rm -fr parent-of-1777
rm -fr parent-of-0755
rm -fr parent-of-0644
rm -fr parent-of-0750
rm -fr parent-of-0710
rm -fr parent-of-0720
rm -fr parent-of-0640
rm -fr parent-of-0700
rm -fr parent-of-0600
rm -fr parent-of-0705
rm -fr parent-of-0604
rm -fr parent-of-0000
$RUN \
--dir "$(pwd -P)"/parent/dir \
--perms 1777 --dir "$(pwd -P)"/parent-of-1777/dir \
--perms 0755 --dir "$(pwd -P)"/parent-of-0755/dir \
--perms 0644 --dir "$(pwd -P)"/parent-of-0644/dir \
--perms 0750 --dir "$(pwd -P)"/parent-of-0750/dir \
--perms 0710 --dir "$(pwd -P)"/parent-of-0710/dir \
--perms 0720 --dir "$(pwd -P)"/parent-of-0720/dir \
--perms 0640 --dir "$(pwd -P)"/parent-of-0640/dir \
--perms 0700 --dir "$(pwd -P)"/parent-of-0700/dir \
--perms 0600 --dir "$(pwd -P)"/parent-of-0600/dir \
--perms 0705 --dir "$(pwd -P)"/parent-of-0705/dir \
--perms 0604 --dir "$(pwd -P)"/parent-of-0604/dir \
--perms 0000 --dir "$(pwd -P)"/parent-of-0000/dir \
true
command stat -c '%a' parent > dir-permissions
assert_file_has_content dir-permissions '^755$'
command stat -c '%a' parent-of-1777 > dir-permissions
assert_file_has_content dir-permissions '^755$'
command stat -c '%a' parent-of-0755 > dir-permissions
assert_file_has_content dir-permissions '^755$'
command stat -c '%a' parent-of-0644 > dir-permissions
assert_file_has_content dir-permissions '^755$'
command stat -c '%a' parent-of-0750 > dir-permissions
assert_file_has_content dir-permissions '^750$'
command stat -c '%a' parent-of-0710 > dir-permissions
assert_file_has_content dir-permissions '^750$'
command stat -c '%a' parent-of-0720 > dir-permissions
assert_file_has_content dir-permissions '^750$'
command stat -c '%a' parent-of-0640 > dir-permissions
assert_file_has_content dir-permissions '^750$'
command stat -c '%a' parent-of-0700 > dir-permissions
assert_file_has_content dir-permissions '^700$'
command stat -c '%a' parent-of-0600 > dir-permissions
assert_file_has_content dir-permissions '^700$'
command stat -c '%a' parent-of-0705 > dir-permissions
assert_file_has_content dir-permissions '^705$'
command stat -c '%a' parent-of-0604 > dir-permissions
assert_file_has_content dir-permissions '^705$'
command stat -c '%a' parent-of-0000 > dir-permissions
assert_file_has_content dir-permissions '^700$'
chmod -R 0700 parent*
rm -fr parent*
ok "Directories created as parents have expected permissions"
$RUN \
--perms 01777 --tmpfs "$(pwd -P)" \
cat /proc/self/mountinfo >&2
$RUN \
--perms 01777 --tmpfs "$(pwd -P)" \
stat -c '%a' "$(pwd -P)" > dir-permissions
assert_file_has_content dir-permissions '^1777$'
$RUN \
--tmpfs "$(pwd -P)" \
stat -c '%a' "$(pwd -P)" > dir-permissions
assert_file_has_content dir-permissions '^755$'
ok "tmpfs has expected permissions"
# 1048576 = 1 MiB
if test -n "${bwrap_is_suid:-}"; then
if $RUN --size 1048576 --tmpfs "$(pwd -P)" true; then
assert_not_reached "Should not allow --size --tmpfs when setuid"
fi
ok "--size --tmpfs is not allowed when setuid"
elif df --output=size --block-size=1K "$(pwd -P)" >/dev/null 2>/dev/null; then
$RUN \
--size 1048576 --tmpfs "$(pwd -P)" \
df --output=size --block-size=1K "$(pwd -P)" > dir-size
assert_file_has_content dir-size '^ *1024$'
$RUN \
--size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \
stat -c '%a' "$(pwd -P)" > dir-permissions
assert_file_has_content dir-permissions '^1777$'
$RUN \
--size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \
df --output=size --block-size=1K "$(pwd -P)" > dir-size
assert_file_has_content dir-size '^ *1024$'
$RUN \
--perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \
stat -c '%a' "$(pwd -P)" > dir-permissions
assert_file_has_content dir-permissions '^1777$'
$RUN \
--perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \
df --output=size --block-size=1K "$(pwd -P)" > dir-size
assert_file_has_content dir-size '^ *1024$'
ok "tmpfs has expected size"
else
$RUN --size 1048576 --tmpfs "$(pwd -P)" true
$RUN --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" true
$RUN --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" true
ok_skip "df is too old, cannot test --size --tmpfs fully"
fi
$RUN \
--file 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^666$'
$RUN \
--perms 0640 --file 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^640$'
$RUN \
--bind-data 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^600$'
$RUN \
--perms 0640 --bind-data 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^640$'
$RUN \
--ro-bind-data 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^600$'
$RUN \
--perms 0640 --ro-bind-data 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^640$'
ok "files have expected permissions"
if $RUN --size 0 --tmpfs /tmp/a true; then
assert_not_reached Zero tmpfs size allowed
fi
if $RUN --size 123bogus --tmpfs /tmp/a true; then
assert_not_reached Bogus tmpfs size allowed
fi
if $RUN --size '' --tmpfs /tmp/a true; then
assert_not_reached Empty tmpfs size allowed
fi
if $RUN --size -12345678 --tmpfs /tmp/a true; then
assert_not_reached Negative tmpfs size allowed
fi
if $RUN --size ' -12345678' --tmpfs /tmp/a true; then
assert_not_reached Negative tmpfs size with space allowed
fi
# This is 2^64
if $RUN --size 18446744073709551616 --tmpfs /tmp/a true; then
assert_not_reached Overflowing tmpfs size allowed
fi
# This is 2^63 + 1; note that the current max size is SIZE_MAX/2
if $RUN --size 9223372036854775809 --tmpfs /tmp/a true; then
assert_not_reached Too-large tmpfs size allowed
fi
ok "bogus tmpfs size not allowed"
if $RUN --perms 0640 --perms 0640 --tmpfs /tmp/a true; then
assert_not_reached Multiple perms options allowed
fi
if $RUN --size 1048576 --size 1048576 --tmpfs /tmp/a true; then
assert_not_reached Multiple perms options allowed
fi
ok "--perms and --size only allowed once"
FOO= BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout
assert_file_has_content stdout barbaz
FOO=wrong BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout
assert_file_has_content stdout barbaz
FOO=wrong BAR=baz $RUN --unsetenv FOO sh -c 'printf "%s%s" "$FOO" "$BAR"' > stdout
printf baz > reference
assert_files_equal stdout reference
FOO=wrong BAR=wrong $RUN --clearenv /usr/bin/env > stdout
echo "PWD=$(pwd -P)" > reference
assert_files_equal stdout reference
ok "environment manipulation"
$RUN sh -c 'echo $0' > stdout
assert_file_has_content stdout sh
$RUN --argv0 sh sh -c 'echo $0' > stdout
assert_file_has_content stdout sh
$RUN --argv0 right sh -c 'echo $0' > stdout
assert_file_has_content stdout right
ok "argv0 manipulation"
echo "foobar" > file-data
$RUN --proc /proc --dev /dev --bind / / --bind-fd 100 /tmp cat /tmp/file-data 100< . > stdout
assert_file_has_content stdout foobar
ok "bind-fd"
$RUN --chdir / --chdir / true > stdout 2>&1
assert_file_has_content stdout '^bwrap: Only the last --chdir option will take effect$'
ok "warning logged for redundant --chdir"
$RUN --level-prefix --chdir / --chdir / true > stdout 2>&1
assert_file_has_content stdout '^<4>bwrap: Only the last --chdir option will take effect$'
ok "--level-prefix"
if test -n "${bwrap_is_suid:-}"; then
ok_skip "no --overlay support"
ok_skip "no --overlay support"
ok_skip "no --tmp-overlay support"
ok_skip "no --ro-overlay support"
ok_skip "no --overlay-src support"
else
mkdir lower1 lower2 upper work
printf 1 > lower1/a
printf 2 > lower1/b
printf 3 > lower2/b
printf 4 > upper/a
# Check if unprivileged overlayfs is available
if ! unshare -rm mount -t overlay -o lowerdir=lower1,upperdir=upper,workdir=work,userxattr overlay lower2; then
ok_skip "no kernel support for unprivileged overlayfs"
ok_skip "no kernel support for unprivileged overlayfs"
ok_skip "no kernel support for unprivileged overlayfs"
ok_skip "no kernel support for unprivileged overlayfs"
ok_skip "no kernel support for unprivileged overlayfs"
else
# Test --overlay
if $RUN --overlay upper work /tmp true 2>err.txt; then
assert_not_reached At least one --overlay-src not required
fi
assert_file_has_content err.txt "^bwrap: --overlay requires at least one --overlay-src"
$RUN --overlay-src lower1 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^4$'
$RUN --overlay-src lower1 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^2$'
$RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^4$'
$RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^3$'
$RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z sh -c 'printf 5 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout
assert_file_has_content stdout '^5$'
assert_file_has_content upper/b '^5$'
ok "--overlay"
# Test --overlay path escaping
# Coincidentally, ":,\ is the face I make contemplating anyone who might
# need this functionality, not that that's going to stop me from supporting
# it.
mkdir 'lower ":,\' 'upper ":,\' 'work ":,\'
printf 1 > 'lower ":,\'/a
$RUN --overlay-src 'lower ":,\' --overlay 'upper ":,\' 'work ":,\' /tmp/x sh -c 'cat /tmp/x/a; printf 2 > /tmp/x/a; cat /tmp/x/a' > stdout
assert_file_has_content stdout '^12$'
assert_file_has_content 'lower ":,\'/a '^1$'
assert_file_has_content 'upper ":,\'/a '^2$'
ok "--overlay path escaping"
# Test --tmp-overlay
printf 1 > lower1/a
printf 2 > lower1/b
printf 3 > lower2/b
if $RUN --tmp-overlay /tmp true 2>err.txt; then
assert_not_reached At least one --overlay-src not required
fi
assert_file_has_content err.txt "^bwrap: --tmp-overlay requires at least one --overlay-src"
$RUN --overlay-src lower1 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^1$'
$RUN --overlay-src lower1 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^2$'
$RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^1$'
$RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^3$'
$RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z sh -c 'printf 4 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout
assert_file_has_content stdout '^4$'
$RUN --overlay-src lower1 --tmp-overlay /tmp/x --overlay-src lower2 --tmp-overlay /tmp/y sh -c 'cat /tmp/x/b; printf 4 > /tmp/x/b; cat /tmp/x/b; cat /tmp/y/b' > stdout
assert_file_has_content stdout '^243$'
assert_file_has_content lower1/b '^2$'
assert_file_has_content lower2/b '^3$'
ok "--tmp-overlay"
# Test --ro-overlay
printf 1 > lower1/a
printf 2 > lower1/b
printf 3 > lower2/b
if $RUN --ro-overlay /tmp true 2>err.txt; then
assert_not_reached At least two --overlay-src not required
fi
assert_file_has_content err.txt "^bwrap: --ro-overlay requires at least two --overlay-src"
if $RUN --overlay-src lower1 --ro-overlay /tmp true 2>err.txt; then
assert_not_reached At least two --overlay-src not required
fi
assert_file_has_content err.txt "^bwrap: --ro-overlay requires at least two --overlay-src"
$RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^1$'
$RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^3$'
$RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z sh -c 'printf 4 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout
assert_file_has_content stdout '^3$'
ok "--ro-overlay"
# Test --overlay-src restrictions
if $RUN --overlay-src /tmp true 2>err.txt; then
assert_not_reached Trailing --overlay-src allowed
fi
assert_file_has_content err.txt "^bwrap: --overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay"
if $RUN --overlay-src /tmp --chdir / true 2>err.txt; then
assert_not_reached --overlay-src allowed to precede non-overlay options
fi
assert_file_has_content err.txt "^bwrap: --overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay"
ok "--overlay-src restrictions"
fi
fi
done_testing

View File

@@ -0,0 +1,635 @@
#!/usr/bin/env python3
# Copyright 2021 Simon McVittie
# SPDX-License-Identifier: LGPL-2.0-or-later
import errno
import logging
import os
import subprocess
import sys
import tempfile
import termios
import unittest
try:
import seccomp
except ImportError:
print('1..0 # SKIP cannot import seccomp Python module')
sys.exit(0)
# This is the @default set from systemd as of 2021-10-11
DEFAULT_SET = set('''
brk
cacheflush
clock_getres
clock_getres_time64
clock_gettime
clock_gettime64
clock_nanosleep
clock_nanosleep_time64
execve
exit
exit_group
futex
futex_time64
get_robust_list
get_thread_area
getegid
getegid32
geteuid
geteuid32
getgid
getgid32
getgroups
getgroups32
getpgid
getpgrp
getpid
getppid
getrandom
getresgid
getresgid32
getresuid
getresuid32
getrlimit
getsid
gettid
gettimeofday
getuid
getuid32
membarrier
mmap
mmap2
munmap
nanosleep
pause
prlimit64
restart_syscall
rseq
rt_sigreturn
sched_getaffinity
sched_yield
set_robust_list
set_thread_area
set_tid_address
set_tls
sigreturn
time
ugetrlimit
'''.split())
# This is the @basic-io set from systemd
BASIC_IO_SET = set('''
_llseek
close
close_range
dup
dup2
dup3
lseek
pread64
preadv
preadv2
pwrite64
pwritev
pwritev2
read
readv
write
writev
'''.split())
# This is the @filesystem-io set from systemd
FILESYSTEM_SET = set('''
access
chdir
chmod
close
creat
faccessat
faccessat2
fallocate
fchdir
fchmod
fchmodat
fcntl
fcntl64
fgetxattr
flistxattr
fremovexattr
fsetxattr
fstat
fstat64
fstatat64
fstatfs
fstatfs64
ftruncate
ftruncate64
futimesat
getcwd
getdents
getdents64
getxattr
inotify_add_watch
inotify_init
inotify_init1
inotify_rm_watch
lgetxattr
link
linkat
listxattr
llistxattr
lremovexattr
lsetxattr
lstat
lstat64
mkdir
mkdirat
mknod
mknodat
newfstatat
oldfstat
oldlstat
oldstat
open
openat
openat2
readlink
readlinkat
removexattr
rename
renameat
renameat2
rmdir
setxattr
stat
stat64
statfs
statfs64
statx
symlink
symlinkat
truncate
truncate64
unlink
unlinkat
utime
utimensat
utimensat_time64
utimes
'''.split())
# Miscellaneous syscalls used during process startup, at least on x86_64
ALLOWED = DEFAULT_SET | BASIC_IO_SET | FILESYSTEM_SET | set('''
arch_prctl
ioctl
madvise
mprotect
mremap
prctl
readdir
umask
'''.split())
# Syscalls we will try to use, expecting them to be either allowed or
# blocked by our allow and/or deny lists
TRY_SYSCALLS = [
'chmod',
'chroot',
'clone3',
'ioctl TIOCNOTTY',
'ioctl TIOCSTI CVE-2019-10063',
'ioctl TIOCSTI',
'listen',
'prctl',
]
class Test(unittest.TestCase):
def setUp(self) -> None:
here = os.path.dirname(os.path.abspath(__file__))
if 'G_TEST_SRCDIR' in os.environ:
self.test_srcdir = os.getenv('G_TEST_SRCDIR') + '/tests'
else:
self.test_srcdir = here
if 'G_TEST_BUILDDIR' in os.environ:
self.test_builddir = os.getenv('G_TEST_BUILDDIR') + '/tests'
else:
self.test_builddir = here
self.bwrap = os.getenv('BWRAP', 'bwrap')
self.try_syscall = os.path.join(self.test_builddir, 'try-syscall')
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'true',
],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=2,
)
if completed.returncode != 0:
raise unittest.SkipTest(
'cannot run bwrap (does it need to be setuid?)'
)
def tearDown(self) -> None:
pass
def test_no_seccomp(self) -> None:
for syscall in TRY_SYSCALLS:
print('# {} without seccomp'.format(syscall))
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
self.try_syscall, syscall,
],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=2,
)
if (
syscall == 'ioctl TIOCSTI CVE-2019-10063'
and completed.returncode == errno.ENOENT
):
print('# Cannot test 64-bit syscall parameter on 32-bit')
continue
if syscall == 'clone3':
# If the kernel supports it, we didn't block it so
# it fails with EFAULT. If the kernel doesn't support it,
# it'll fail with ENOSYS instead.
self.assertIn(
completed.returncode,
(errno.ENOSYS, errno.EFAULT),
)
elif syscall.startswith('ioctl') or syscall == 'listen':
self.assertEqual(completed.returncode, errno.EBADF)
else:
self.assertEqual(completed.returncode, errno.EFAULT)
def test_seccomp_allowlist(self) -> None:
with tempfile.TemporaryFile() as allowlist_temp:
allowlist = seccomp.SyscallFilter(seccomp.ERRNO(errno.ENOSYS))
if os.uname().machine == 'x86_64':
# Allow Python and try-syscall to be different word sizes
allowlist.add_arch(seccomp.Arch.X86)
for syscall in ALLOWED:
try:
allowlist.add_rule(seccomp.ALLOW, syscall)
except Exception as e:
print('# Cannot add {} to allowlist: {!r}'.format(syscall, e))
allowlist.export_bpf(allowlist_temp)
for syscall in TRY_SYSCALLS:
print('# allowlist vs. {}'.format(syscall))
allowlist_temp.seek(0, os.SEEK_SET)
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'--seccomp', str(allowlist_temp.fileno()),
self.try_syscall, syscall,
],
pass_fds=(allowlist_temp.fileno(),),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=2,
)
if (
syscall == 'ioctl TIOCSTI CVE-2019-10063'
and completed.returncode == errno.ENOENT
):
print('# Cannot test 64-bit syscall parameter on 32-bit')
continue
if syscall.startswith('ioctl'):
# We allow this, so it is executed (and in this simple
# example, immediately fails)
self.assertEqual(completed.returncode, errno.EBADF)
elif syscall in ('chroot', 'listen', 'clone3'):
# We don't allow these, so they fail with ENOSYS.
# clone3 might also be failing with ENOSYS because
# the kernel genuinely doesn't support it.
self.assertEqual(completed.returncode, errno.ENOSYS)
else:
# We allow this, so it is executed (and in this simple
# example, immediately fails)
self.assertEqual(completed.returncode, errno.EFAULT)
def test_seccomp_denylist(self) -> None:
with tempfile.TemporaryFile() as denylist_temp:
denylist = seccomp.SyscallFilter(seccomp.ALLOW)
if os.uname().machine == 'x86_64':
# Allow Python and try-syscall to be different word sizes
denylist.add_arch(seccomp.Arch.X86)
# Using ECONNREFUSED here because it's unlikely that any of
# these syscalls will legitimately fail with that code, so
# if they fail like this, it will be as a result of seccomp.
denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chmod')
denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chroot')
denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'prctl')
denylist.add_rule(
seccomp.ERRNO(errno.ECONNREFUSED), 'ioctl',
seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI),
)
denylist.export_bpf(denylist_temp)
for syscall in TRY_SYSCALLS:
print('# denylist vs. {}'.format(syscall))
denylist_temp.seek(0, os.SEEK_SET)
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'--seccomp', str(denylist_temp.fileno()),
self.try_syscall, syscall,
],
pass_fds=(denylist_temp.fileno(),),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=2,
)
if (
syscall == 'ioctl TIOCSTI CVE-2019-10063'
and completed.returncode == errno.ENOENT
):
print('# Cannot test 64-bit syscall parameter on 32-bit')
continue
if syscall == 'clone3':
# If the kernel supports it, we didn't block it so
# it fails with EFAULT. If the kernel doesn't support it,
# it'll fail with ENOSYS instead.
self.assertIn(
completed.returncode,
(errno.ENOSYS, errno.EFAULT),
)
elif syscall in ('ioctl TIOCNOTTY', 'listen'):
# Not on the denylist
self.assertEqual(completed.returncode, errno.EBADF)
else:
# We blocked all of these
self.assertEqual(completed.returncode, errno.ECONNREFUSED)
def test_seccomp_stacked(self, allowlist_first=False) -> None:
with tempfile.TemporaryFile(
) as allowlist_temp, tempfile.TemporaryFile(
) as denylist_temp:
# This filter is a simplified version of what Flatpak wants
allowlist = seccomp.SyscallFilter(seccomp.ERRNO(errno.ENOSYS))
denylist = seccomp.SyscallFilter(seccomp.ALLOW)
if os.uname().machine == 'x86_64':
# Allow Python and try-syscall to be different word sizes
allowlist.add_arch(seccomp.Arch.X86)
denylist.add_arch(seccomp.Arch.X86)
for syscall in ALLOWED:
try:
allowlist.add_rule(seccomp.ALLOW, syscall)
except Exception as e:
print('# Cannot add {} to allowlist: {!r}'.format(syscall, e))
denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chmod')
denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chroot')
denylist.add_rule(
seccomp.ERRNO(errno.ECONNREFUSED), 'ioctl',
seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI),
)
# All seccomp programs except the last must allow prctl(),
# because otherwise we wouldn't be able to add the remaining
# seccomp programs. We document that the last program can
# block prctl, so test that.
if allowlist_first:
denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'prctl')
allowlist.export_bpf(allowlist_temp)
denylist.export_bpf(denylist_temp)
for syscall in TRY_SYSCALLS:
print('# stacked vs. {}'.format(syscall))
allowlist_temp.seek(0, os.SEEK_SET)
denylist_temp.seek(0, os.SEEK_SET)
if allowlist_first:
fds = [allowlist_temp.fileno(), denylist_temp.fileno()]
else:
fds = [denylist_temp.fileno(), allowlist_temp.fileno()]
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'--add-seccomp-fd', str(fds[0]),
'--add-seccomp-fd', str(fds[1]),
self.try_syscall, syscall,
],
pass_fds=fds,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=2,
)
if (
syscall == 'ioctl TIOCSTI CVE-2019-10063'
and completed.returncode == errno.ENOENT
):
print('# Cannot test 64-bit syscall parameter on 32-bit')
continue
if syscall == 'ioctl TIOCNOTTY':
# Not denied by the denylist, and allowed by the allowlist
self.assertEqual(completed.returncode, errno.EBADF)
elif syscall in ('clone3', 'listen'):
# We didn't deny these, so the denylist has no effect
# and we fall back to the allowlist, which doesn't
# include them either.
# clone3 might also be failing with ENOSYS because
# the kernel genuinely doesn't support it.
self.assertEqual(completed.returncode, errno.ENOSYS)
elif syscall == 'chroot':
# This is denied by the denylist *and* not allowed by
# the allowlist. The result depends which one we added
# first: the most-recently-added filter "wins".
if allowlist_first:
self.assertEqual(
completed.returncode,
errno.ECONNREFUSED,
)
else:
self.assertEqual(completed.returncode, errno.ENOSYS)
elif syscall == 'prctl':
# We can only put this on the denylist if the denylist
# is the last to be added.
if allowlist_first:
self.assertEqual(
completed.returncode,
errno.ECONNREFUSED,
)
else:
self.assertEqual(completed.returncode, errno.EFAULT)
else:
# chmod is allowed by the allowlist but blocked by the
# denylist. Denying takes precedence over allowing,
# regardless of order.
self.assertEqual(completed.returncode, errno.ECONNREFUSED)
def test_seccomp_stacked_allowlist_first(self) -> None:
self.test_seccomp_stacked(allowlist_first=True)
def test_seccomp_invalid(self) -> None:
with tempfile.TemporaryFile(
) as allowlist_temp, tempfile.TemporaryFile(
) as denylist_temp:
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'--add-seccomp-fd', '-1',
'true',
],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
self.assertIn(b'bwrap: Invalid fd: -1\n', completed.stderr)
self.assertEqual(completed.returncode, 1)
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'--seccomp', '0a',
'true',
],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
self.assertIn(b'bwrap: Invalid fd: 0a\n', completed.stderr)
self.assertEqual(completed.returncode, 1)
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'--add-seccomp-fd', str(denylist_temp.fileno()),
'--seccomp', str(allowlist_temp.fileno()),
'true',
],
pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
self.assertIn(
b'bwrap: --seccomp cannot be combined with --add-seccomp-fd\n',
completed.stderr,
)
self.assertEqual(completed.returncode, 1)
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'--seccomp', str(allowlist_temp.fileno()),
'--add-seccomp-fd', str(denylist_temp.fileno()),
'true',
],
pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
self.assertIn(
b'--add-seccomp-fd cannot be combined with --seccomp',
completed.stderr,
)
self.assertEqual(completed.returncode, 1)
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'--add-seccomp-fd', str(allowlist_temp.fileno()),
'--add-seccomp-fd', str(allowlist_temp.fileno()),
'true',
],
pass_fds=(allowlist_temp.fileno(), allowlist_temp.fileno()),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
self.assertIn(
b"bwrap: Can't read seccomp data: ",
completed.stderr,
)
self.assertEqual(completed.returncode, 1)
allowlist_temp.write(b'\x01')
allowlist_temp.seek(0, os.SEEK_SET)
completed = subprocess.run(
[
self.bwrap,
'--ro-bind', '/', '/',
'--add-seccomp-fd', str(denylist_temp.fileno()),
'--add-seccomp-fd', str(allowlist_temp.fileno()),
'true',
],
pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
self.assertIn(
b'bwrap: Invalid seccomp data, must be multiple of 8\n',
completed.stderr,
)
self.assertEqual(completed.returncode, 1)
def main():
logging.basicConfig(level=logging.DEBUG)
try:
from tap.runner import TAPTestRunner
except ImportError:
TAPTestRunner = None # type: ignore
if TAPTestRunner is not None:
runner = TAPTestRunner()
runner.set_stream(True)
unittest.main(testRunner=runner)
else:
print('# tap.runner not available, using simple TAP output')
print('1..1')
program = unittest.main(exit=False)
if program.result.wasSuccessful():
print('ok 1 - %r' % program.result)
else:
print('not ok 1 - %r' % program.result)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -xeuo pipefail
srcd=$(cd $(dirname "$0") && pwd)
. "${srcd}/libtest.sh"
echo "1..1"
# This test needs user namespaces
if test -n "${bwrap_is_suid:-}"; then
echo "ok - # SKIP no setuid support for --unshare-user"
else
mkfifo donepipe
$RUN --info-fd 42 --unshare-user --unshare-pid sh -c 'readlink /proc/self/ns/pid > sandbox-pidns; cat < donepipe' >/dev/null 42>info.json &
while ! test -f sandbox-pidns; do sleep 1; done
SANDBOX1PID=$(extract_child_pid info.json)
ASAN_OPTIONS=detect_leaks=0 LSAN_OPTIONS=detect_leaks=0 \
$RUN --userns 11 --pidns 12 readlink /proc/self/ns/pid > sandbox2-pidns 11< /proc/$SANDBOX1PID/ns/user 12< /proc/$SANDBOX1PID/ns/pid
echo foo > donepipe
assert_files_equal sandbox-pidns sandbox2-pidns
rm donepipe info.json sandbox-pidns
echo "ok - Test --pidns"
fi

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -xeuo pipefail
srcd=$(cd $(dirname "$0") && pwd)
. "${srcd}/libtest.sh"
echo "1..1"
# This test needs user namespaces
if test -n "${bwrap_is_suid:-}"; then
echo "ok - # SKIP no setuid support for --unshare-user"
else
mkfifo donepipe
$RUN --info-fd 42 --unshare-user sh -c 'readlink /proc/self/ns/user > sandbox-userns; cat < donepipe' >/dev/null 42>info.json &
while ! test -f sandbox-userns; do sleep 1; done
SANDBOX1PID=$(extract_child_pid info.json)
$RUN --userns 11 readlink /proc/self/ns/user > sandbox2-userns 11< /proc/$SANDBOX1PID/ns/user
echo foo > donepipe
assert_files_equal sandbox-userns sandbox2-userns
rm donepipe info.json sandbox-userns
echo "ok - Test --userns"
fi

View File

@@ -0,0 +1,247 @@
/*
* Copyright © 2019-2021 Collabora Ltd.
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include "utils.h"
/* A small implementation of TAP */
static unsigned int test_number = 0;
__attribute__((format(printf, 1, 2)))
static void
ok (const char *format, ...)
{
va_list ap;
printf ("ok %u - ", ++test_number);
va_start (ap, format);
vprintf (format, ap);
va_end (ap);
printf ("\n");
}
/* for simplicity we always die immediately on failure */
#define not_ok(fmt, ...) die (fmt, ## __VA_ARGS__)
/* approximately GLib-compatible helper macros */
#define g_test_message(fmt, ...) printf ("# " fmt "\n", ## __VA_ARGS__)
#define g_assert_cmpstr(left_expr, op, right_expr) \
do { \
const char *left = (left_expr); \
const char *right = (right_expr); \
if (strcmp0 (left, right) op 0) \
ok ("%s (\"%s\") %s %s (\"%s\")", #left_expr, left, #op, #right_expr, right); \
else \
not_ok ("expected %s (\"%s\") %s %s (\"%s\")", \
#left_expr, left, #op, #right_expr, right); \
} while (0)
#define g_assert_cmpint(left_expr, op, right_expr) \
do { \
intmax_t left = (left_expr); \
intmax_t right = (right_expr); \
if (left op right) \
ok ("%s (%ji) %s %s (%ji)", #left_expr, left, #op, #right_expr, right); \
else \
not_ok ("expected %s (%ji) %s %s (%ji)", \
#left_expr, left, #op, #right_expr, right); \
} while (0)
#define g_assert_cmpuint(left_expr, op, right_expr) \
do { \
uintmax_t left = (left_expr); \
uintmax_t right = (right_expr); \
if (left op right) \
ok ("%s (%ju) %s %s (%ju)", #left_expr, left, #op, #right_expr, right); \
else \
not_ok ("expected %s (%ju) %s %s (%ju)", \
#left_expr, left, #op, #right_expr, right); \
} while (0)
#define g_assert_true(expr) \
do { \
if ((expr)) \
ok ("%s", #expr); \
else \
not_ok ("expected %s to be true", #expr); \
} while (0)
#define g_assert_false(expr) \
do { \
if (!(expr)) \
ok ("!(%s)", #expr); \
else \
not_ok ("expected %s to be false", #expr); \
} while (0)
#define g_assert_null(expr) \
do { \
if ((expr) == NULL) \
ok ("%s was null", #expr); \
else \
not_ok ("expected %s to be null", #expr); \
} while (0)
#define g_assert_nonnull(expr) \
do { \
if ((expr) != NULL) \
ok ("%s wasn't null", #expr); \
else \
not_ok ("expected %s to be non-null", #expr); \
} while (0)
static int
strcmp0 (const char *left,
const char *right)
{
if (left == right)
return 0;
if (left == NULL)
return -1;
if (right == NULL)
return 1;
return strcmp (left, right);
}
static void
test_n_elements (void)
{
int three[] = { 1, 2, 3 };
g_assert_cmpuint (N_ELEMENTS (three), ==, 3);
}
static void
test_strconcat (void)
{
const char *a = "aaa";
const char *b = "bbb";
char *ab = strconcat (a, b);
g_assert_cmpstr (ab, ==, "aaabbb");
free (ab);
}
static void
test_strconcat3 (void)
{
const char *a = "aaa";
const char *b = "bbb";
const char *c = "ccc";
char *abc = strconcat3 (a, b, c);
g_assert_cmpstr (abc, ==, "aaabbbccc");
free (abc);
}
static void
test_has_prefix (void)
{
g_assert_true (has_prefix ("foo", "foo"));
g_assert_true (has_prefix ("foobar", "foo"));
g_assert_false (has_prefix ("foobar", "fool"));
g_assert_false (has_prefix ("foo", "fool"));
g_assert_true (has_prefix ("foo", ""));
g_assert_true (has_prefix ("", ""));
g_assert_false (has_prefix ("", "no"));
g_assert_false (has_prefix ("yes", "no"));
}
static void
test_has_path_prefix (void)
{
static const struct
{
const char *str;
const char *prefix;
bool expected;
} tests[] =
{
{ "/run/host/usr", "/run/host", true },
{ "/run/host/usr", "/run/host/", true },
{ "/run/host", "/run/host", true },
{ "////run///host////usr", "//run//host", true },
{ "////run///host////usr", "//run//host////", true },
{ "/run/hostage", "/run/host", false },
/* Any number of leading slashes is ignored, even zero */
{ "foo/bar", "/foo", true },
{ "/foo/bar", "foo", true },
};
size_t i;
for (i = 0; i < N_ELEMENTS (tests); i++)
{
const char *str = tests[i].str;
const char *prefix = tests[i].prefix;
bool expected = tests[i].expected;
if (expected)
g_test_message ("%s should have path prefix %s", str, prefix);
else
g_test_message ("%s should not have path prefix %s", str, prefix);
if (expected)
g_assert_true (has_path_prefix (str, prefix));
else
g_assert_false (has_path_prefix (str, prefix));
}
}
static void
test_string_builder (void)
{
StringBuilder sb = {0};
strappend (&sb, "aaa");
g_assert_cmpstr (sb.str, ==, "aaa");
strappend (&sb, "bbb");
g_assert_cmpstr (sb.str, ==, "aaabbb");
strappendf (&sb, "c%dc%s", 9, "x");
g_assert_cmpstr (sb.str, ==, "aaabbbc9cx");
strappend_escape_for_mount_options (&sb, "/path :,\\");
g_assert_cmpstr (sb.str, ==, "aaabbbc9cx/path \\:\\,\\\\");
strappend (&sb, "zzz");
g_assert_cmpstr (sb.str, ==, "aaabbbc9cx/path \\:\\,\\\\zzz");
free (sb.str);
sb = (StringBuilder){0};
strappend_escape_for_mount_options (&sb, "aaa");
g_assert_cmpstr (sb.str, ==, "aaa");
free (sb.str);
sb = (StringBuilder){0};
strappend_escape_for_mount_options (&sb, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
g_assert_cmpstr (sb.str, ==, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
free (sb.str);
}
int
main (int argc UNUSED,
char **argv UNUSED)
{
setvbuf (stdout, NULL, _IONBF, 0);
test_n_elements ();
test_strconcat ();
test_strconcat3 ();
test_has_prefix ();
test_has_path_prefix ();
test_string_builder ();
printf ("1..%u\n", test_number);
return 0;
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2021 Simon McVittie
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* Try one or more system calls that might have been blocked by a
* seccomp filter. Return the last value of errno seen.
*
* In general, we pass a bad fd or pointer to each syscall that will
* accept one, so that it will fail with EBADF or EFAULT without side-effects.
*
* This helper is used for regression tests in both bubblewrap and flatpak.
* Please keep both copies in sync.
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <sys/types.h>
#if defined(_MIPS_SIM)
# if _MIPS_SIM == _ABIO32
# define MISSING_SYSCALL_BASE 4000
# elif _MIPS_SIM == _ABI64
# define MISSING_SYSCALL_BASE 5000
# elif _MIPS_SIM == _ABIN32
# define MISSING_SYSCALL_BASE 6000
# else
# error "Unknown MIPS ABI"
# endif
#endif
#if defined(__ia64__)
# define MISSING_SYSCALL_BASE 1024
#endif
#if defined(__alpha__)
# define MISSING_SYSCALL_BASE 110
#endif
#if defined(__x86_64__) && defined(__ILP32__)
# define MISSING_SYSCALL_BASE 0x40000000
#endif
/*
* MISSING_SYSCALL_BASE:
*
* Number to add to the syscall numbers of recently-added syscalls
* to get the appropriate syscall for the current ABI.
*/
#ifndef MISSING_SYSCALL_BASE
# define MISSING_SYSCALL_BASE 0
#endif
#ifndef __NR_clone3
# define __NR_clone3 (MISSING_SYSCALL_BASE + 435)
#endif
/*
* The size of clone3's parameter (as of 2021)
*/
#define SIZEOF_STRUCT_CLONE_ARGS ((size_t) 88)
/*
* An invalid pointer that will cause syscalls to fail with EFAULT
*/
#define WRONG_POINTER ((char *) 1)
#ifndef PR_GET_CHILD_SUBREAPER
#define PR_GET_CHILD_SUBREAPER 37
#endif
int
main (int argc, char **argv)
{
int errsv = 0;
int i;
for (i = 1; i < argc; i++)
{
const char *arg = argv[i];
if (strcmp (arg, "print-errno-values") == 0)
{
printf ("EBADF=%d\n", EBADF);
printf ("EFAULT=%d\n", EFAULT);
printf ("ENOENT=%d\n", ENOENT);
printf ("ENOSYS=%d\n", ENOSYS);
printf ("EPERM=%d\n", EPERM);
}
else if (strcmp (arg, "chmod") == 0)
{
/* If not blocked by seccomp, this will fail with EFAULT */
if (chmod (WRONG_POINTER, 0700) != 0)
{
errsv = errno;
perror (arg);
}
}
else if (strcmp (arg, "chroot") == 0)
{
/* If not blocked by seccomp, this will fail with EFAULT */
if (chroot (WRONG_POINTER) != 0)
{
errsv = errno;
perror (arg);
}
}
else if (strcmp (arg, "clone3") == 0)
{
/* If not blocked by seccomp, this will fail with EFAULT */
if (syscall (__NR_clone3, WRONG_POINTER, SIZEOF_STRUCT_CLONE_ARGS) != 0)
{
errsv = errno;
perror (arg);
}
}
else if (strcmp (arg, "ioctl TIOCNOTTY") == 0)
{
/* If not blocked by seccomp, this will fail with EBADF */
if (ioctl (-1, TIOCNOTTY) != 0)
{
errsv = errno;
perror (arg);
}
}
else if (strcmp (arg, "ioctl TIOCSTI") == 0)
{
/* If not blocked by seccomp, this will fail with EBADF */
if (ioctl (-1, TIOCSTI, WRONG_POINTER) != 0)
{
errsv = errno;
perror (arg);
}
}
#ifdef __LP64__
else if (strcmp (arg, "ioctl TIOCSTI CVE-2019-10063") == 0)
{
unsigned long not_TIOCSTI = (0x123UL << 32) | (unsigned long) TIOCSTI;
/* If not blocked by seccomp, this will fail with EBADF */
if (syscall (__NR_ioctl, -1, not_TIOCSTI, WRONG_POINTER) != 0)
{
errsv = errno;
perror (arg);
}
}
#endif
else if (strcmp (arg, "listen") == 0)
{
/* If not blocked by seccomp, this will fail with EBADF */
if (listen (-1, 42) != 0)
{
errsv = errno;
perror (arg);
}
}
else if (strcmp (arg, "prctl") == 0)
{
/* If not blocked by seccomp, this will fail with EFAULT */
if (prctl (PR_GET_CHILD_SUBREAPER, WRONG_POINTER, 0, 0, 0) != 0)
{
errsv = errno;
perror (arg);
}
}
else
{
fprintf (stderr, "Unsupported syscall \"%s\"\n", arg);
errsv = ENOENT;
}
}
return errsv;
}

View File

@@ -0,0 +1,2 @@
/_build/
/subprojects/

View File

@@ -0,0 +1,3 @@
This is a simple example of a project that uses bubblewrap as a
subproject. The intention is that if this project can successfully build
bubblewrap as a subproject, then so could Flatpak.

View File

@@ -0,0 +1,26 @@
#!/usr/bin/python3
# Copyright 2022 Collabora Ltd.
# SPDX-License-Identifier: LGPL-2.0-or-later
import subprocess
import sys
if __name__ == '__main__':
completed = subprocess.run(
['objdump', '-T', '-x', sys.argv[1]],
stdout=subprocess.PIPE,
)
stdout = completed.stdout
assert stdout is not None
seen_rpath = False
for line in stdout.splitlines():
words = line.strip().split()
if words and words[0] in (b'RPATH', b'RUNPATH'):
print(line.decode(errors='backslashreplace'))
assert len(words) == 2, words
assert words[1] == b'${ORIGIN}/../lib', words
seen_rpath = True
assert seen_rpath

View File

@@ -0,0 +1 @@
#error Should not use superproject config.h to compile bubblewrap

View File

@@ -0,0 +1 @@
#error Should not use superproject generated config.h to compile bubblewrap

View File

@@ -0,0 +1,20 @@
project(
'use-bubblewrap-as-subproject',
'c',
version : '0',
meson_version : '>=0.49.0',
)
configure_file(
output : 'config.h',
input : 'dummy-config.h.in',
configuration : configuration_data(),
)
subproject(
'bubblewrap',
default_options : [
'install_rpath=${ORIGIN}/../lib',
'program_prefix=not-flatpak-',
],
)

View File

@@ -0,0 +1,136 @@
newlines lf
input_tab_size 8
output_tab_size 8
string_escape_char 92
string_escape_char2 0
# indenting
indent_columns 2
indent_with_tabs 0
indent_align_string True
indent_brace 2
indent_braces false
indent_braces_no_func True
indent_func_call_param false
indent_func_def_param false
indent_func_proto_param false
indent_switch_case 0
indent_case_brace 2
indent_paren_close 1
# spacing
sp_arith Add
sp_assign Add
sp_enum_assign Add
sp_bool Add
sp_compare Add
sp_inside_paren Remove
sp_inside_fparens Remove
sp_func_def_paren Force
sp_func_proto_paren Force
sp_paren_paren Remove
sp_balance_nested_parens False
sp_paren_brace Remove
sp_before_square Remove
sp_before_squares Remove
sp_inside_square Remove
sp_before_ptr_star Add
sp_between_ptr_star Remove
sp_after_comma Add
sp_before_comma Remove
sp_after_cast Add
sp_sizeof_paren Add
sp_not Remove
sp_inv Remove
sp_addr Remove
sp_member Remove
sp_deref Remove
sp_sign Remove
sp_incdec Remove
sp_attribute_paren remove
sp_macro Force
sp_func_call_paren Force
sp_func_call_user_paren Remove
set func_call_user _ N_ C_ g_autoptr g_auto
sp_brace_typedef add
sp_cond_colon add
sp_cond_question add
sp_defined_paren remove
# alignment
align_keep_tabs False
align_with_tabs False
align_on_tabstop False
align_number_left True
align_func_params True
align_var_def_span 0
align_var_def_amp_style 1
align_var_def_colon true
align_enum_equ_span 0
align_var_struct_span 2
align_var_def_star_style 2
align_var_def_amp_style 2
align_typedef_span 2
align_typedef_func 0
align_typedef_star_style 2
align_typedef_amp_style 2
# newlines
nl_assign_leave_one_liners True
nl_enum_leave_one_liners False
nl_func_leave_one_liners False
nl_if_leave_one_liners False
nl_end_of_file Add
nl_assign_brace Remove
nl_func_var_def_blk 1
nl_fcall_brace Add
nl_enum_brace Remove
nl_struct_brace Force
nl_union_brace Force
nl_if_brace Force
nl_brace_else Force
nl_elseif_brace Force
nl_else_brace Add
nl_for_brace Force
nl_while_brace Force
nl_do_brace Force
nl_brace_while Force
nl_switch_brace Force
nl_before_case True
nl_after_case False
nl_func_type_name Force
nl_func_proto_type_name Remove
nl_func_paren Remove
nl_func_decl_start Remove
nl_func_decl_args Force
nl_func_decl_end Remove
nl_fdef_brace Force
nl_after_return False
nl_define_macro False
nl_create_if_one_liner False
nl_create_for_one_liner False
nl_create_while_one_liner False
nl_after_semicolon True
nl_multi_line_cond true
# mod
# I'd like these to be remove, but that removes brackets in if { if { foo } }, which i dislike
# Not clear what to do about that...
mod_full_brace_for Remove
mod_full_brace_if Remove
mod_full_brace_if_chain True
mod_full_brace_while Remove
mod_full_brace_do Remove
mod_full_brace_nl 3
mod_paren_on_return Remove
# line splitting
#code_width = 78
ls_for_split_full True
ls_func_split_full True
# positioning
pos_bool Trail
pos_conditional Trail

2
codex-rs/vendor/bubblewrap/uncrustify.sh vendored Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
uncrustify -c uncrustify.cfg --no-backup `git ls-tree --name-only -r HEAD | grep \\\.[ch]$`

1080
codex-rs/vendor/bubblewrap/utils.c vendored Normal file

File diff suppressed because it is too large Load Diff

217
codex-rs/vendor/bubblewrap/utils.h vendored Normal file
View File

@@ -0,0 +1,217 @@
/* bubblewrap
* Copyright (C) 2016 Alexander Larsson
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#if 0
#define debug(...) bwrap_log (LOG_DEBUG, __VA_ARGS__)
#else
#define debug(...)
#endif
#define UNUSED __attribute__((__unused__))
#define N_ELEMENTS(arr) (sizeof (arr) / sizeof ((arr)[0]))
#ifndef TEMP_FAILURE_RETRY
#define TEMP_FAILURE_RETRY(expression) \
(__extension__ \
({ long int __result; \
do __result = (long int) (expression); \
while (__result == -1L && errno == EINTR); \
__result; }))
#endif
#define PIPE_READ_END 0
#define PIPE_WRITE_END 1
#ifndef PR_SET_CHILD_SUBREAPER
#define PR_SET_CHILD_SUBREAPER 36
#endif
extern bool bwrap_level_prefix;
void bwrap_log (int severity,
const char *format,
...) __attribute__((format (printf, 2, 3)));
#define warn(...) bwrap_log (LOG_WARNING, __VA_ARGS__)
void die_with_error (const char *format,
...) __attribute__((__noreturn__)) __attribute__((format (printf, 1, 2)));
void die_with_mount_error (const char *format,
...) __attribute__((__noreturn__)) __attribute__((format (printf, 1, 2)));
void die (const char *format,
...) __attribute__((__noreturn__)) __attribute__((format (printf, 1, 2)));
void die_oom (void) __attribute__((__noreturn__));
void die_unless_label_valid (const char *label);
void fork_intermediate_child (void);
void *xmalloc (size_t size);
void *xcalloc (size_t nmemb, size_t size);
void *xrealloc (void *ptr,
size_t size);
char *xstrdup (const char *str);
void strfreev (char **str_array);
void xclearenv (void);
void xsetenv (const char *name,
const char *value,
int overwrite);
void xunsetenv (const char *name);
char *strconcat (const char *s1,
const char *s2);
char *strconcat3 (const char *s1,
const char *s2,
const char *s3);
char * xasprintf (const char *format,
...) __attribute__((format (printf, 1, 2)));
bool has_prefix (const char *str,
const char *prefix);
bool has_path_prefix (const char *str,
const char *prefix);
bool path_equal (const char *path1,
const char *path2);
int fdwalk (int proc_fd,
int (*cb)(void *data,
int fd),
void *data);
char *load_file_data (int fd,
size_t *size);
char *load_file_at (int dirfd,
const char *path);
int write_file_at (int dirfd,
const char *path,
const char *content);
int write_to_fd (int fd,
const char *content,
ssize_t len);
int copy_file_data (int sfd,
int dfd);
int copy_file (const char *src_path,
const char *dst_path,
mode_t mode);
int create_file (const char *path,
mode_t mode,
const char *content);
int ensure_file (const char *path,
mode_t mode);
int ensure_dir (const char *path,
mode_t mode);
int get_file_mode (const char *pathname);
int mkdir_with_parents (const char *pathname,
mode_t mode,
bool create_last);
void create_pid_socketpair (int sockets[2]);
void send_pid_on_socket (int socket);
int read_pid_from_socket (int socket);
char *get_oldroot_path (const char *path);
char *get_newroot_path (const char *path);
char *readlink_malloc (const char *pathname);
/* syscall wrappers */
int raw_clone (unsigned long flags,
void *child_stack);
int pivot_root (const char *new_root,
const char *put_old);
char *label_mount (const char *opt,
const char *mount_label);
int label_exec (const char *exec_label);
int label_create_file (const char *file_label);
const char *mount_strerror (int errsv);
static inline void
cleanup_freep (void *p)
{
void **pp = (void **) p;
if (*pp)
free (*pp);
}
static inline void
cleanup_strvp (void *p)
{
void **pp = (void **) p;
strfreev (*pp);
}
static inline void
cleanup_fdp (int *fdp)
{
int fd;
assert (fdp);
fd = *fdp;
if (fd != -1)
(void) close (fd);
}
#define cleanup_free __attribute__((cleanup (cleanup_freep)))
#define cleanup_fd __attribute__((cleanup (cleanup_fdp)))
#define cleanup_strv __attribute__((cleanup (cleanup_strvp)))
static inline void *
steal_pointer (void *pp)
{
void **ptr = (void **) pp;
void *ref;
ref = *ptr;
*ptr = NULL;
return ref;
}
/* type safety */
#define steal_pointer(pp) \
(0 ? (*(pp)) : (steal_pointer) (pp))
typedef struct _StringBuilder StringBuilder;
struct _StringBuilder
{
char * str;
size_t size;
size_t offset;
};
void strappend (StringBuilder *dest,
const char *src);
void strappendf (StringBuilder *dest,
const char *fmt,
...);
void strappend_escape_for_mount_options (StringBuilder *dest,
const char *src);