feat(sandbox): prepare protected single-node factory delegation

This commit is contained in:
2026-10-13 02:10:11 +08:00
parent 033d31d420
commit 3a9d5e47d4
6 changed files with 382 additions and 0 deletions
+26
View File
@@ -19,6 +19,7 @@ proxy = importlib.import_module("scripts.release.proxy")
services = importlib.import_module("scripts.release.services")
service_bootstrap = importlib.import_module("scripts.release.service_bootstrap")
service_install = importlib.import_module("scripts.release.service_install")
factory_service = importlib.import_module("scripts.release.factory_service")
def test_delivery_authorities_and_layout():
@@ -492,3 +493,28 @@ def test_unit_snapshot_binding_and_inventory_cannot_add_a_privileged_unit():
def test_unit_snapshot_does_not_adopt_foreign_or_ambiguous_targets(target):
with pytest.raises(policy.DeliveryError, match="unknown system unit target"):
service_install.previous_batch(target, "kf-api.service")
def test_factory_composition_keeps_privileged_boundary_and_finite_lifetime():
source, checksum = "123456789abc" + "d" * 28, "f" * 64
hashes = {name: "b" * 64 for name in configuration.CONFIG_FILES}
receipt = {
"schema_version": 1,
"source_sha": source,
"release_id": "sd-123456789abc",
"configuration_id": configuration.configuration_id(source, checksum, hashes),
"manifest_sha256": checksum,
"configuration_files": hashes,
"operator_attested_measurement": False,
"installed": False,
"execution_verified": False,
}
contents = factory_service.render(receipt).decode()
assert " prepare-factory --config " in contents
assert " --lifetime-seconds 3600 --max-requests 4096" in contents
assert "KillMode=mixed" in contents and "Restart=no" in contents
assert "NoNewPrivileges" not in contents and "SystemCallFilter" not in contents
assert "PrivatePIDs" not in contents and "ProtectControlGroups" not in contents
assert "EnvironmentFile" not in contents and "LoadCredential" not in contents
assert "env -i" in contents and "MemorySwapMax=0" in contents
assert receipt["configuration_id"] in contents
@@ -0,0 +1,205 @@
//! Explicit fixed-node delegation preparation, separate from execution permits.
//! Only Sandbox owns these privileged kernel operations.
use crate::{cgroup, execution, factory};
use rustix::fs::{FlockOperation, Mode};
use serde::{Deserialize, Serialize};
use std::io::{self, Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
const NODE: &str = "/sys/fs/cgroup/kfonlinejudge.slice";
const CHILD: &str = "kf-judge-tasks";
const FACTORY: &str = "/sys/fs/cgroup/kfonlinejudge.slice/kf-sandbox-factory.service";
#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
struct DelegationRecord {
schema_version: u8,
boot_id: String,
device: u64,
inode: u64,
}
fn denied() -> io::Error {
io::Error::new(
io::ErrorKind::PermissionDenied,
"factory preparation refused",
)
}
fn empty(path: &Path) -> io::Result<()> {
match std::fs::symlink_metadata(path) {
Ok(_) => {
crate::isolation::root::open_directory(path)?;
if std::fs::read_dir(path)?.next().transpose()?.is_some() {
return Err(denied());
}
Ok(())
}
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}
pub fn prepare(config_path: &Path) -> io::Result<serde_json::Value> {
factory::clean_environment()?;
let config = execution::load_config(config_path)?;
execution::authorize_registry(&config)?;
let policy = config.config();
if policy.state_root != "/var/lib/kf-judge"
|| policy.tasks_root != "/var/lib/kf-judge/tasks"
|| policy.delegated_cgroup_root != format!("{NODE}/{CHILD}")
|| policy.identities.worker_uid != 1202
|| policy.identities.worker_gid != 1202
|| policy.identities.first_task_uid != 100000
|| policy.identities.first_task_gid != 100000
|| policy.identities.count != 1
|| policy.max_concurrent_runs != 1
{
return Err(denied());
}
let own = std::fs::read_to_string("/proc/self/cgroup")?;
if own.trim() != format!("0::{}", FACTORY.trim_start_matches("/sys/fs/cgroup")) {
return Err(denied());
}
let supervisor = crate::isolation::root::open_directory(Path::new(FACTORY))?;
if cgroup::read(&supervisor, "cgroup.procs")?.trim() != std::process::id().to_string() {
return Err(denied());
}
// A helper will later apply its own required flags. Inherited filtering or
// no-new-privileges is not silently treated as a working execution gate.
let status = std::fs::read_to_string("/proc/self/status")?;
if !status.lines().any(|s| {
s.strip_prefix("NoNewPrivs:")
.is_some_and(|v| v.trim() == "0")
}) {
return Err(denied());
}
let node = crate::isolation::root::open_directory(Path::new(NODE))?;
if rustix::fs::fstatfs(&node)?.f_type as u64 != 0x6367_7270
|| cgroup::read(&node, "cgroup.type")?.trim() != "domain"
|| !cgroup::read(&node, "cgroup.procs")?.trim().is_empty()
{
return Err(denied());
}
for (name, expected) in [
("memory.max", "4026531840"),
("memory.swap.max", "0"),
("pids.max", "384"),
("cpu.max", "200000 100000"),
] {
if cgroup::read(&node, name)?.trim() != expected {
return Err(denied());
}
}
let controllers = cgroup::read(&node, "cgroup.subtree_control")?;
if !["cpu", "memory", "pids"]
.iter()
.all(|c| controllers.split_whitespace().any(|v| v == *c))
{
return Err(denied());
}
for name in ["tasks", "identities", "captures"] {
empty(&Path::new(&policy.state_root).join(name))?;
}
let root = Path::new(&policy.state_root).join("factory");
if !root.try_exists()? {
std::fs::create_dir(&root)?;
std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o711))?;
}
let root_fd = crate::isolation::root::open_directory(&root)?;
let lock = factory::private_file(&root.join("lock"))?;
rustix::fs::flock(&lock, FlockOperation::NonBlockingLockExclusive)?;
match std::fs::symlink_metadata(root.join("control.sock")) {
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
Ok(_) => return Err(denied()),
}
for name in ["tasks", "identities", "captures"] {
empty(&Path::new(&policy.state_root).join(name))?;
}
let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")?
.trim()
.to_owned();
if boot.len() != 36 || !boot.bytes().all(|c| c.is_ascii_hexdigit() || c == b'-') {
return Err(denied());
}
let record_path = root.join("delegation.json");
let old = if record_path.try_exists()? {
let mut file = factory::private_file(&record_path)?;
if file.metadata()?.len() > 1024 {
return Err(denied());
}
let mut bytes = Vec::new();
(&mut file).take(1025).read_to_end(&mut bytes)?;
let record: DelegationRecord = serde_json::from_slice(&bytes).map_err(|_| denied())?;
if record.schema_version != 1 || record.boot_id.len() != 36 {
return Err(denied());
}
Some(record)
} else {
if std::fs::symlink_metadata(&record_path).is_ok() {
return Err(denied());
}
None
};
let delegated = Path::new(&policy.delegated_cgroup_root);
let existed = delegated.try_exists()?;
if existed {
let fd = crate::isolation::root::open_directory(delegated)?;
let st = rustix::fs::fstat(&fd)?;
if old.as_ref()
!= Some(&DelegationRecord {
schema_version: 1,
boot_id: boot.clone(),
device: st.st_dev,
inode: st.st_ino,
})
|| !cgroup::read(&fd, "cgroup.procs")?.trim().is_empty()
{
return Err(denied());
}
if std::fs::read_dir(delegated)?.any(|entry| {
entry.map_or(true, |e| {
e.file_type().map_or(true, |t| t.is_dir() || t.is_symlink())
})
}) {
return Err(denied());
}
} else {
if std::fs::symlink_metadata(delegated).is_ok()
|| old.as_ref().is_some_and(|record| record.boot_id == boot)
{
return Err(denied());
}
// A new boot may recreate an empty delegation only after the persistent
// task/lease/capture checks above. Unexpected same-boot loss is refused.
rustix::fs::mkdirat(&node, CHILD, Mode::from_raw_mode(0o700))?;
}
// Existing descriptor-based checks apply/read back aggregate ceilings,
// no swap, controller availability and bounded depth/descendant counts.
let _group = cgroup::DelegatedCgroup::open(&config).map_err(|_| denied())?;
let fd = crate::isolation::root::open_directory(delegated)?;
let st = rustix::fs::fstat(&fd)?;
let record = DelegationRecord {
schema_version: 1,
boot_id: boot,
device: st.st_dev,
inode: st.st_ino,
};
let bytes = serde_json::to_vec(&record)?;
let mut stored = factory::private_file(&record_path)?;
stored.set_len(0)?;
stored.write_all(&bytes)?;
stored.sync_all()?;
rustix::fs::fsync(&root_fd)?;
let actual = ["memory.max", "memory.swap.max", "pids.max", "cpu.max"]
.into_iter()
.map(|name| Ok((name, cgroup::read(&fd, name)?.trim().to_owned())))
.collect::<io::Result<std::collections::BTreeMap<_, _>>>()?;
Ok(serde_json::json!({
"delegation_prepared": true, "existing_owned_delegation": existed,
"actual_limits": actual, "services_started": false, "isolation_verified": false,
}))
}
+2
View File
@@ -41,3 +41,5 @@ pub mod rpc;
pub mod factory;
#[cfg(target_os = "linux")]
pub mod factory_cycles;
#[cfg(target_os = "linux")]
pub mod factory_preparation;
+20
View File
@@ -63,6 +63,11 @@ enum Command {
#[arg(long)]
cycle_id: String,
},
/// Root fixed-node preparation; never grants an execution permit.
PrepareFactory {
#[arg(long)]
config: PathBuf,
},
#[cfg(target_os = "linux")]
#[command(hide = true)]
FactoryChild {
@@ -202,6 +207,21 @@ fn main() -> ExitCode {
spec_error("unsupported_platform", None)
}
}
Command::PrepareFactory { config } => {
#[cfg(target_os = "linux")]
match kf_sandbox::factory_preparation::prepare(&config) {
Ok(report) => {
println!("{report}");
ExitCode::SUCCESS
}
Err(_) => spec_error("factory_preparation_refused", None),
}
#[cfg(not(target_os = "linux"))]
{
let _ = config;
spec_error("unsupported_platform", None)
}
}
#[cfg(target_os = "linux")]
Command::FactoryChild { config, run_id } => {
// stdout is the root factory lifeline, never a log stream.
+14
View File
@@ -151,3 +151,17 @@ may leave a mixed set of pointers, but each pre-start check rejects an unselecte
binding. Repeat installation can finish the same verified batch after stopping
all app units. Loaded/inactive unit readback starts no service and makes no
claim about effective runtime limits, readiness, Sandbox gates or VM acceptance.
`python3.13 -m scripts.release.factory_service --output DIRECTORY` composes the
separate Root factory unit only after selection and protected configuration checks.
It requires matching composition code in that delivery, uses the fixed Root
binary and empty business environment, and preserves the supervisor reservations
and finite lifetime. It does not inherit the ordinary service namespace/filter
flags. Its pre-start `kf-sandbox prepare-factory` performs privileged cgroup
preparation inside Sandbox: the exact node/UID pool, single supervisor process,
empty task/lease/capture resources and factory lock are required. A retained
boot/inode record recognizes only its own empty delegation; an unknown group,
same-boot loss or conflicting record refuses. New-boot recreation requires empty
persistent resources. No journal/lease is deleted and preparation grants no
execution permit. Neither composition nor lexical tests are Root/kernel/service
acceptance; install, restart, failure recovery and actual execution remain VM-only.
+115
View File
@@ -0,0 +1,115 @@
"""Root factory unit composition, separate from ordinary service hardening."""
import argparse
import json
import os
from pathlib import Path
from . import configuration, install, preflight, service_install, services
from .policy import DeliveryError, canonical, digest
def render(receipt: dict) -> bytes:
receipt = configuration.validate_receipt(receipt, installed=False)
release, batch = receipt["release_id"], receipt["configuration_id"]
payload = install.RELEASES / release / "payload"
binary = payload / "bin/kf-sandbox"
runtime = configuration.installed_path(release, batch, "runtime.json")
clean = "/usr/bin/env -i PATH=/usr/bin:/bin LC_ALL=C "
values = [
"[Unit]",
"Description=KF bounded Root helper factory",
"After=local-fs.target",
"[Service]",
"Type=exec",
"User=root",
"Group=root",
"Slice=kfonlinejudge.slice",
"WorkingDirectory=" + str(payload),
"UMask=0077",
"ExecStartPre="
+ clean
+ services.python_environment(payload)
+ f" {payload}/python/bin/python3.13 -s -P -m scripts.release.services check-binding"
+ f" --release-id {release} --configuration-id {batch}",
"ExecStartPre=" + clean + str(binary) + " prepare-factory --config " + str(runtime),
"ExecStart="
+ clean
+ str(binary)
+ " factory --config "
+ str(runtime)
+ " --lifetime-seconds 3600 --max-requests 4096",
"MemoryMax=256M",
"MemorySwapMax=0",
"TasksMax=32",
"CPUQuota=50%",
"LimitCORE=0",
"LimitFSIZE=64M",
"LimitNOFILE=256",
"LimitSTACK=16M",
"TimeoutStartSec=1800",
"RuntimeMaxSec=3700",
"TimeoutStopSec=30",
"KillMode=mixed",
"Restart=no",
"StandardOutput=journal",
"StandardError=journal",
"LogRateLimitIntervalSec=30s",
"LogRateLimitBurst=100",
"[Install]",
"WantedBy=multi-user.target",
]
return ("\n".join(values) + "\n").encode()
def generate(output: Path) -> dict:
if os.getuid() != 0 or os.geteuid() != 0:
raise DeliveryError("Root factory composition requires trusted operator")
selected, original = service_install.current_receipt()
payload = install.RELEASES / selected["release_id"] / "payload"
for name in ("factory_service.py", "services.py"):
installed = payload / "delivery/scripts/release" / name
preflight.protected(installed)
if digest(installed) != digest(Path(__file__).parent / name):
raise DeliveryError("factory composition differs from selected delivery")
preflight.protected(payload / "bin/kf-sandbox")
if output.exists() or output.is_symlink():
raise DeliveryError("factory unit output must be new")
preflight.protected(output.parent, directory=True)
output.mkdir(mode=0o700)
unit = output / "kf-sandbox-factory.service"
unit.write_bytes(render(original))
unit.chmod(0o600)
result = {
"schema_version": 1,
"source_sha": original["source_sha"],
"configuration_id": selected["configuration_id"],
"configuration_receipt_sha256": selected["receipt_sha256"],
"unit_sha256": digest(unit),
"installed": False,
"services_started": False,
"execution_verified": False,
}
receipt = output / "receipt.json"
receipt.write_bytes(canonical(result))
receipt.chmod(0o600)
return result
def main() -> None:
parser = argparse.ArgumentParser(description="Protected Root factory unit composition")
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
try:
print(json.dumps(generate(args.output), sort_keys=True))
except (OSError, ValueError) as error:
parser.exit(
1,
"Factory composition refused: "
+ (str(error) if isinstance(error, DeliveryError) else type(error).__name__)
+ "\n",
)
if __name__ == "__main__":
main()