Skip to content

Commit b657585

Browse files
committed
fix(volume): address CLI and verity review findings
1 parent 895e600 commit b657585

8 files changed

Lines changed: 74 additions & 63 deletions

File tree

dstack/Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dstack/crates/dstack-cli-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ license.workspace = true
1212
anyhow.workspace = true
1313
http-client = { workspace = true, features = ["prpc"] }
1414
dstack-vmm-rpc.workspace = true
15+
dstack-types.workspace = true
1516
serde_json.workspace = true
1617
# advisory file locking (flock) for the allowlist/state read-modify-write;
1718
# already in the dependency tree transitively, so no extra compile cost.

dstack/crates/dstack-cli-core/src/compose.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ use serde_json::json;
1313
/// `kms_enabled` selects KMS mode (deterministic, upgradeable per-app keys);
1414
/// gateway and local-key-provider are off for the direct-port single-node flow.
1515
///
16-
/// `verity_volumes` is a list of `(source, verity_root, target)` tuples. Each becomes a
17-
/// measured `verity_volumes` entry, so the CVM only seeds content matching the
18-
/// attested root. Empty for a normal deploy.
16+
/// Each `verity_volumes` entry is measured, so the CVM only mounts content
17+
/// matching the attested root. Empty for a normal deploy.
1918
pub fn build_app_compose(
2019
name: &str,
2120
docker_compose_yaml: &str,
2221
kms_enabled: bool,
23-
verity_volumes: &[(String, String, String)],
22+
verity_volumes: &[dstack_types::VerityVolume],
2423
) -> String {
2524
let mut manifest = json!({
2625
"manifest_version": 2,
@@ -41,10 +40,7 @@ pub fn build_app_compose(
4140
"secure_time": false,
4241
});
4342
if !verity_volumes.is_empty() {
44-
manifest["verity_volumes"] = json!(verity_volumes
45-
.iter()
46-
.map(|(source, root, target)| json!({ "source": source, "verity_root": root, "target": target }))
47-
.collect::<Vec<_>>());
43+
manifest["verity_volumes"] = json!(verity_volumes);
4844
}
4945
// pretty-print via Value's Display (`{:#}`) — infallible, and byte-identical
5046
// to serde_json::to_string_pretty (avoids an expect on an unfailable Result).

dstack/crates/dstack-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ anyhow.workspace = true
1919
clap.workspace = true
2020
dstack-cli-core.workspace = true
2121
dstack-volume.workspace = true
22+
dstack-types.workspace = true
2223
fs-err.workspace = true
24+
hex.workspace = true
2325
serde_json.workspace = true
2426
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
2527
tracing.workspace = true

dstack/crates/dstack-cli/src/main.rs

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//! `logs`, a global `-j/--json`).
1212
1313
use anyhow::{bail, Context, Result};
14-
use clap::{Parser, Subcommand};
14+
use clap::{Parser, Subcommand, ValueEnum};
1515
use dstack_cli_core::layout::InstallLayout;
1616
use dstack_cli_core::vmm::{Vmm, DEFAULT_HOST};
1717
use dstack_cli_core::{compose, ports, rpc};
@@ -77,7 +77,7 @@ enum Command {
7777
ports: Vec<String>,
7878
/// attach a verity volume, as printed by `dstack verity`: the file name
7979
/// in the vmm's volumes_dir, its verity_root, and the target
80-
/// (`docker` or a mount path). Repeatable.
80+
/// (an absolute mount path). Repeatable.
8181
#[arg(long = "volume", value_name = "NAME:VERITY_ROOT:TARGET")]
8282
volumes: Vec<String>,
8383
/// deploy in non-KMS mode (ephemeral keys; no KMS required).
@@ -124,11 +124,29 @@ enum Command {
124124
#[arg(long, short = 'o', default_value = "verity.img")]
125125
output: String,
126126
/// squashfs compression: `none` (the default), `zstd`, or `gzip`.
127-
#[arg(long, default_value = "none")]
128-
compress: String,
127+
#[arg(long, value_enum, default_value_t, conflicts_with = "fs_image")]
128+
compress: CompressionArg,
129129
},
130130
}
131131

132+
#[derive(Clone, Copy, Default, ValueEnum)]
133+
enum CompressionArg {
134+
#[default]
135+
None,
136+
Zstd,
137+
Gzip,
138+
}
139+
140+
impl From<CompressionArg> for dstack_volume::Compression {
141+
fn from(value: CompressionArg) -> Self {
142+
match value {
143+
CompressionArg::None => Self::None,
144+
CompressionArg::Zstd => Self::Zstd,
145+
CompressionArg::Gzip => Self::Gzip,
146+
}
147+
}
148+
}
149+
132150
#[tokio::main]
133151
async fn main() -> Result<()> {
134152
// progress (e.g. `verity` pulling layers) goes to stderr so it never mixes
@@ -223,37 +241,22 @@ async fn main() -> Result<()> {
223241
fs_image,
224242
output,
225243
compress,
226-
} => {
227-
cmd_verity(
228-
dir.as_deref(),
229-
fs_image.as_deref(),
230-
&output,
231-
&compress,
232-
json,
233-
)
234-
.await
235-
}
244+
} => cmd_verity(dir.as_deref(), fs_image.as_deref(), &output, compress, json).await,
236245
}
237246
}
238247

