Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/user-guide/en/runtime/blaze.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,54 @@ Leave `listen.http_addr` disabled in production until
Daemon shutdown also does not yet wait for every active HTTP handler or release
all runtime owners, so an in-flight request may observe a closed connection.

## File Storage Compatibility and Safety Checks

This release does not change the storage configuration fields, HTTP API, or
successful sandbox lifecycle. It tightens failure handling for unsafe file
storage layouts and ownership changes.

The file provider rejects `storage.instances_dir` when its path contains `..`
or a symbolic-link component; when `storage.images_dir` and
`storage.instances_dir` resolve to overlapping filesystem locations, including
through a symbolic link or bind mount; when the instances root would own
`daemon.state_dir` or enter one of its sandbox UUID subtrees, directly or
through an alias; or when another daemon already owns the same instances root.
Resolve these errors by choosing distinct, stable directories; do not work
around them with path aliases.

Blaze still creates a missing `storage.instances_dir` on first start. Every
directory into which Blaze publishes a missing component or sandbox slot, and
the final instances root, must be owned by root or the daemon's effective user
and must not be writable by group or other users. A shared writable ancestor
in the already-existing path is accepted only when it is sticky and the next
component is owned by root or the daemon's effective user. A sticky publication
parent is still rejected because another user could reserve a not-yet-published
name. These checks preserve normal first-start behavior while rejecting layouts
where an untrusted user could exchange or reserve a directory. Root and
processes running as the same effective user as the daemon remain within the
host administration trust boundary. Stop Blaze before an administrator changes
its storage paths.

Missing path components and sandbox slots are published without replacing an
existing name. If the target name is occupied at atomic publication, Blaze
rejects the operation and leaves that directory unchanged.
If a published sandbox slot changes identity before allocation completes,
creation enters recovery handling and automatic cleanup by the stable sandbox
identifier is suppressed for that daemon process so that the replacement is
not removed. This safeguard does not claim to isolate Blaze from concurrent
changes made by root or the daemon's own effective user across a process restart.
Inspect or restore the storage path before restarting after such an intervention.

Blaze retains ownership of the instances root that it opened at startup. If
the configured pathname is later renamed or replaced, operations that must
create or reconstruct backend-visible paths fail closed instead of using the
replacement. This includes the reconstruction step that normally starts a
periodic synchronization attempt. Release always uses the original opened
root, and a synchronization attempt that completed reconstruction before the
replacement also finishes against that root. The replacement directory is not
modified accidentally. Restore the configured pathname to the original
directory before requesting new allocation, reconstruction, or synchronization.

## Storage Artifact Synchronization

Blaze can periodically persist the already-written host artifacts and directory
Expand Down
33 changes: 33 additions & 0 deletions docs/user-guide/zh/runtime/blaze.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,39 @@ read 响应过大时返回 HTTP 502 和
保持 `listen.http_addr` 关闭。Daemon 停止时也不会等待全部 HTTP handler 或
释放所有 runtime owner,因此正在执行的请求可能看到连接关闭。

## 文件存储兼容性与安全检查

本次变更不修改存储配置字段、HTTP API 或正常的沙箱生命周期,只收紧不安全
文件存储布局和所有权变化时的失败处理。

以下情况会导致文件存储提供程序拒绝 `storage.instances_dir`:路径包含 `..` 或
符号链接组件;`storage.images_dir` 和 `storage.instances_dir` 直接或通过符号链接、
绑定挂载解析到重叠的文件系统位置;实例根会覆盖 `daemon.state_dir` 或进入其
沙箱 UUID 子树(包括通过别名重叠);同一实例根已经由另一个守护进程占用。
遇到这些错误时,应选择相互独立且稳定的目录,不要使用路径别名绕过检查。

Blaze 首次启动时仍会自动创建缺失的 `storage.instances_dir`。用于发布缺失组件或
沙箱存储槽的每个目录,以及最终实例根,必须由超级用户或守护进程的有效用户所有,
并且不得允许组用户或其他用户写入。已有路径中的共享可写祖先只有在设置粘滞位、且
下一级组件由超级用户或守护进程的有效用户所有时才会被接受。用于发布新名称的父目录
即使设置了粘滞位也会被拒绝,因为其他用户仍可能抢占尚未发布的名称。这些检查保留
正常的首次启动行为,同时拒绝不受信任用户能够在发布期间交换或抢占目录的布局。
超级用户和与守护进程使用相同有效用户的进程仍属于主机管理信任边界。管理员更改
存储路径前应停止 Blaze。

