Skip to content

Commit c889ee5

Browse files
authored
Merge pull request #348 from Dstack-TEE/storage-fs
cvm: Support for ext4
2 parents 3938a49 + 21aa6b2 commit c889ee5

File tree

4 files changed

+265
-23
lines changed

4 files changed

+265
-23
lines changed

docs/security-guide/cvm-boundaries.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ This is the main configuration file for the application in JSON format:
4141
| secure_time | boolean | Whether secure time is enabled |
4242
| pre_launch_script | string | Prelaunch bash script that runs before execute `docker compose up` |
4343
| init_script | string | Bash script that executed prior to dockerd startup |
44+
| storage_fs | string | Filesystem type for the data disk of the CVM. Supported values: "zfs", "ext4". default to "zfs". **ZFS:** Ensures filesystem integrity with built-in data protection features. **ext4:** Provides better performance for database applications with lower overhead and faster I/O operations, but no strong integrity protection. |
4445

4546

4647
The hash of this file content is extended to RTMR3 as event name `compose-hash`. Remote verifier can extract the compose-hash during remote attestation.

dstack-types/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub struct AppCompose {
3939
pub no_instance_id: bool,
4040
#[serde(default = "default_true")]
4141
pub secure_time: bool,
42+
#[serde(default)]
43+
pub storage_fs: Option<String>,
4244
}
4345

