Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8bd1f22
deploy/aarch64: add reproducible mvm vmlinux build pipeline
May 14, 2026
603d2ca
docker/Dockerfile.builder: support multi-arch builder image
May 20, 2026
ac95952
hypervisor: fix build and runtime regressions on aarch64
May 20, 2026
ef1d89d
shim: enable aarch64 support for CubeShim
May 20, 2026
3c92c6c
agent: enable aarch64 support for the guest agent
May 20, 2026
84c753d
cubenet: enable arm64 build for the cubevs eBPF loader
May 20, 2026
4458fd9
cubelet: fix image pull and overlay snapshot edge cases
May 20, 2026
9e940df
cubemaster: drop the legacy gomonkey v1 dependency for aarch64
May 20, 2026
3f706d4
deploy/guest-image: switch base image to a shared amd64/arm64 one
May 20, 2026
3455e78
deploy/one-click: resolve mkcert binary per host architecture
May 20, 2026
40ebc4d
deploy/one-click: make container images overridable via env.example
May 21, 2026
89ae1df
docs: document initial aarch64 support across deployment guides
May 22, 2026
b042360
hypervisor: fall back to PMU-less vcpu init on aarch64
May 26, 2026
da00564
CubeShim: probe vsock as fallback when ready notify is missing
May 26, 2026
ffdb001
agent: notify ready after accept loop and harden guest init mounts
May 26, 2026
2813369
shim: gate x86-only earlyprintk/mitigations cmdline params for aarch64
May 25, 2026
0d52f6d
cubelet: avoid divide-by-zero in GetDeviceIdleRatio for Btrfs
May 25, 2026
7339509
network-agent: probe gateway before reading neighbor MAC; defensive o…
May 25, 2026
df1545c
shim: sanitize empty seccomp.defaultAction before oci-spec deserializ…
May 25, 2026
0b3daae
fix(hypervisor): call start_migration before snapshot to fix VirtioFS…
May 19, 2026
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
1 change: 0 additions & 1 deletion CubeMaster/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ go 1.24.8
// toolchain go1.22.9

require (
github.com/agiledragon/gomonkey v2.0.2+incompatible
github.com/agiledragon/gomonkey/v2 v2.9.0
github.com/alicebob/miniredis/v2 v2.35.0
github.com/fsnotify/fsnotify v1.9.0
Expand Down
2 changes: 0 additions & 2 deletions CubeMaster/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
github.com/agiledragon/gomonkey/v2 v2.9.0 h1:PDiKKybR596O6FHW+RVSG0Z7uGCBNbmbUXh3uCNQ7Hc=
github.com/agiledragon/gomonkey/v2 v2.9.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
Expand Down
2 changes: 1 addition & 1 deletion CubeMaster/integration/mock_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"testing"
"time"

"github.com/agiledragon/gomonkey"
"github.com/agiledragon/gomonkey/v2"
"github.com/alicebob/miniredis/v2"
"github.com/gomodule/redigo/redis"
"github.com/google/uuid"
Expand Down
57 changes: 57 additions & 0 deletions CubeNet/cubevs/bpf_arm64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Tencent Inc. All rights reserved.

//go:build arm64

package cubevs

import (
"bytes"
_ "embed"
"fmt"

"github.com/cilium/ebpf"
)

// loadLocalgw returns the embedded CollectionSpec for localgw.
func loadLocalgw() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_LocalgwBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load localgw: %w", err)
}
return spec, nil
}

// loadMvmtap returns the embedded CollectionSpec for mvmtap.
func loadMvmtap() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_MvmtapBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load mvmtap: %w", err)
}
return spec, nil
}

// loadNodenic returns the embedded CollectionSpec for nodenic.
func loadNodenic() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_NodenicBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load nodenic: %w", err)
}
return spec, nil
}

// Reuse the checked-in little-endian eBPF objects. The original bpf2go wrappers
// are amd64-only, but the objects themselves are eBPF bytecode and are usable on
// arm64 Linux as well.
//
//go:embed localgw_x86_bpfel.o
var _LocalgwBytes []byte

