use super::*;
pub(crate) fn redirect_stdio_to_bulk_port() {
use std::io::Read;
use std::os::unix::io::{AsRawFd, FromRawFd};
fn make_pipe() -> Option<(std::fs::File, std::fs::File)> {
let mut fds = [0i32; 2];
let r = unsafe { libc::pipe(fds.as_mut_ptr()) };
if r < 0 {
return None;
}
let read_end = unsafe { std::fs::File::from_raw_fd(fds[0]) };
let write_end = unsafe { std::fs::File::from_raw_fd(fds[1]) };
Some((read_end, write_end))
}
fn spawn_forwarder(mut read_end: std::fs::File, name: &'static str, sender: fn(&[u8]) -> bool) {
let _ = std::thread::Builder::new()
.name(name.into())
.spawn(move || {
let mut buf = [0u8; STDIO_CHUNK_BYTES];
loop {
match read_end.read(&mut buf) {
Ok(0) => break, Ok(n) => {
let _ = sender(&buf[..n]);
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
});
}
let Some((stdout_r, stdout_w)) = make_pipe() else {
tracing::error!("ktstr-init: redirect_stdio_to_bulk_port: pipe(stdout) failed");
return;
};
let Some((stderr_r, stderr_w)) = make_pipe() else {
tracing::error!("ktstr-init: redirect_stdio_to_bulk_port: pipe(stderr) failed");
return;
};
let (rc1, err1, rc2, err2) = unsafe {
let r1 = libc::dup2(stdout_w.as_raw_fd(), 1);
let e1 = std::io::Error::last_os_error();
let r2 = libc::dup2(stderr_w.as_raw_fd(), 2);
let e2 = std::io::Error::last_os_error();
(r1, e1, r2, e2)
};
if rc1 < 0 {
tracing::error!(err = %err1, "ktstr-init: redirect_stdio_to_bulk_port: dup2(stdout) failed");
}
if rc2 < 0 {
tracing::error!(err = %err2, "ktstr-init: redirect_stdio_to_bulk_port: dup2(stderr) failed");
}
spawn_forwarder(stdout_r, "ktstr-stdout-fwd", |b| {
crate::vmm::guest_comms::send_stdout_chunk(b)
});
spawn_forwarder(stderr_r, "ktstr-stderr-fwd", |b| {
crate::vmm::guest_comms::send_stderr_chunk(b)
});
}
pub(crate) fn shell_mode_requested() -> bool {
fs::read_to_string("/proc/cmdline")
.map(|c| cmdline_contains_token(&c, "KTSTR_MODE=shell"))
.unwrap_or(false)
}
pub(crate) fn disk_template_mode_requested() -> bool {
fs::read_to_string("/proc/cmdline")
.map(|c| cmdline_contains_token(&c, "KTSTR_MODE=disk_template"))
.unwrap_or(false)
}
pub(crate) fn cmdline_contains_token(cmdline: &str, token: &str) -> bool {
cmdline.split_whitespace().any(|s| s == token)
}
pub(crate) fn run_disk_template_mode() -> i32 {
redirect_stdio_to_bulk_port();
const MKFS: &str = "/bin/mkfs.btrfs";
tracing::info!(mkfs = MKFS, target = "/dev/vda", "running mkfs.btrfs");
let mut diag = format!(
"MKFS_DIAG /dev/vda exists={} /sys/class/block/vda exists={}\n",
std::path::Path::new("/dev/vda").exists(),
std::path::Path::new("/sys/class/block/vda").exists(),
);
match std::fs::metadata(MKFS) {
Ok(m) => diag.push_str(&format!("MKFS_DIAG {MKFS} size={}\n", m.len())),
Err(e) => diag.push_str(&format!("MKFS_DIAG {MKFS} metadata failed: {e}\n")),
}
let output = with_sigchld_default(|| Command::new(MKFS).args(["-f", "/dev/vda"]).output());
let mut code = match output {
Ok(o) => {
diag.push_str(&format!(
"MKFS_DIAG exit={:?}\nMKFS_STDOUT:\n{}\nMKFS_STDERR:\n{}\n",
o.status.code(),
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr),
));
o.status.code().unwrap_or(1)
}
Err(e) => {
diag.push_str(&format!("MKFS_DIAG mkfs spawn failed: {e}\n"));
1
}
};
const BTRFS_DEV_MAGIC: u64 = 0x4D5F_5366_5248_425F;
let magic = read_dev_vda_magic();
let magic_present = magic.as_ref().is_ok_and(|&m| m == BTRFS_DEV_MAGIC);
diag.push_str(&format!(
"MKFS_DIAG in-guest /dev/vda magic@0x10040 = {}\n",
match &magic {
Ok(m) => format!("0x{m:016x}"),
Err(e) => format!("read failed: {e}"),
},
));
if code == 0 {
if let Some(flush_err) = flush_template_disk() {
diag.push_str(&format!("MKFS_DIAG flush: {flush_err}\n"));
}
if !magic_present {
diag.push_str(
"MKFS_DIAG VERDICT: mkfs exited 0 but /dev/vda has no btrfs magic \
in-guest — silent no-op (exit overridden to 64)\n",
);
code = 64;
}
}
if let Ok(mut com2) = std::fs::OpenOptions::new().write(true).open("/dev/ttyS1") {
use std::io::Write;
let _ = com2.write_all(diag.as_bytes());
let _ = com2.flush();
}
code
}
fn read_dev_vda_magic() -> std::io::Result<u64> {
use std::io::{Read, Seek, SeekFrom};
let mut f = std::fs::File::open("/dev/vda")?;
f.seek(SeekFrom::Start(0x1_0040))?;
let mut buf = [0u8; 8];
f.read_exact(&mut buf)?;
Ok(u64::from_le_bytes(buf))
}
fn flush_template_disk() -> Option<String> {
match std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/vda")
{
Ok(f) => f
.sync_all()
.err()
.map(|e| format!("sync_all(/dev/vda) failed: {e}")),
Err(e) => Some(format!("open(/dev/vda) for flush failed: {e}")),
}
}
pub(crate) fn shell_exec_cmd() -> Option<String> {
fs::read_to_string("/exec_cmd")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
pub(crate) fn cmdline_val(key: &str) -> Option<String> {
let cmdline = fs::read_to_string("/proc/cmdline").ok()?;
let prefix = format!("{key}=");
cmdline
.split_whitespace()
.find_map(|s| s.strip_prefix(&prefix))
.map(|s| s.to_string())
}
pub(crate) fn build_include_path() -> String {
use std::collections::BTreeSet;
use std::os::unix::fs::PermissionsExt;
let include_dir = std::path::Path::new("/include-files");
let mut dirs = BTreeSet::new();
if include_dir.is_dir() {
for entry in walkdir::WalkDir::new(include_dir).follow_links(true) {
let Ok(entry) = entry else { continue };
if entry.file_type().is_file()
&& entry
.metadata()
.is_ok_and(|m| m.permissions().mode() & 0o111 != 0)
&& let Some(parent) = entry.path().parent()
{
dirs.insert(parent.to_string_lossy().to_string());
}
}
}
let mut path_parts: Vec<String> = dirs.into_iter().collect();
path_parts.push("/bin".to_string());
path_parts.join(":")
}
pub(crate) fn redirect_all_stdio_to(path: &str) {
use std::os::unix::io::AsRawFd;
let Ok(dev) = fs::OpenOptions::new().read(true).write(true).open(path) else {
return;
};
let fd = dev.as_raw_fd();
let (rc0, err0, rc1, err1, rc2, err2) = unsafe {
let r0 = libc::dup2(fd, 0);
let e0 = std::io::Error::last_os_error();
let r1 = libc::dup2(fd, 1);
let e1 = std::io::Error::last_os_error();
let r2 = libc::dup2(fd, 2);
let e2 = std::io::Error::last_os_error();
(r0, e0, r1, e1, r2, e2)
};
if rc0 < 0 {
tracing::error!(path, err = %err0, "ktstr-init: redirect_all_stdio_to: dup2(stdin) failed");
}
if rc1 < 0 {
tracing::error!(path, err = %err1, "ktstr-init: redirect_all_stdio_to: dup2(stdout) failed");
}
if rc2 < 0 {
tracing::error!(path, err = %err2, "ktstr-init: redirect_all_stdio_to: dup2(stderr) failed");
}
}
pub(crate) fn shell_console_device() -> &'static str {
if Path::new(HVC0).exists() { HVC0 } else { COM2 }
}
pub(crate) fn mount_devpts() {
mkdir_p("/dev/pts");
let result = mount(
Some("devpts"),
"/dev/pts",
Some("devpts"),
MsFlags::empty(),
None::<&str>,
);
if let Err(e) = result {
tracing::error!(err = %e, "ktstr-init: mount devpts on /dev/pts failed");
}
}
pub(crate) fn spawn_shell_with_pty() {
let pty = match openpty(None, None) {
Ok(p) => p,
Err(e) => {
tracing::error!(err = %e, "ktstr-init: openpty failed");
return;
}
};
let slave_fd = pty.slave.as_raw_fd();
if let (Some(cols), Some(rows)) = (cmdline_val("KTSTR_COLS"), cmdline_val("KTSTR_ROWS"))
&& let (Ok(cols), Ok(rows)) = (cols.parse::<u16>(), rows.parse::<u16>())
{
let ws = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe {
libc::ioctl(slave_fd, libc::TIOCSWINSZ, &ws);
}
}
let term = cmdline_val("KTSTR_TERM").unwrap_or_else(|| "linux".to_string());
let colorterm = cmdline_val("KTSTR_COLORTERM");
let child = unsafe {
let mut cmd = Command::new("/bin/busybox");
cmd.arg("sh")
.env("TERM", &term)
.env("PS1", "\x1b[2m^Ax=quit\x1b[0m \\w # ");
if let Some(ref ct) = colorterm {
cmd.env("COLORTERM", ct);
}
cmd.stdin(Stdio::from(OwnedFd::from_raw_fd(libc::dup(slave_fd))))
.stdout(Stdio::from(OwnedFd::from_raw_fd(libc::dup(slave_fd))))
.stderr(Stdio::from(OwnedFd::from_raw_fd(libc::dup(slave_fd))))
.pre_exec(move || {
if libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
if libc::ioctl(slave_fd, libc::TIOCSCTTY, 0) < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
})
.spawn()
};
drop(pty.slave);
let mut child = match child {
Ok(c) => c,
Err(e) => {
tracing::error!(err = %e, "ktstr-init: spawn shell failed");
return;
}
};
let child_pid = child.id();
let stdin_fd = unsafe { BorrowedFd::borrow_raw(0) };
if let Ok(mut termios) = tcgetattr(stdin_fd) {
cfmakeraw(&mut termios);
let _ = tcsetattr(stdin_fd, SetArg::TCSANOW, &termios);
}
proxy_serial_pty(&pty.master, child_pid);
match child.wait() {
Ok(status) => {
tracing::debug!(?status, "shell exited");
}
Err(e) if e.raw_os_error() == Some(libc::ECHILD) => {}
Err(e) => {
tracing::warn!(err = %e, "ktstr-init: wait for shell failed");
}
}
}
fn proxy_serial_pty(master: &OwnedFd, child_pid: u32) {
let stdin_fd = unsafe { BorrowedFd::borrow_raw(0) };
let stdout_fd = unsafe { BorrowedFd::borrow_raw(1) };
let master_fd = master.as_fd();
let mut buf = [0u8; 4096];
loop {
let mut pollfds = [
PollFd::new(stdin_fd, PollFlags::POLLIN),
PollFd::new(master_fd, PollFlags::POLLIN),
];
match poll(&mut pollfds, PollTimeout::from(200u16)) {
Ok(0) => {
if !Path::new(&format!("/proc/{child_pid}")).exists() {
break;
}
continue;
}
Ok(_) => {}
Err(nix::errno::Errno::EINTR) => continue,
Err(_) => break,
}
if let Some(revents) = pollfds[0].revents() {
if revents.contains(PollFlags::POLLIN) {
match nix::unistd::read(stdin_fd, &mut buf) {
Ok(0) => break,
Ok(n) => {
let _ = nix::unistd::write(master_fd, &buf[..n]);
}
Err(nix::errno::Errno::EINTR) => {}
Err(_) => break,
}
}
if revents.intersects(PollFlags::POLLERR | PollFlags::POLLHUP) {
break;
}
}
if let Some(revents) = pollfds[1].revents() {
if revents.intersects(PollFlags::POLLERR | PollFlags::POLLHUP) {
break;
}
if revents.contains(PollFlags::POLLIN) {
match nix::unistd::read(master_fd, &mut buf) {
Ok(0) => break,
Ok(n) => {
let _ = nix::unistd::write(stdout_fd, &buf[..n]);
}
Err(nix::errno::Errno::EINTR) => {}
Err(_) => break,
}
}
}
}
}