Skip to content

Commit e96bad4

Browse files
committed
fix: Only setup serial device if input is a terminal/FIFO
Currently, whenever firecracker starts with a daemonized jailer, it prints the following warning: "Failed to register serial input fd: event_manager: failed to manage epoll file descriptor: Operation not permitted (os error 1)". This is because if the jailer daemonizes the firecracker process, it will redirect stdin to /dev/null. However, when trying to register FILENO_STDIN to epoll via epoll_ctl, it will return EPERM if such a redirection has happened. However, if the jailer is daemonized, then this will result in the correct behavior, because we do not _want_ a serial device here. The changes in this commit make firecracker only try to initialize the serial device if FILENO_STDIN is a terminal (as reported by isatty(3)) or a FIFO pipe (as reported by fstat). This fixes the incorrect warnings. Signed-off-by: Patrick Roy <[email protected]>
1 parent 9c51dc6 commit e96bad4

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
- Better logs during validation of CPU ID in snapshot restoration path. Also
1313
Firecracker now does not fail if it can't get CPU ID from the host or
1414
can't find CPU ID in the snapshot.
15+
- Changed the serial device to only try to initialize itself if stdin is a terminal
16+
or a FIFO pipe. This fixes logged warnings about the serial device failing to
17+
initialize if the process is daemonized (in which case stdin is /dev/null instead
18+
of a terminal).
1519

1620
### Fixed
1721

src/vmm/src/devices/legacy/serial.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,14 @@ impl<I: Read + AsRawFd + Send + Debug> MutEventSubscriber
265265
if self.input.is_some() && self.serial.events().buffer_ready_event_fd.is_some() {
266266
let serial_fd = self.serial_input_fd();
267267
let buf_ready_evt = self.buffer_ready_evt_fd();
268-
if serial_fd != -1 {
268+
269+
// If the jailer is instructed to daemonize before exec-ing into firecracker, we set
270+
// stdin, stdout and stderr to be open('/dev/null'). However, if stdin is redirected
271+
// from /dev/null then trying to register FILENO_STDIN to epoll will fail with EPERM.
272+
// Therefore, only try to register stdin to epoll if it is a terminal or a FIFO pipe.
273+
// SAFETY: isatty has no invariants that need to be upheld. If serial_fd is an invalid
274+
// argument, it will return 0 and set errno to EBADF.
275+
if unsafe { libc::isatty(serial_fd) } == 1 || is_fifo(serial_fd) {
269276
if let Err(err) = ops.add(Events::new(&serial_fd, EventSet::IN)) {
270277
warn!("Failed to register serial input fd: {}", err);
271278
}
@@ -277,6 +284,24 @@ impl<I: Read + AsRawFd + Send + Debug> MutEventSubscriber
277284
}
278285
}
279286

287+
/// Checks whether the given file descriptor is a FIFO pipe.
288+
fn is_fifo(fd: RawFd) -> bool {
289+
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
290+
291+
// SAFETY: No unsafety can be introduced by passing in an invalid file descriptor to fstat,
292+
// it will return -1 and set errno to EBADF. The pointer passed to fstat is valid for writing
293+
// a libc::stat structure.
294+
if unsafe { libc::fstat(fd, stat.as_mut_ptr()) } < 0 {
295+
return false;
296+
}
297+
298+
// SAFETY: We can safely assume the libc::stat structure to be initialized, as libc::fstat
299+
// returning 0 guarantees that the memory is now initialized with the requested file metadata.
300+
let stat = unsafe { stat.assume_init() };
301+
302+
(stat.st_mode & libc::S_IFIFO) != 0
303+
}
304+
280305
impl<I: Read + AsRawFd + Send + Debug + 'static>
281306
SerialWrapper<EventFdTrigger, SerialEventsWrapper, I>
282307
{
@@ -303,6 +328,8 @@ impl<I: Read + AsRawFd + Send + Debug + 'static>
303328

304329
#[cfg(test)]
305330
mod tests {
331+
#![allow(clippy::undocumented_unsafe_blocks)]
332+
306333
use utils::eventfd::EventFd;
307334

308335
use super::*;
@@ -340,4 +367,23 @@ mod tests {
340367
// The `invalid_read_count` metric should be the same as before the one-byte reads.
341368
assert_eq!(invalid_reads_after_2, invalid_reads_after);
342369
}
370+
371+
#[test]
372+
fn test_is_fifo() {
373+
// invalid file descriptors arent fifos
374+
let invalid = -1;
375+
assert!(!is_fifo(invalid));
376+
377+
// Fifos are fifos
378+
let mut fds: [libc::c_int; 2] = [0; 2];
379+
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
380+
assert!(rc == 0);
381+
382+
assert!(is_fifo(fds[0]));
383+
assert!(is_fifo(fds[1]));
384+
385+
// Files arent fifos
386+
let tmp_file = utils::tempfile::TempFile::new().unwrap();
387+
assert!(!is_fifo(tmp_file.as_file().as_raw_fd()));
388+
}
343389
}

tests/integration_tests/functional/test_serial_io.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,29 @@ def test_serial_block(test_microvm_with_api):
219219

220220
# Should be significantly more than before the `cat` command.
221221
assert last_count - init_count > 10000
222+
223+
224+
REGISTER_FAILED_WARNING = "Failed to register serial input fd: event_manager: failed to manage epoll file descriptor: Operation not permitted (os error 1)"
225+
226+
227+
def test_no_serial_fd_error_when_daemonized(uvm_plain):
228+
"""
229+
Tests that when running firecracker daemonized, the serial device
230+
does not try to register stdin to epoll (which would fail due to stdin no
231+
longer being pointed at a terminal).
232+
233+
Regression test for #4037.
234+
"""
235+
236+
test_microvm = uvm_plain
237+
test_microvm.spawn()
238+
test_microvm.add_net_iface()
239+
test_microvm.basic_config(
240+
vcpu_count=1,
241+
mem_size_mib=512,
242+
)
243+
test_microvm.start()
244+
245+
test_microvm.ssh.run("true")
246+
247+
assert REGISTER_FAILED_WARNING not in test_microvm.log_data

0 commit comments

Comments
 (0)