缺失路径组件和沙箱存储槽会以不覆盖已有名称的方式发布。如果原子发布时目标名称
已被占用,Blaze 会拒绝操作并保持该目录不变。如果已发布的沙箱存储槽在
分配完成前改变身份,创建会进入恢复处理;在当前守护进程存活期间,系统会禁止按
稳定沙箱标识自动清理存储,避免删除替换目录。该保护不承诺隔离超级用户或守护进程
自身有效用户跨进程重启实施的并发路径更改。发生此类管理操作后,重启前必须检查或
恢复存储路径。

Blaze 会保留启动时打开的实例根所有权。如果配置路径随后被重命名或替换,必须创建
或重建后端可见路径的操作会失败关闭,而不会使用替换目录;周期同步通常也会在开始时
执行这一步重建。释放操作始终使用原先打开的存储根;如果同步操作在路径被替换前已经
完成重建,也会在该存储根上完成,因此不会意外修改替换目录。再次请求新的存储分配、
重建或同步前,应将配置路径恢复为原目录。

## 存储制品同步

Blaze 可以定期持久化 running sandbox 中已经写入的宿主机制品和目录元数据。
Expand Down
22 changes: 22 additions & 0 deletions src/blaze/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,28 @@ sync_timeout = "30s" # Maximum scheduler wait for reconstruction plus arti
```

The `file` provider uses standard filesystem operations for sandbox storage. The `auto` provider probes available backends in priority order (currently equivalent to `file`). Unrecognized values will log a warning and fall back to `file`.
Existing storage configuration fields, HTTP APIs, and successful lifecycle
behavior are unchanged. The file provider now rejects an instances-root path
that contains `..` or a symbolic-link component; storage roots that resolve to
overlapping locations; an instances root that would own the daemon state
directory or enter a sandbox UUID subtree, directly or through an alias; or a
root already owned by another daemon. First-start creation remains supported,
but every publication parent and the resulting instances root must be owned by
root or the daemon's effective user and must not be writable by group or other
users. A shared writable ancestor in an existing path is accepted only when it
is sticky and the next component is owned by root or the daemon's effective
user. If the target name is occupied at atomic publication, or if a published
slot is replaced before allocation completes, the operation fails without
adopting or removing the replacement during that daemon process. This cleanup
suppression does not survive a daemon restart; inspect or restore the path
before restarting. Root and processes running as the daemon's effective user
remain within the host administration trust boundary; stop Blaze before
changing its storage paths.
If the configured pathname is replaced while Blaze is running, new allocation
and reconstruction fail closed. Release, and any synchronization attempt that
already completed reconstruction, continue against the original storage root.
See [File storage compatibility and safety checks](../../docs/user-guide/en/runtime/blaze.md#file-storage-compatibility-and-safety-checks)
for the complete behavior.
When periodic synchronization is enabled, a completed provider failure is
isolated from later sandboxes. If a provider cannot stop its filesystem work at
the deadline, that work keeps the sandbox operation lock and the single
Expand Down
13 changes: 13 additions & 0 deletions src/blaze/README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ sync_timeout = "30s" # scheduler 等待 slot 重建与制品同步的最
```