4446
fn default_true() -> bool {

dstack-util/src/system_setup.rs

Lines changed: 180 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
use std::{
66
collections::{BTreeMap, BTreeSet},
7+
fmt::Display,
78
ops::Deref,
89
path::{Path, PathBuf},
10+
process::Command,
11+
str::FromStr,
912
};
1013

1114
use anyhow::{anyhow, bail, Context, Result};
@@ -85,6 +88,67 @@ struct InstanceInfo {
8588
app_id: Vec<u8>,
8689
}
8790

91+
#[derive(Debug, Clone, Copy, PartialEq, Default)]
92+
enum FsType {
93+
#[default]
94+
Zfs,
95+
Ext4,
96+
}
97+
98+
impl Display for FsType {
99+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100+
match self {
101+
FsType::Zfs => write!(f, "zfs"),
102+
FsType::Ext4 => write!(f, "ext4"),
103+
}
104+
}
105+
}
106+
107+
impl FromStr for FsType {
108+
type Err = anyhow::Error;
109+
fn from_str(s: &str) -> Result<Self, Self::Err> {
110+
match s.to_lowercase().as_str() {
111+
"zfs" => Ok(FsType::Zfs),
112+
"ext4" => Ok(FsType::Ext4),
113+
_ => bail!("Invalid filesystem type: {s}, supported types: zfs, ext4"),
114+
}
115+
}
116+
}
117+
118+
#[derive(Debug, Clone, Default)]
119+
struct DstackOptions {
120+
storage_encrypted: bool,
121+
storage_fs: FsType,
122+
}
123+
124+
fn parse_dstack_options(shared: &HostShared) -> Result<DstackOptions> {
125+
let cmdline = fs::read_to_string("/proc/cmdline").context("Failed to read /proc/cmdline")?;
126+
127+
let mut options = DstackOptions {
128+
storage_encrypted: true, // Default to encryption enabled
129+
storage_fs: FsType::Zfs, // Default to ZFS
130+
};
131+
132+
for param in cmdline.split_whitespace() {
133+
if let Some(value) = param.strip_prefix("dstack.storage_encrypted=") {
134+
match value {
135+
"0" | "false" | "no" | "off" => options.storage_encrypted = false,
136+
"1" | "true" | "yes" | "on" => options.storage_encrypted = true,
137+
_ => {
138+
bail!("Invalid value for dstack.storage_encrypted: {value}");
139+
}
140+
}
141+
} else if let Some(value) = param.strip_prefix("dstack.storage_fs=") {
142+
options.storage_fs = value.parse().context("Failed to parse dstack.storage_fs")?;
143+
}
144+
}
145+
146+
if let Some(fs) = &shared.app_compose.storage_fs {
147+
options.storage_fs = fs.parse().context("Failed to parse storage_fs")?;
148+
}
149+
Ok(options)
150+
}
151+
88152
impl InstanceInfo {
89153
fn is_initialized(&self) -> bool {
90154
!self.instance_id_seed.is_empty()
@@ -621,38 +685,122 @@ impl<'a> Stage0<'a> {
621685
}
622686
}
623687

624-
async fn mount_data_disk(&self, initialized: bool, disk_crypt_key: &str) -> Result<()> {
688+
async fn mount_data_disk(
689+
&self,
690+
initialized: bool,
691+
disk_crypt_key: &str,
692+
opts: &DstackOptions,
693+
) -> Result<()> {
625694
let name = "dstack_data_disk";
626-
let fs_dev = "/dev/mapper/".to_string() + name;
627695
let mount_point = &self.args.mount_point;
696+
697+
// Determine the device to use based on encryption settings
698+
let fs_dev = if opts.storage_encrypted {
699+
format!("/dev/mapper/{name}")
700+
} else {
701+
self.args.device.to_string_lossy().to_string()
702+
};
703+
704+
cmd!(mkdir -p $mount_point).context("Failed to create mount point")?;
705+
628706
if !initialized {
629707
self.vmm
630708
.notify_q("boot.progress", "initializing data disk")
631709
.await;
632-
info!("Setting up disk encryption");
633-
self.luks_setup(disk_crypt_key, name)?;
634-
cmd! {
635-
mkdir -p $mount_point;
636-
zpool create -o autoexpand=on dstack $fs_dev;
637-
zfs create -o mountpoint=$mount_point -o atime=off -o checksum=blake3 dstack/data;
710+
711+
if opts.storage_encrypted {
712+
info!("Setting up disk encryption");
713+
self.luks_setup(disk_crypt_key, name)?;
714+
} else {
715+
info!("Skipping disk encryption as requested by kernel cmdline");
716+
}
717+
718+
match opts.storage_fs {
719+
FsType::Zfs => {
720+
info!("Creating ZFS filesystem");
721+
cmd! {
722+
zpool create -o autoexpand=on dstack $fs_dev;
723+
zfs create -o mountpoint=$mount_point -o atime=off -o checksum=blake3 dstack/data;
724+
}
725+
.context("Failed to create zpool")?;
726+
}
727+
FsType::Ext4 => {
728+
info!("Creating ext4 filesystem");
729+
cmd! {
730+
mkfs.ext4 -F $fs_dev;
731+
mount $fs_dev $mount_point;
732+
}
733+
.context("Failed to create ext4 filesystem")?;
734+
}
638735
}
639-
.context("Failed to create zpool")?;
640736
} else {
641737
self.vmm
642738
.notify_q("boot.progress", "mounting data disk")
643739
.await;
644-
info!("Mounting encrypted data disk");
645-
self.open_encrypted_volume(disk_crypt_key, name)?;
646-
cmd! {
647-
zpool import dstack;
648-
zpool status dstack;
649-
zpool online -e dstack $fs_dev; // triggers autoexpand
740+
741+
if opts.storage_encrypted {
742+
info!("Mounting encrypted data disk");
743+
self.open_encrypted_volume(disk_crypt_key, name)?;
744+
} else {
745+
info!("Mounting unencrypted data disk");
746+
}
747+
748+
match opts.storage_fs {
749+
FsType::Zfs => {
750+
cmd! {
751+
zpool import dstack;
752+
zpool status dstack;
753+
zpool online -e dstack $fs_dev; // triggers autoexpand
754+
}
755+
.context("Failed to import zpool")?;
756+
if cmd!(mountpoint -q $mount_point).is_err() {
757+
cmd!(zfs mount dstack/data).context("Failed to mount zpool")?;
758+
}
759+
}
760+
FsType::Ext4 => {
761+
Self::mount_e2fs(&fs_dev, mount_point)
762+
.context("Failed to mount ext4 filesystem")?;
763+
}
764+
}
765+
}
766+
Ok(())
767+
}
768+
769+
fn mount_e2fs(dev: &impl AsRef<Path>, mount_point: &impl AsRef<Path>) -> Result<()> {
770+
let dev = dev.as_ref();
771+
let mount_point = mount_point.as_ref();
772+
info!("Checking filesystem");
773+
774+
let e2fsck_status = Command::new("e2fsck")
775+
.arg("-f")
776+
.arg("-p")
777+
.arg(dev)
778+
.status()
779+
.with_context(|| format!("Failed to run e2fsck on {}", dev.display()))?;
780+
781+
match e2fsck_status.code() {
782+
Some(0 | 1) => {}
783+
Some(code) => {
784+
bail!(
785+
"e2fsck exited with status {code} while checking {}",
786+
dev.display()
787+
);
650788
}
651-
.context("Failed to import zpool")?;
652-
if cmd!(mountpoint -q $mount_point).is_err() {
653-
cmd!(zfs mount dstack/data).context("Failed to mount zpool")?;
789+
None => {
790+
bail!(
791+
"e2fsck terminated by signal while checking {}",
792+
dev.display()
793+
);
654794
}
655795
}
796+
797+
cmd! {
798+
info "Trying to resize filesystem if needed";
799+
resize2fs $dev;
800+
info "Mounting filesystem";
801+
mount $dev $mount_point;
802+
}
803+
.context("Failed to prepare ext4 filesystem")?;
656804
Ok(())
657805
}
658806

@@ -802,9 +950,21 @@ impl<'a> Stage0<'a> {
802950
let keys_json = serde_json::to_string(&app_keys).context("Failed to serialize app keys")?;
803951
fs::write(self.app_keys_file(), keys_json).context("Failed to write app keys")?;
804952

953+
// Parse kernel command line options
954+
let opts = parse_dstack_options(&self.shared).context("Failed to parse kernel cmdline")?;
955+
extend_rtmr3("storage-fs", opts.storage_fs.to_string().as_bytes())?;
956+
info!(
957+
"Filesystem options: encryption={}, filesystem={:?}",
958+
opts.storage_encrypted, opts.storage_fs
959+
);
960+
805961
self.vmm.notify_q("boot.progress", "unsealing env").await;
806-
self.mount_data_disk(is_initialized, &hex::encode(&app_keys.disk_crypt_key))
807-
.await?;
962+
self.mount_data_disk(
963+
is_initialized,
964+
&hex::encode(&app_keys.disk_crypt_key),
965+
&opts,
966+
)
967+
.await?;
808968
self.vmm
809969
.notify_q(
810970
"instance.info",

vmm/src/console.html

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,56 @@
492492
font-family: monospace;
493493
}
494494
</style>
495+
<style>
496+
/* Help icon tooltip styles */
497+
.help-icon {
498+
display: inline-block;
499+
margin-left: 5px;
500+
color: #666;
501+
cursor: help;
502+
position: relative;
503+
}
504+
505+
.help-icon:hover {
506+
color: #4285F4;
507+
}
508+
509+
.help-icon .tooltip {
510+
visibility: hidden;
511+
width: 300px;
512+
background-color: #333;
513+
color: #fff;
514+
text-align: left;
515+
border-radius: 6px;
516+
padding: 10px;
517+
position: absolute;
518+
z-index: 1000;
519+
bottom: 125%;
520+
left: 50%;
521+
margin-left: -150px;
522+
opacity: 0;
523+
transition: opacity 0.3s;
524+
font-size: 12px;
525+
line-height: 1.4;
526+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
527+
}
528+
529+
.help-icon .tooltip::after {
530+
content: "";
531+
position: absolute;
532+
top: 100%;
533+
left: 50%;
534+
margin-left: -5px;
535+
border-width: 5px;
536+
border-style: solid;
537+
border-color: #333 transparent transparent transparent;
538+
}
539+
540+
.help-icon:hover .tooltip {
541+
visibility: visible;
542+
opacity: 1;
543+
}
544+
</style>
495545
<style>
496546
.ml-text-ro {
497547
white-space: pre-wrap;
@@ -662,9 +712,27 @@ <h2>Deploy a new instance</h2>
662712
</div>
663713

664714
<div class="form-group">
665-
<label for="diskSize">Disk Size (GB)</label>
715+
<label for="diskSize">Storage (GB)</label>
666716
<input id="diskSize" v-model.number="vmForm.disk_size" type="number"
667-
placeholder="Disk size in GB" required>
717+
placeholder="Storage size in GB" required>
718+
</div>
719+
720+
<div class="form-group">
721+
<label for="storageFs">Storage Filesystem
722+
<span class="help-icon">
723+
<i class="fas fa-question-circle"></i>
724+
<span class="tooltip">
725+
<strong>ZFS:</strong> Ensures filesystem integrity with built-in data protection features.<br><br>
726+
<strong>ext4:</strong> Provides better performance for database applications
727+
with lower overhead and faster I/O operations.
728+
</span>
729+
</span>
730+
</label>
731+
<select id="storageFs" v-model="vmForm.storage_fs">
732+
<option value="">Default (ZFS)</option>
733+
<option value="zfs">ZFS</option>
734+
<option value="ext4">ext4</option>
735+
</select>
668736
</div>
669737

670738
<div class="form-group full-width">
@@ -1000,6 +1068,11 @@ <h4 style="margin: 0 0 12px 0;">App Information</h4>
10001068
<span class="detail-label">Features:</span>
10011069
<span class="detail-value">{{ getFlags(vm) || 'None' }}</span>
10021070
</div>
1071+
<div class="detail-item">
1072+
<span class="detail-label">Storage FS:</span>
1073+
<span class="detail-value">{{ vm.appCompose?.storage_fs ?
1074+
vm.appCompose.storage_fs.toUpperCase() : 'ZFS (default)' }}</span>
1075+
</div>
10031076
</div>
10041077

10051078
<h4 style="margin: 0 0 12px 0;">Docker Compose File</h4>
@@ -1464,6 +1537,7 @@ <h3>Derive VM</h3>
14641537
username: '',
14651538
token_key: ''
14661539
},
1540+
storage_fs: '',
14671541
app_id: '',
14681542
kms_enabled: true,
14691543
local_key_provider_enabled: false,
@@ -1642,6 +1716,10 @@ <h3>Derive VM</h3>
16421716
"secure_time": false,
16431717
};
16441718

1719+
if (vmForm.value.storage_fs) {
1720+
app_compose.storage_fs = vmForm.value.storage_fs;
1721+
}
1722+
16451723
if (vmForm.value.preLaunchScript?.trim()) {
16461724
app_compose.pre_launch_script = vmForm.value.preLaunchScript;
16471725
}
@@ -2336,7 +2414,8 @@ <h3>Derive VM</h3>
23362414
() => vmForm.value.docker_config.enabled,
23372415
() => vmForm.value.docker_config.username,
23382416
() => vmForm.value.docker_config.token_key,
2339-
() => vmForm.value.encryptedEnvs
2417+
() => vmForm.value.encryptedEnvs,
2418+
() => vmForm.value.storage_fs
23402419
], async () => {
23412420
try {
23422421
const appCompose = await makeAppComposeFile();

0 commit comments

Comments
 (0)