mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
vmm: buffer socket serial output for late-connecting clients
In Socket serial mode the device output sink was only installed once a client connected, so output produced beforehand (kernel boot messages, cloud-init) was dropped, and a client attaching after boot saw a blank screen. Only PTY mode wrapped the sink in a SerialBuffer. Install a persistent SerialBuffer as the Socket device's output sink at SerialManager construction (discarding downstream via io::sink() until a client connects), so output is captured into the 1 MiB ring even with no client attached. On connect, retarget the buffer at the accepted client and flush the backlog before live output resumes; on disconnect, keep buffering so output produced while no client is attached is delivered to the next one. The accepted socket is made non-blocking via set_nonblocking() so a slow client cannot stall the vCPU thread (SerialBuffer re-buffers on WouldBlock). The serial-manager thread gains two syscalls under seccomp: sendto (replaying the backlog is the first time it writes to the socket) and ioctl restricted to FIONBIO, which is what set_nonblocking() issues. Fixes: #7907 Signed-off-by: Max Makarov <maxpain@linux.com> Assisted-by: Claude:claude-opus-4-8 [Claude Code]
This commit is contained in:
committed by
Rob Bradford
parent
3955fdd22a
commit
9889f6d403
@@ -544,6 +544,10 @@ fn create_api_ioctl_seccomp_rule() -> Result<Vec<SeccompRule>, BackendError> {
|
|||||||
Ok(or![and![Cond::new(1, ArgLen::Dword, Eq, FIONBIO as _)?]])
|
Ok(or![and![Cond::new(1, ArgLen::Dword, Eq, FIONBIO as _)?]])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_serial_manager_ioctl_seccomp_rule() -> Result<Vec<SeccompRule>, BackendError> {
|
||||||
|
Ok(or![and![Cond::new(1, ArgLen::Dword, Eq, FIONBIO as _)?]])
|
||||||
|
}
|
||||||
|
|
||||||
fn create_signal_handler_ioctl_seccomp_rule() -> Result<Vec<SeccompRule>, BackendError> {
|
fn create_signal_handler_ioctl_seccomp_rule() -> Result<Vec<SeccompRule>, BackendError> {
|
||||||
Ok(or![
|
Ok(or![
|
||||||
and![Cond::new(1, ArgLen::Dword, Eq, TCGETS as _)?],
|
and![Cond::new(1, ArgLen::Dword, Eq, TCGETS as _)?],
|
||||||
@@ -1046,6 +1050,7 @@ fn serial_manager_thread_rules() -> Result<Vec<(i64, Vec<SeccompRule>)>, Backend
|
|||||||
(libc::SYS_exit, vec![]),
|
(libc::SYS_exit, vec![]),
|
||||||
(libc::SYS_fcntl, vec![]),
|
(libc::SYS_fcntl, vec![]),
|
||||||
(libc::SYS_futex, vec![]),
|
(libc::SYS_futex, vec![]),
|
||||||
|
(libc::SYS_ioctl, create_serial_manager_ioctl_seccomp_rule()?),
|
||||||
(libc::SYS_madvise, vec![]),
|
(libc::SYS_madvise, vec![]),
|
||||||
(libc::SYS_mmap, vec![]),
|
(libc::SYS_mmap, vec![]),
|
||||||
(libc::SYS_munmap, vec![]),
|
(libc::SYS_munmap, vec![]),
|
||||||
@@ -1054,6 +1059,7 @@ fn serial_manager_thread_rules() -> Result<Vec<(i64, Vec<SeccompRule>)>, Backend
|
|||||||
(libc::SYS_recvfrom, vec![]),
|
(libc::SYS_recvfrom, vec![]),
|
||||||
(libc::SYS_rt_sigprocmask, vec![]),
|
(libc::SYS_rt_sigprocmask, vec![]),
|
||||||
(libc::SYS_rt_sigreturn, vec![]),
|
(libc::SYS_rt_sigreturn, vec![]),
|
||||||
|
(libc::SYS_sendto, vec![]),
|
||||||
(libc::SYS_shutdown, vec![]),
|
(libc::SYS_shutdown, vec![]),
|
||||||
(libc::SYS_sigaltstack, vec![]),
|
(libc::SYS_sigaltstack, vec![]),
|
||||||
(libc::SYS_write, vec![]),
|
(libc::SYS_write, vec![]),
|
||||||
|
|||||||
+103
-23
@@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::{Read, Write};
|
||||||
use std::net::Shutdown;
|
use std::net::Shutdown;
|
||||||
use std::os::fd::OwnedFd;
|
use std::os::fd::OwnedFd;
|
||||||
use std::os::unix::io::{AsRawFd, FromRawFd};
|
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||||
@@ -129,7 +129,44 @@ pub struct SerialManager {
|
|||||||
kill_evt: EventFd,
|
kill_evt: EventFd,
|
||||||
handle: Option<thread::JoinHandle<()>>,
|
handle: Option<thread::JoinHandle<()>>,
|
||||||
pty_write_out: Option<Arc<AtomicBool>>,
|
pty_write_out: Option<Arc<AtomicBool>>,
|
||||||
socket_path: Option<PathBuf>,
|
socket_console: Option<SocketConsole>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `Write` handle so the serial device and the serial-manager thread can
|
||||||
|
/// share one `SerialBuffer` (the device holds it as its output sink).
|
||||||
|
struct SharedSerialBuffer(Arc<Mutex<SerialBuffer>>);
|
||||||
|
|
||||||
|
impl Write for SharedSerialBuffer {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.0.lock().unwrap().write(buf)
|
||||||
|
}
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.0.lock().unwrap().flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SocketConsole {
|
||||||
|
path: PathBuf,
|
||||||
|
/// Persistent ring buffer: captures serial output while no client is
|
||||||
|
/// connected and replays it on connect. Shared with the serial device's
|
||||||
|
/// `out` sink and retargeted by the epoll thread.
|
||||||
|
buffer: Arc<Mutex<SerialBuffer>>,
|
||||||
|
write_out: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SocketConsole {
|
||||||
|
fn attach_client(&self, writer: UnixStream) -> Result<()> {
|
||||||
|
let mut buffer = self.buffer.lock().unwrap();
|
||||||
|
buffer.set_out(Box::new(writer));
|
||||||
|
self.write_out.store(true, Ordering::Release);
|
||||||
|
buffer.flush().map_err(Error::FlushOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_client(&self) {
|
||||||
|
self.write_out.store(false, Ordering::Release);
|
||||||
|
self.buffer.lock().unwrap().set_out(Box::new(io::sink()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SerialManager {
|
impl SerialManager {
|
||||||
@@ -219,6 +256,29 @@ impl SerialManager {
|
|||||||
.set_out(Some(Box::new(buffer)));
|
.set_out(Some(Box::new(buffer)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Install the persistent buffer as the device's sink so output produced
|
||||||
|
// before the first client connects is captured rather than dropped.
|
||||||
|
let mut socket_console = None;
|
||||||
|
if let ConsoleTransport::Socket(_) = transport
|
||||||
|
&& let Some(path) = socket_path
|
||||||
|
{
|
||||||
|
let write_out = Arc::new(AtomicBool::new(false));
|
||||||
|
let buffer = Arc::new(Mutex::new(SerialBuffer::new(
|
||||||
|
Box::new(io::sink()),
|
||||||
|
write_out.clone(),
|
||||||
|
)));
|
||||||
|
serial
|
||||||
|
.as_ref()
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.set_out(Some(Box::new(SharedSerialBuffer(buffer.clone()))));
|
||||||
|
socket_console = Some(SocketConsole {
|
||||||
|
path,
|
||||||
|
buffer,
|
||||||
|
write_out,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Use 'OwnedFd' to manage lifetime
|
// Use 'OwnedFd' to manage lifetime
|
||||||
// SAFETY: epoll_fd is valid
|
// SAFETY: epoll_fd is valid
|
||||||
let epoll_fd = unsafe { OwnedFd::from_raw_fd(epoll_fd) };
|
let epoll_fd = unsafe { OwnedFd::from_raw_fd(epoll_fd) };
|
||||||
@@ -230,7 +290,7 @@ impl SerialManager {
|
|||||||
kill_evt,
|
kill_evt,
|
||||||
handle: None,
|
handle: None,
|
||||||
pty_write_out,
|
pty_write_out,
|
||||||
socket_path,
|
socket_console,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +338,7 @@ impl SerialManager {
|
|||||||
let transport = self.transport.clone();
|
let transport = self.transport.clone();
|
||||||
let serial = self.serial.clone();
|
let serial = self.serial.clone();
|
||||||
let pty_write_out = self.pty_write_out.clone();
|
let pty_write_out = self.pty_write_out.clone();
|
||||||
|
let socket_console = self.socket_console.clone();
|
||||||
let mut reader: Option<UnixStream> = None;
|
let mut reader: Option<UnixStream> = None;
|
||||||
|
|
||||||
// In case of PTY, we want to be able to detect a connection on the
|
// In case of PTY, we want to be able to detect a connection on the
|
||||||
@@ -352,6 +413,11 @@ impl SerialManager {
|
|||||||
// Accept them, create a reader and a writer.
|
// Accept them, create a reader and a writer.
|
||||||
let (unix_stream, _) =
|
let (unix_stream, _) =
|
||||||
listener.accept().map_err(Error::AcceptConnection)?;
|
listener.accept().map_err(Error::AcceptConnection)?;
|
||||||
|
// Non-blocking so a slow client can't stall
|
||||||
|
// the vCPU (SerialBuffer re-buffers on WouldBlock).
|
||||||
|
unix_stream
|
||||||
|
.set_nonblocking(true)
|
||||||
|
.map_err(Error::SetNonBlocking)?;
|
||||||
let writer =
|
let writer =
|
||||||
unix_stream.try_clone().map_err(Error::CloneUnixStream)?;
|
unix_stream.try_clone().map_err(Error::CloneUnixStream)?;
|
||||||
|
|
||||||
@@ -367,7 +433,10 @@ impl SerialManager {
|
|||||||
.map_err(Error::Epoll)?;
|
.map_err(Error::Epoll)?;
|
||||||
|
|
||||||
reader = Some(unix_stream);
|
reader = Some(unix_stream);
|
||||||
serial.lock().unwrap().set_out(Some(Box::new(writer)));
|
|
||||||
|
if let Some(ref socket_console) = socket_console {
|
||||||
|
socket_console.attach_client(writer)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EpollDispatch::File => {
|
EpollDispatch::File => {
|
||||||
if event.events & libc::EPOLLIN as u32 != 0 {
|
if event.events & libc::EPOLLIN as u32 != 0 {
|
||||||
@@ -375,22 +444,35 @@ impl SerialManager {
|
|||||||
let count = match &transport {
|
let count = match &transport {
|
||||||
ConsoleTransport::Socket(_) => {
|
ConsoleTransport::Socket(_) => {
|
||||||
if let Some(mut serial_reader) = reader.as_ref() {
|
if let Some(mut serial_reader) = reader.as_ref() {
|
||||||
let count = serial_reader
|
match serial_reader.read(&mut input) {
|
||||||
.read(&mut input)
|
Ok(0) => {
|
||||||
.map_err(Error::ReadInput)?;
|
info!(
|
||||||
if count == 0 {
|
"Remote end closed serial socket"
|
||||||
info!("Remote end closed serial socket");
|
);
|
||||||
serial_reader
|
serial_reader
|
||||||
.shutdown(Shutdown::Both)
|
.shutdown(Shutdown::Both)
|
||||||
.map_err(Error::ShutdownConnection)?;
|
.map_err(
|
||||||
reader = None;
|
Error::ShutdownConnection,
|
||||||
serial
|
)?;
|
||||||
.as_ref()
|
reader = None;
|
||||||
.lock()
|
if let Some(ref socket_console) =
|
||||||
.unwrap()
|
socket_console
|
||||||
.set_out(None);
|
{
|
||||||
|
socket_console.detach_client();
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Ok(count) => count,
|
||||||
|
// Non-blocking socket with no pending
|
||||||
|
// input on this wakeup: nothing to do.
|
||||||
|
Err(e)
|
||||||
|
if e.kind()
|
||||||
|
== io::ErrorKind::WouldBlock =>
|
||||||
|
{
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Err(e) => return Err(Error::ReadInput(e)),
|
||||||
}
|
}
|
||||||
count
|
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -455,10 +537,8 @@ impl Drop for SerialManager {
|
|||||||
if let Some(handle) = self.handle.take() {
|
if let Some(handle) = self.handle.take() {
|
||||||
handle.join().ok();
|
handle.join().ok();
|
||||||
}
|
}
|
||||||
if let ConsoleTransport::Socket(_) = self.transport
|
if let Some(socket_console) = self.socket_console.as_ref() {
|
||||||
&& let Some(socket_path) = self.socket_path.as_ref()
|
std::fs::remove_file(&socket_console.path)
|
||||||
{
|
|
||||||
std::fs::remove_file(socket_path.as_os_str())
|
|
||||||
.map_err(Error::RemoveUnixSocket)
|
.map_err(Error::RemoveUnixSocket)
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user