239248
async fn cmd_verity(
240249
dir: Option<&str>,
241250
fs_image: Option<&str>,
242251
output: &str,
243-
compress: &str,
252+
compress: CompressionArg,
244253
json: bool,
245254
) -> Result<()> {
246-
let compress = match compress {
247-
"none" => dstack_volume::Compression::None,
248-
"zstd" => dstack_volume::Compression::Zstd,
249-
"gzip" => dstack_volume::Compression::Gzip,
250-
other => bail!("unknown --compress '{other}' (use none|zstd|gzip)"),
251-
};
252255
let result = dstack_volume::verity(dstack_volume::VerityOptions {
253256
dir: dir.map(std::path::PathBuf::from),
254257
fs_image: fs_image.map(std::path::PathBuf::from),
255258
output: output.into(),
256-
compress,
259+
compress: compress.into(),
257260
})
258261
.await?;
259262

@@ -290,13 +293,6 @@ async fn cmd_verity(
290293
Ok(())
291294
}
292295

293-
/// A parsed `--volume` spec. All fields become part of measured app-compose.
294-
struct VolumeSpec {
295-
source: String,
296-
verity_root: String,
297-
target: String,
298-
}
299-
300296
/// Parse a `--volume` spec `NAME:VERITY_ROOT:TARGET`.
301297
///
302298
/// `NAME` is the volume file in the vmm's volumes_dir. `VERITY_ROOT` and `TARGET`
@@ -305,7 +301,7 @@ struct VolumeSpec {
305301
/// spec to paste.
306302
///
307303
/// `TARGET` is an absolute read-only mount path in the guest.
308-
fn parse_volume(spec: &str) -> Result<VolumeSpec> {
304+
fn parse_volume(spec: &str) -> Result<dstack_types::VerityVolume> {
309305
let mut parts = spec.splitn(3, ':');
310306
let name = parts.next().unwrap_or_default();
311307
let (root, target) = match (parts.next(), parts.next()) {
@@ -326,10 +322,12 @@ fn parse_volume(spec: &str) -> Result<VolumeSpec> {
326322
if !target.starts_with('/') {
327323
bail!("target '{target}' must be an absolute path");
328324
}
329-
Ok(VolumeSpec {
325+
let mut verity_root = [0; 32];
326+
hex::decode_to_slice(root, &mut verity_root).context("decoding verity_root")?;
327+
Ok(dstack_types::VerityVolume {
330328
source: name.to_string(),
331-
verity_root: root.to_string(),
332-
target: target.to_string(),
329+
verity_root,
330+
target: target.into(),
333331
})
334332
}
335333

@@ -425,11 +423,7 @@ async fn cmd_deploy(
425423

426424
// each --volume declares a measured verity_volumes entry, so the built
427425
// app-compose (and thus app_id) binds the attested roots.
428-
let verity_volumes: Vec<(String, String, String)> = parsed_volumes
429-
.iter()
430-
.map(|v| (v.source.clone(), v.verity_root.clone(), v.target.clone()))
431-
.collect();
432-
let app_compose = compose::build_app_compose(name, &yaml, !no_kms, &verity_volumes);
426+
let app_compose = compose::build_app_compose(name, &yaml, !no_kms, &parsed_volumes);
433427

434428
let mut cfg = rpc::VmConfiguration {
435429
name: name.to_string(),
@@ -669,8 +663,8 @@ mod tests {
669663
let root = "a".repeat(64);
670664
let data = parse_volume(&format!("weights.img:{root}:/models/llama")).unwrap();
671665
assert_eq!(data.source, "weights.img");
672-
assert_eq!(data.verity_root, root);
673-
assert_eq!(data.target, "/models/llama");
666+
assert_eq!(data.verity_root, [0xaa; 32]);
667+
assert_eq!(data.target, std::path::Path::new("/models/llama"));
674668

675669
assert!(parse_volume("weights.img").is_err()); // missing verity_root:target
676670
assert!(parse_volume(&format!("weights.img:{root}")).is_err()); // missing target
@@ -734,5 +728,23 @@ mod tests {
734728
"rootfs.ext4"
735729
])
736730
.is_err());
731+
assert!(Cli::try_parse_from([
732+
"dstack",
733+
"verity",
734+
"--fs-image",
735+
"rootfs.ext4",
736+
"--compress",
737+
"zstd"
738+
])
739+
.is_err());
740+
assert!(Cli::try_parse_from([
741+
"dstack",
742+
"verity",
743+
"--dir",
744+
"data",
745+
"--compress",
746+
"invalid"
747+
])
748+
.is_err());
737749
}
738750
}

dstack/crates/dstack-volume/src/bin/dstack-volume.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
//! table. Everything read from a disk is untrusted: kind handlers use the
1010
//! measured app compose as their source of policy and cryptographic identity.
1111
12-
use std::collections::HashSet;
1312
use std::io::Read;
1413
use std::os::unix::ffi::OsStrExt;
1514
use std::os::unix::fs::MetadataExt;
@@ -96,9 +95,8 @@ fn mount_all(compose_path: PathBuf) -> Result<()> {
9695
requested = compose.verity_volumes.len(),
9796
"discovered dstack volumes"
9897
);
99-
let mut used = HashSet::new();
10098
for (index, requested) in compose.verity_volumes.iter().enumerate() {
101-
activate_requested(index, requested, &volumes, &mut used).with_context(|| {
99+
activate_requested(index, requested, &volumes).with_context(|| {
102100
format!(
103101
"failed to activate required volume {index} at {}",
104102
requested.target.display()
@@ -276,24 +274,23 @@ fn activate_requested(
276274
index: usize,
277275
requested: &RequestedVolume,
278276
volumes: &[VerityVolume],
279-
used: &mut HashSet<usize>,
280277
) -> Result<()> {
281-
let (candidate_index, candidate) = volumes
278+
let candidate = volumes
282279
.iter()
283-
.enumerate()
284-
.find(|(candidate_index, volume)| {
285-
!used.contains(candidate_index) && volume.root_hash == requested.verity_root
286-
})
280+
.find(|volume| volume.root_hash == requested.verity_root)
287281
.context("no attached volume advertises the measured root")?;
288282

289283
let mapper_name = format!("dstack-verity{index}");
290284
let mapped = PathBuf::from(format!("/dev/mapper/{mapper_name}"));
291285
let expected_root = hex::encode(requested.verity_root);
292286
if mapped.exists() {
293287
if mapping_root(&mapper_name)?.eq_ignore_ascii_case(&expected_root) {
294-
verify_first_block(&mapped)?;
295-
mount_volume(requested, &mapped)?;
296-
used.insert(candidate_index);
288+
if let Err(err) =
289+
verify_first_block(&mapped).and_then(|_| mount_volume(requested, &mapped))
290+
{
291+
let _ = run_cmd!(veritysetup close $mapper_name);
292+
return Err(err);
293+
}
297294
info!(mapper = mapper_name, "reused active verity mapping");
298295
return Ok(());
299296
}
@@ -310,7 +307,6 @@ fn activate_requested(
310307
let _ = run_cmd!(veritysetup close $mapper_name);
311308
return Err(err);
312309
}
313-
used.insert(candidate_index);
314310
Ok(())
315311
}
316312

dstack/crates/dstack-volume/src/volume.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ fn seal_data_image(
142142
let uuid = uuid_from_data(data_path, data_size)?;
143143
let out = run_fun!(
144144
veritysetup format --salt $salt_hex --uuid $uuid
145-
--data-block-size 4096 --hash-block-size 4096 $data_path $hash_path
145+
--hash sha256 --format 1 --data-block-size 4096 --hash-block-size 4096
146+
$data_path $hash_path
146147
)
147148
.context("running veritysetup format")?;
148149
let verity_root =

os/yocto/layers/meta-dstack/recipes-core/dstack-guest/dstack-guest.bb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ DSTACK_ROOTFS_SRC ?= "${DSTACK_MONOREPO_ROOT}/os/common/rootfs"
1515
S = "${UNPACKDIR}/repo/dstack"
1616
DSTACK_ROOTFS_FILES = "${UNPACKDIR}/repo/os/common/rootfs"
1717

18-
RDEPENDS:${PN} += "bash"
18+
RDEPENDS:${PN} += "bash cryptsetup util-linux-blkid util-linux-mount"
1919

2020
DEPENDS += "rsync-native tpm2-tss"
2121
DEPENDS += "cmake-native"

0 commit comments

Comments
 (0)