`file` provider 使用标准文件系统操作管理 sandbox 存储。`auto` 按优先级探测可用 provider(当前等同于 `file`)。无法识别的值将记录告警并回退到 `file`。
现有存储配置字段、HTTP API 和正常生命周期行为保持不变。文件存储提供程序现在会
拒绝包含 `..` 或符号链接组件的实例根路径、解析后位置重叠的存储根、会覆盖守护进程
状态目录或进入沙箱 UUID 子树的实例根(包括通过别名重叠),以及已经由另一个
守护进程占用的实例根。首次启动时仍会自动创建缺失的实例根,但用于发布目录的每个
父目录及最终实例根必须由超级用户或守护进程的有效用户所有,并且不得允许组用户或
其他用户写入。已有路径中的共享可写祖先只有在设置粘滞位、且下一级组件由超级用户或
守护进程的有效用户所有时才会被接受。如果原子发布时目标名称已被占用,或者已发布的
存储槽在分配完成前被替换,操作会失败;当前守护进程不会采用或删除替换目录。该清理
禁令不会跨守护进程重启保留,重启前必须检查或恢复路径。超级用户和与守护进程使用
相同有效用户的进程属于主机管理信任边界;更改存储路径前应停止 Blaze。
如果 Blaze 运行期间配置路径被替换,新的存储分配和重建会失败关闭。释放操作以及
已经完成重建的同步操作仍作用于启动时打开的原存储根。
完整行为见[文件存储兼容性与安全检查](../../docs/user-guide/zh/runtime/blaze.md#文件存储兼容性与安全检查)。
启用周期同步后,已经返回的 provider 失败不会中断后续 sandbox。如果 provider
在 deadline 到达时仍无法停止文件系统操作,该操作会继续持有 sandbox operation
lock 和唯一的同步许可直至完成;后续同步会被推迟而不会不断累积。service loop
Expand Down
31 changes: 31 additions & 0 deletions src/blaze/crates/blaze-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ impl DaemonConfig {
/// Validate cross-field invariants that serde cannot express.
pub fn validate(&self) -> Result<()> {
validate_storage_paths(&self.storage.images_dir, &self.storage.instances_dir)?;
validate_state_boundary(
&self.storage.instances_dir,
"storage.instances_dir",
&self.daemon.state_dir,
)?;
self.storage.sync_schedule()?;
self.storage.sync_timeout_duration()?;
let template_boundaries = [
Expand Down Expand Up @@ -805,4 +810,30 @@ mod tests {
total.template.max_total_bytes = 0;
assert!(total.validate().is_err());
}

#[test]
fn rejects_instances_root_inside_daemon_lifecycle_state() {
let state = PathBuf::from("/srv/blaze-state");
for instances in [
state.clone(),
state
.join("86b59faf-3b91-46e4-9db0-2468b8336eb6")
.join("slot"),
] {
let mut config = DaemonConfig::default();
config.daemon.state_dir = state.clone();
config.storage.images_dir = PathBuf::from("/srv/blaze-images");
config.storage.instances_dir = instances;
config.template.dir = PathBuf::from("/srv/blaze-templates");
config.template.import_root = Some(PathBuf::from("/srv/blaze-imports"));
config.policy.dir = PathBuf::from("/srv/blaze-policies");
config.daemon.socket = PathBuf::from("/srv/blaze-run/api.sock");

let error = config
.validate()
.expect_err("instances root must not enter lifecycle state");

assert!(error.to_string().contains("storage.instances_dir"));
}
}
}
49 changes: 38 additions & 11 deletions src/blaze/crates/blaze-core/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,39 +53,66 @@ pub struct AcquireOpts {
pub mem_size: u64,
}

/// Storage allocation failure with an optional residual slot owner.
/// Cleanup responsibility after a storage allocation failure.
#[derive(Debug)]
pub enum StorageAcquireDisposition {
/// The provider proved that the failed request left no resources.
Clean,
/// The caller owns a provider object that can be retried by stable slot ID.
Residual(StorageSlot),
/// Stable-name cleanup is unsafe and requires manual inspection.
ManualCleanupRequired,
}

/// Storage allocation failure with an explicit cleanup disposition.
///
/// A provider returns `residual` only when rollback could not remove resources
/// that were created for this request. The caller must retain the stable slot
/// ID until a later release succeeds.
/// A provider returns `Residual` when rollback could not remove resources that
/// were created for this request. `ManualCleanupRequired` means that the stable
/// slot name cannot safely identify the provider object.
#[derive(Debug, Error)]
#[error("{source}")]
pub struct StorageAcquireError {
#[source]
source: BlazeError,
residual: Option<StorageSlot>,
disposition: StorageAcquireDisposition,
}

impl StorageAcquireError {
/// Build a failure after the provider confirmed that no resources remain.
pub fn clean(source: BlazeError) -> Self {
Self {
source,
residual: None,
disposition: StorageAcquireDisposition::Clean,
}
}

/// Build a failure that transfers residual slot ownership to the caller.
/// Build a failure that transfers possible residual ownership to the caller.
pub fn with_residual(source: BlazeError, residual: StorageSlot) -> Self {
Comment thread
WeissonHan marked this conversation as resolved.
Self {
source,
residual: Some(residual),
disposition: StorageAcquireDisposition::Residual(residual),
}
}

/// Split the original provider error from any residual slot owner.
pub fn into_parts(self) -> (BlazeError, Option<StorageSlot>) {
(self.source, self.residual)
/// Build a failure that cannot be cleaned up safely by stable slot name.
pub fn with_manual_cleanup_required(source: BlazeError) -> Self {
Self {
source,
disposition: StorageAcquireDisposition::ManualCleanupRequired,
}
}

/// Whether automatic cleanup by stable slot name must be suppressed.
pub fn requires_manual_cleanup(&self) -> bool {
matches!(
self.disposition,
StorageAcquireDisposition::ManualCleanupRequired
)
}

/// Split the provider error from the required cleanup action.
pub fn into_parts(self) -> (BlazeError, StorageAcquireDisposition) {
(self.source, self.disposition)
}
}

Expand Down
Loading
Loading