From d90aac80c718e405e8e2be373781d99e29a5b3fe Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Thu, 7 May 2026 21:21:00 +0000 Subject: [PATCH] qemu: open serial log with O_APPEND via chardev Use -chardev file,append=on for the serial console instead of -serial file:. The latter opens the path with plain O_WRONLY|O_CREAT, so QEMU writes at its internal fd offset. When a copytruncate-style log rotation truncates the file out from under it, the next write lands at the stale offset, leaving a sparse hole of NUL bytes from byte 0 onward. With append=on QEMU uses O_APPEND, so every write atomically seeks to EOF and post-truncate writes correctly resume at byte 0. --- lib/hypervisor/qemu/config.go | 11 +++++++++-- lib/hypervisor/qemu/config_test.go | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/hypervisor/qemu/config.go b/lib/hypervisor/qemu/config.go index 4b395c40..7c65af29 100644 --- a/lib/hypervisor/qemu/config.go +++ b/lib/hypervisor/qemu/config.go @@ -100,9 +100,16 @@ func BuildArgs(cfg hypervisor.VMConfig) []string { args = append(args, "-device", deviceArg) } - // Serial console output to file + // Serial console output to file. Use a chardev with append=on so QEMU + // opens the file with O_APPEND. Without it, QEMU writes at its internal + // fd offset; if the file is externally truncated (e.g. log rotation via + // copytruncate) subsequent writes leave a sparse hole of NUL bytes from + // byte 0 to the stale offset, which downstream log readers will pick up. if cfg.SerialLogPath != "" { - args = append(args, "-serial", fmt.Sprintf("file:%s", cfg.SerialLogPath)) + args = append(args, + "-chardev", fmt.Sprintf("file,id=serial0,path=%s,append=on", cfg.SerialLogPath), + "-serial", "chardev:serial0", + ) } else { args = append(args, "-serial", "stdio") } diff --git a/lib/hypervisor/qemu/config_test.go b/lib/hypervisor/qemu/config_test.go index 1cb64c93..f4a3e452 100644 --- a/lib/hypervisor/qemu/config_test.go +++ b/lib/hypervisor/qemu/config_test.go @@ -144,8 +144,10 @@ func TestBuildArgs_SerialLog(t *testing.T) { args := BuildArgs(cfg) + assert.Contains(t, args, "-chardev") + assert.Contains(t, args, "file,id=serial0,path=/var/log/app.log,append=on") assert.Contains(t, args, "-serial") - assert.Contains(t, args, "file:/var/log/app.log") + assert.Contains(t, args, "chardev:serial0") } func TestBuildArgs_NoSerialLog(t *testing.T) {