//go:embed mvmtap_x86_bpfel.o
var _MvmtapBytes []byte

//go:embed nodenic_x86_bpfel.o
var _NodenicBytes []byte
3 changes: 2 additions & 1 deletion CubeProxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM cube-sandbox-image.tencentcloudcr.com/opensource/openresty:1.21.4.1-6-alpine-fat
ARG BASE_IMAGE=cube-sandbox-image.tencentcloudcr.com/opensource/openresty:1.21.4.1-6-alpine-fat
FROM ${BASE_IMAGE}

LABEL maintainer="staryxchen <staryxchen@tencent.com>"

Expand Down
31 changes: 29 additions & 2 deletions CubeShim/shim/src/common/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,35 @@ impl Utils {
pub fn load_spec(bundle: &str) -> CResult<Spec> {
let mut conf_path = PathBuf::from(bundle);
conf_path.push("config.json");
let spec = Spec::load(conf_path)
.map_err(|e| format!("load config failed:{} bundle:{}", e, bundle))?;

// Read the config.json as a raw JSON value first, so we can sanitize
// fields that the oci-spec 0.6.x deserializer rejects. Recent
// containerd releases emit `linux.seccomp = {"defaultAction": ""}`
// when the runtime spec has no seccomp profile, and the empty string
// fails to deserialize into the LinuxSeccompAction enum, which makes
// sandbox creation fall over with an opaque serde error.
//
// Strip the empty-action seccomp object up-front; an absent seccomp
// section is the correct representation of "no profile applied".
let data = fs::read_to_string(&conf_path).map_err(|e| {
format!(
"load config failed:read file {:?}: {} bundle:{}",
conf_path, e, bundle
)
})?;
let mut value: serde_json::Value = serde_json::from_str(&data)
.map_err(|e| format!("load config failed:parse json: {} bundle:{}", e, bundle))?;
if let Some(linux) = value.get_mut("linux") {
if let Some(seccomp) = linux.get("seccomp") {
if seccomp.get("defaultAction").and_then(|v| v.as_str()) == Some("") {
if let Some(obj) = linux.as_object_mut() {
obj.remove("seccomp");
}
}
}
}
let spec: Spec = serde_json::from_value(value)
.map_err(|e| format!("load config failed:serde failed:{} bundle:{}", e, bundle))?;
Ok(spec)
}

Expand Down
48 changes: 34 additions & 14 deletions CubeShim/shim/src/hypervisor/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,36 @@ pub struct VmConfig {

impl Default for VmConfig {
fn default() -> Self {
let params = vec![
let mut params = vec![
"root=/dev/pmem0".to_string(),
"rootflags=dax,errors=remount-ro ro".to_string(),
"rootfstype=ext4".to_string(),
"panic=1".to_string(),
"no_timer_check".to_string(),
"noreplace-smp".to_string(),
"printk.devkmsg=on".to_string(),
"console=hvc0".to_string(),
];
#[cfg(target_arch = "x86_64")]
params.extend(["no_timer_check".to_string(), "noreplace-smp".to_string()]);
params.extend(["printk.devkmsg=on".to_string()]);
#[cfg(target_arch = "aarch64")]
params.push("console=ttyAMA0,115200".to_string());
#[cfg(not(target_arch = "aarch64"))]
params.push("console=hvc0".to_string());
params.extend([
"net.ifnames=0".to_string(),
"audit=0".to_string(),
"LANG=C".to_string(),
"raid=noautodetect".to_string(),
"earlyprintk=ttyS0".to_string(),
"agent.debug_console".to_string(),
"agent.debug_console_vport=1026".to_string(),
]);
// x86-only kernel cmdline parameters. The ARM kernel does not recognize
// these and forwards unknown tokens as argv to /sbin/init (= cube-agent),
// making clap's argument parser fail and the agent exit immediately
// (kernel panic on init / "unrecognized argument" from agent).
#[cfg(target_arch = "x86_64")]
params.extend([
"earlyprintk=ttyS0".to_string(),
"mitigations=off".to_string(),
];
]);

let pmems = vec![PmemConfig {
file: PathBuf::from(IMAGE_PATH),
Expand Down Expand Up @@ -452,24 +464,32 @@ mod tests {
assert_eq!(config.console.mode, ConsoleOutputMode::Tty);
assert_eq!(config.rng.src, PathBuf::from("/dev/urandom"));

let params = vec![
let mut params = vec![
"root=/dev/pmem0".to_string(),
"rootflags=dax,errors=remount-ro ro".to_string(),
"rootfstype=ext4".to_string(),
"panic=1".to_string(),
"no_timer_check".to_string(),
"noreplace-smp".to_string(),
"printk.devkmsg=on".to_string(),
"console=hvc0".to_string(),
];
#[cfg(target_arch = "x86_64")]
params.extend(["no_timer_check".to_string(), "noreplace-smp".to_string()]);
params.extend(["printk.devkmsg=on".to_string()]);
#[cfg(target_arch = "aarch64")]
params.push("console=ttyAMA0,115200".to_string());
#[cfg(not(target_arch = "aarch64"))]
params.push("console=hvc0".to_string());
params.extend([
"net.ifnames=0".to_string(),
"audit=0".to_string(),
"LANG=C".to_string(),
"raid=noautodetect".to_string(),
"earlyprintk=ttyS0".to_string(),
"agent.debug_console".to_string(),
"agent.debug_console_vport=1026".to_string(),
]);
#[cfg(target_arch = "x86_64")]
params.extend([
"earlyprintk=ttyS0".to_string(),
"mitigations=off".to_string(),
];
]);
assert_eq!(config.cmdlines, params);
}

Expand Down
3 changes: 3 additions & 0 deletions CubeShim/shim/src/hypervisor/cube_hypervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ impl CubeHypervisor {
return Err(self.status_err("oops: ch is not None".to_string()));
}
cube_hypervisor::set_runtime_seccomp_rules(vec![
#[cfg(target_arch = "x86_64")]
(libc::SYS_mkdir, vec![]),
#[cfg(target_arch = "aarch64")]
(libc::SYS_mkdirat, vec![]),
(libc::SYS_getsockopt, vec![]),
(libc::SYS_setsockopt, vec![]),
(libc::SYS_faccessat2, vec![]),
Expand Down
130 changes: 119 additions & 11 deletions CubeShim/shim/src/sandbox/sb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use tokio::time::{sleep, Duration};

use super::device;
use super::disk::Disk;
use super::pmem::{self, Pmem};
use super::pmem::Pmem;
//use tokio_uring::fs::UnixStream;

const ANNO_SANDBOX_DNS: &str = "cube.sandbox.dns";
Expand Down Expand Up @@ -776,23 +776,131 @@ impl SandBox {
{
let ch = self.ch.as_mut().unwrap().lock().await;
let start = Instant::now();
let ev = ch
.wait_notify(Duration::from_nanos(1000 * 1000 * 1000 * 10 as u64))
.await?;
let total_wait = Duration::from_secs(30);
let wait_res = ch.wait_notify(total_wait).await;
drop(ch);

if CH::NotifyEvent::VsockServerReady != ev {
return Err(format!(
"Not an expected event, expected:{:?}, actual:{:?}",
CH::NotifyEvent::VsockServerReady,
match &wait_res {
Ok(ev) if *ev == CH::NotifyEvent::VsockServerReady => {
infof!(
self.log,
"vm ready, vsock is listening (notify path), cost:{}",
start.elapsed().as_millis()
);
return Ok(snapshot);
}
Ok(ev) if *ev == CH::NotifyEvent::VmShutdown => {
return Err(format!(
"Not an expected event, expected:{:?}, actual:{:?}",
CH::NotifyEvent::VsockServerReady,
ev
));
}
Ok(ev) => warnf!(
self.log,
"wait_notify got unexpected non-fatal event {:?}, fallback to vsock probe",
ev
));
),
Err(e) => warnf!(
self.log,
"wait_notify VsockServerReady failed: {}, fallback to vsock probe",
e
),
}
let duration = start.elapsed().as_millis();
infof!(self.log, "vm ready, vsock is listening, cost:{}", duration);

let probe_budget = Duration::from_secs(15);
Self::probe_vsock_ready(&self.id, probe_budget, &self.log)
.await
.map_err(|e| {
let orig = match &wait_res {
Ok(ev) => format!("{:?}", ev),
Err(orig) => orig.clone(),
};
format!(
"vsock not ready: notify={}, probe={}",
orig, e
)
})?;
infof!(
self.log,
"vm ready, vsock is listening (probe path), cost:{}",
start.elapsed().as_millis()
);
}
Ok(snapshot)
}

async fn probe_vsock_ready(
sandbox_id: &str,
budget: Duration,
log: &Log,
) -> std::result::Result<(), String> {
let vsock_path = Utils::vsock_path(sandbox_id);
const AGENT_VSOCK_PORT: u32 = 1024;
const PROBE_INTERVAL: Duration = Duration::from_millis(200);
let per_try_timeout = std::cmp::min(budget, Duration::from_millis(2000));

let deadline = std::time::Instant::now() + budget;
let mut last_err = String::from("not attempted");
while std::time::Instant::now() < deadline {
if !vsock_path.exists() {
last_err = format!("vsock socket {:?} not bound yet", vsock_path);
} else {
match tokio::time::timeout(
per_try_timeout,
Self::try_hybrid_vsock_handshake(&vsock_path, AGENT_VSOCK_PORT),
)
.await
{
Ok(Ok(())) => return Ok(()),
Ok(Err(e)) => last_err = format!("handshake: {}", e),
Err(_) => {
last_err = format!(
"handshake timeout {}ms",
per_try_timeout.as_millis()
)
}
}
}
debugf!(log, "vsock probe: {}", last_err);
sleep(PROBE_INTERVAL).await;
}
Err(format!(
"vsock probe gave up in {}ms, last: {}",
budget.as_millis(),
last_err
))
}

async fn try_hybrid_vsock_handshake(
vsock_path: &PathBuf,
port: u32,
) -> std::result::Result<(), String> {
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::net::UnixStream;

let mut conn = UnixStream::connect(vsock_path)
.await
.map_err(|e| format!("connect {:?} failed: {}", vsock_path, e))?;
conn.write_all(format!("CONNECT {}\n", port).as_bytes())
.await
.map_err(|e| format!("write CONNECT failed: {}", e))?;
let mut reader = tokio::io::BufReader::new(conn);
let mut response = String::new();
let n = reader
.read_line(&mut response)
.await
.map_err(|e| format!("read response failed: {}", e))?;
if n == 0 {
return Err("EOF before response (muxer/guest closed; likely no listener on guest:1024 -> agent ttRPC server not started)".to_string());
}
if response.contains("OK") {
Ok(())
} else {
Err(format!("handshake response: {:?}", response.trim()))
}
}

async fn boot_vm(&mut self) -> CResult<()> {
let config = self.prepare_resource().await?;
let mut ch = self.ch.as_mut().unwrap().lock().await;
Expand Down
3 changes: 3 additions & 0 deletions CubeShim/shim/src/snapshot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,10 @@ impl Snapshot {
fn launch_vmm(&mut self) -> CResult<()> {
//launch
cube_hypervisor::set_runtime_seccomp_rules(vec![
#[cfg(target_arch = "x86_64")]
(libc::SYS_mkdir, vec![]),
#[cfg(target_arch = "aarch64")]
(libc::SYS_mkdirat, vec![]),
(libc::SYS_getsockopt, vec![]),
(libc::SYS_setsockopt, vec![]),
]);
Expand Down
Loading
Loading