Skip to content

Fix port IO in fw_cfg device for x86 - #190

Merged
scholzp merged 8 commits into
cyberus-technology:gardenlinuxfrom
scholzp:fw_cfg_fix_pio
Aug 25, 2026
Merged

Fix port IO in fw_cfg device for x86#190
scholzp merged 8 commits into
cyberus-technology:gardenlinuxfrom
scholzp:fw_cfg_fix_pio

Conversation

@scholzp

@scholzp scholzp commented Aug 4, 2026

Copy link
Copy Markdown

This PR is the starting ground for the fw_cfg rework in CHV by reworking the port IO transportation path. The current implementation has many issues such as panicking when reading beyond item lengths. Moreover, the current implementation doesn't follow QEMU semantics, which is addressed in this PR. The next followup will introduce compatibility for well-known legacy items. After that, we can safely merge the bootorder feature, as the fw_cfg device will than semantically act similar to the QEMU implementation from the guest's perspective with a reduced set of featues.

The DMA path is broken. Therefore it is deactivated in this patch series. For our fork we decided to use the Port IO interface only. This doesn't has any implication to the bootorder feature.

We reject building for aarch64, because the MMIO mapped register based transportation layer for aarch64 is broken and doesn't follow QEMU semantics either. This, similar to other cleanup, is left for followups. The entire rework of fw_cfg will target upstream and we can sooner or later replace this version with the upstream one. A list of all defects can be found here: https://github.com/cobaltcore-dev/cobaltcore/issues/641

This is also includes the fix for the DATA register read handling, which accepts arbitrary length at the moment. This needs to be fixed in kvm_ioctls crate and then globally in CHV.

I tested locally that the bootorder feature still works. This commit series contains a commit that activates the fw_cfg feature. I'll remove it once the pipeline finished. It's only purpose is to run the pipeline with fw_cfg activated. Find a pipeline here: https://gitlab.cyberus-technology.de/cyberus/cloud/libvirt/-/merge_requests/268

@scholzp
scholzp force-pushed the fw_cfg_fix_pio branch 3 times, most recently from f2df444 to 2a7766b Compare August 5, 2026 13:39
@arctic-alpaca

Copy link
Copy Markdown

The entire rework of fw_cfg will target upstream and we can sooner or later replace this version with the upstream one.

I'm unsure what that means, could you elaborate on what the current state of upstream is and how we get to using upstream?

@scholzp

scholzp commented Aug 6, 2026

Copy link
Copy Markdown
Author

The entire rework of fw_cfg will target upstream and we can sooner or later replace this version with the upstream one.

I'm unsure what that means, could you elaborate on what the current state of upstream is and how we get to using upstream?

Good call!
I found a list of defects in the current implementation that would all need to be gradually fixed. You can find them here: https://github.com/cobaltcore-dev/cobaltcore/issues/641

@amphi amphi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really nice commit history, thanks!

Comment thread devices/src/legacy/fw_cfg.rs
Comment thread devices/src/legacy/fw_cfg.rs Outdated
error!("fw_cfg: selector register is write-only.");
}
(PORT_FW_CFG_DATA, _) => _ = self.read_data(data, size as u32),
(PORT_FW_CFG_DATA, 1) => _ = self.read_data(data, size as u32),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there should be a case for (PORT_FW_CFG_DATA, _)? Otherwise we will see read from unknown port, which is not really true as the port is known.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right but also this is solved by a later commit.

Comment thread devices/src/legacy/fw_cfg.rs
Comment thread devices/src/legacy/fw_cfg.rs Outdated
Comment on lines +794 to +799
_ => {
debug!(
"fw_cfg: read from unknown port {port:#x}: {size:#x} bytes and offset {offset:#x}."
"fw_cfg: Unsupported {:#x}-byte read from port {port:#x}.",
data.len()
);
data.fill(0x0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not really checking whether this is a read to a valid port with an incorrect width. This is the catch-all for "something is wrong". Is this really the intended behavior?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. I now differ between reads of unsupported sizes from valid ports and reads from unknown ports.

Comment thread devices/src/legacy/fw_cfg.rs
Comment thread devices/src/legacy/fw_cfg.rs Outdated

#[derive(Error, Debug)]
pub enum FwCfgError {
#[error("Reading the source (mostly a host file) failed.")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does mostly a host file mean here? Does it mean that the source is most likely a host file? If yes, I don't think this should be part of the error message.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

IllegalSelector,
#[error("The cursor already points to the item'e end")]
CursorBehindContent,
#[error("The accessed item is too large")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The accessed item is too large

should be

The accessed item is to large

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too large

is correct.

@amphi amphi Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always mix this up, I hate it. But I guess then the error should be TooLarge?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be solved. :D

@arctic-alpaca arctic-alpaca Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error variant below where this comment points to is ToLarge, which should be TooLarge.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Thanks for pointing out again!

@arctic-alpaca arctic-alpaca left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice commit history indeed 👍

Comment thread devices/src/legacy/fw_cfg.rs
Comment thread devices/src/legacy/fw_cfg.rs
Comment thread devices/src/legacy/fw_cfg.rs Outdated
Comment thread devices/src/legacy/fw_cfg.rs Outdated
Comment thread devices/src/legacy/fw_cfg.rs Outdated
Comment thread devices/src/legacy/fw_cfg.rs Outdated
Comment thread devices/src/legacy/fw_cfg.rs Outdated
@scholzp
scholzp force-pushed the fw_cfg_fix_pio branch 3 times, most recently from e719416 to 09e43f2 Compare August 7, 2026 16:35
Comment thread devices/src/legacy/fw_cfg.rs
Comment thread devices/src/legacy/fw_cfg.rs Outdated
Comment thread devices/src/legacy/fw_cfg.rs Outdated
Comment thread devices/src/legacy/fw_cfg.rs Outdated
@scholzp
scholzp force-pushed the fw_cfg_fix_pio branch 2 times, most recently from ad279ca to c0804c9 Compare August 19, 2026 11:55

@arctic-alpaca arctic-alpaca left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@scholzp

scholzp commented Aug 19, 2026

Copy link
Copy Markdown
Author

I issued a new pipeline run with the patches contained in this PR.

I'll remove the commit enabling the fw_cfg feature now so the remaining CI steps can be run.

@amphi amphi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one last issue.

Comment on lines +780 to 804
fn read_content(&mut self, data: &mut [u8]) -> FwCfgContentAccessResult<u32> {
let content_size = self
.get_selected_content()?
.size()
.map_err(|_| FwCfgContentAccessError::TooLarge)?;

let remaining_content_bytes = content_size.saturating_sub(self.data_offset);
let content_bytes_to_copy = u32::min(remaining_content_bytes, data.len() as u32);
let planned_end = self.data_offset + content_bytes_to_copy;
let read_size = self
.get_selected_content()?
.access(self.data_offset)
.read(data[..content_bytes_to_copy as usize].as_mut_bytes())
.map_err(|_| FwCfgContentAccessError::ReadError)?;

// Only relevant for file backed items. These can change between
// access so the data used to calculate can be stale. We cannot fix this.
if read_size != content_bytes_to_copy as usize {
return Err(FwCfgContentAccessError::ReadError);
}
Some(size as u8)

self.data_offset = planned_end;

Ok(content_bytes_to_copy)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You changed semantics quite a bit here.

You now calculate the content size on every access, using an unchecked sub (in FwCfgContent::size). You already acknowledged that the file may change between accesses, so if the file is truncated (what happens after add_kernel_data), the size calculation could underflow, leading to more errors later. You should at least use a checked_sub in FwCfgContent::size). Otherwise this could panic during runtime I think.

Also you are now using seek() followed by read() when you read a file-backed item. AFAIK this can break when concurrent reads happen, because the cursor is shared. You can fix that by using read_exact_at like the old code did.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

You are totally right. I trusted this code to much. It was originally used for the DMA access path so I blindly reused it and threw away code in the POI path that did the same but in a save way. I integrated the save implementation into the FwCfgContentAccess adapter to get the best out of both worlds.

@scholzp
scholzp force-pushed the fw_cfg_fix_pio branch 4 times, most recently from b2e047d to c762a39 Compare August 24, 2026 18:39
@scholzp scholzp mentioned this pull request Aug 24, 2026
Comment on lines 801 to 819
(offset, _) if qemu_mapped_offsets.any(|mapped_offset| mapped_offset == offset) => {
// We read from a port that should actually be mapped to fw_cfg. Note that QEMU
// doesn't map the entire range but leaves a hole at 0x512 and 0x513. We mimic this
// by doing a no-op below for this range.
debug!(
"fw_cfg: Unsupported {:#x}-byte read from address: base={:#x} + offset={:#x}.",
data.len(),
PORT_FW_CFG_BASE,
offset
);

data.fill(0x0);
}
(offset, _) => {
// We read from a port that shouldn't be mapped to fw_cfg and do nothing but warn.
debug!(
"fw_cfg: read to unmapped address: base={PORT_FW_CFG_BASE:#x} + offset={offset:#x}. Read length: {size}. This is a wrong mapping and a bug!"
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if I am reading this correctly, but the lower arm triggers for e.g. offset 0x512 and 0x513, correct? If yes, then I think you should still fill data with zeroes. Otherwise you just return whatever bytes were already there, not necessarily zeroes.

If no, then ignore this comment.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right! There are inconsistencies betweeen the code and the commit message. I fixed the commit message to clarify that we treat port 0x512 and 0x513 as unmapped. In CHV context this means leaving the buffer unchanged for reads. In QEMU context, the buffer would be filled with all-ones. And on real hardware this is simply UB.

We obviously go the CHV way.

The DMA interface in `fw_cfg` in the CHV implementation is broken and
needs an overhaul. We deactivate it to force a guest to use the
traditional interface instead and ignore the DMA interface.[0]

We make DMA transfers no-ops for now and adjust the test to verify this
instead until we do a rework of the DMA path.

[0] https://www.qemu.org/docs/master/specs/fw_cfg.html#guest-side-dma-interface

On-behalf-of: SAP pascal.scholz@sap.com
Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
The selector 0x0000 has to return the bytes "QEMU" in the traditional
interface.[0] Each additional read beyond those four bytes should
return 0x0. The previous implementation exposed the complete DMA
signature if a guest reads more than four bytes from the selector
0x0000 instead.

We fix this by treating each signature separately.

[0] https://www.qemu.org/docs/master/specs/fw_cfg.html#signature-key-0x0000-fw-cfg-signature

On-behalf-of: SAP pascal.scholz@sap.com
Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
In the traditional/PIO interface the DATA register has a width of one
byte on x86.[0]
We therefore do not allow reads with larger widths. Currently, all
tests assume a read with a width of one byte too.

We reject `fw_cfg` in aarch64 builds for now as these changes introduce
an incompatibility that adds to the incomplete implementation of it.
aarch64 support is a task to be solved in follow-up work, as this also
includes making corrections to the MMIO transport implementation and
FDT corrections.

[0] https://www.qemu.org/docs/master/specs/fw_cfg.html#data-register

On-behalf-of: SAP pascal.scholz@sap.com
Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
QEMU checks if the read has an allowed width.[0] If it detects an
invalid read to a device mapped port, then QEMU will treat the
register reads as a read from unassigned I/O.[1] As a result, it
will return 0x0 for the whole buffer. For unmapped IO port access,
QEMU writes all-ones to the buffer. Unlike QEMU, CHV doesn't answer
unmapped reads with a deterministic value and leaves the buffer
unchanged instead. We preserve CHV behavior for ports unknown to
fw_cfg.

QEMU maps two separate memory regions to fw_cfg. One starting
at FW_CFG_IO_BASE which is 0x510 and one starting at 0x514.[2] The first
is two bytes, the size of the latter is eight bytes. This leaves a hole
at addresses 0x512 and 0x513 in the x86 I/O port mapping.

We mimic this behavior for CHV's `fw_cfg` design for improved
compatibility and respect the hole accordingly. In the last case this
means that reading from the hole at 0x512 and 0x513 is not treated by
zero-filling the buffer. Instead, we tread them as unmapped port and
leave the buffer unchanged.

[0] https://github.com/qemu/qemu/blob/6e9a825c1d4e7b62d072e99a89ecd1a74c7f0d55/hw/nvram/fw_cfg.c#L533
[1] https://github.com/qemu/qemu/blob/6e9a825c1d4e7b62d072e99a89ecd1a74c7f0d55/system/memory.c#L1480
[2] https://github.com/qemu/qemu/blob/6e9a825c1d4e7b62d072e99a89ecd1a74c7f0d55/hw/i386/fw_cfg.c#L130

On-behalf-of: SAP pascal.scholz@sap.com
Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
Using the result of an addition comes with the risk of it overflowing.
In terms of the address matching in `read` and `write` it makes no
difference if we use the provided offset directly instead of adding the
base to it.

On-behalf-of: SAP pascal.scholz@sap.com
Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
While the documentation states that the SELECTOR register is
write-only, QEMU actually allows reading the SELECTOR register. This
is because QEMU uses a contiguous mapping for the SELECTOR and DATA
registers to allow the 16-bit width of the SELECTOR register.[0]
As a consequence, a read from SELECTOR is delegated to the same callback
as a read from DATA. It does not return the SELECTOR value.

We mimic this for maximal compatibility.

[0] https://github.com/qemu/qemu/blob/6e9a825c1d4e7b62d072e99a89ecd1a74c7f0d55/hw/nvram/fw_cfg.c#L539

On-behalf-of: SAP pascal.scholz@sap.com
Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
Currently, the read implementation isn't complete and doesn't handle
some error cases gracefully. We rework it with the aim of maximal
compatibility to QEMU and add tests for it.
Problems of the old implementation include:
 * Reads beyond EOF should yield 0x0.[0] These currently panic.
 * Register reads with invalid SELECTOR should also yield 0x0.[1]
 * If provided with a buffer larger than an item, then the remaining
   buffer bytes should be set to zero.[1]

We use the existing interface for reading items that is also used for
DMA access and bring bug fixes from the POI read interface to it.
These include:
 * Fixing a possible underflow when calculating the offset within a
   file. Such an underflow can occur if the file is shrunk after
   the last successful access, placing the cursor behind the EOF.
 * Replace `seek` + `read` pattern by `read_exact_at` to make
   repeated access save.

[0] https://www.qemu.org/docs/master/specs/fw_cfg.html#data-register
[1] https://github.com/qemu/qemu/blob/6e9a825c1d4e7b62d072e99a89ecd1a74c7f0d55/hw/nvram/fw_cfg.c#L382

On-behalf-of: SAP pascal.scholz@sap.com
Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
This is a fix to allow string reads that map to `rep ins`. KVM returns
an I/O exit with the respective port, the read size of the instruction
and the count of such reads. `kvm-ioctls` creates a buffer from this
with the size of `count * size` bytes.[0]

This makes it impossible to decide if the buffer was a single
four-byte-width read of the kind `inl` or `rep ins` with RCX set to 4,
for example. While the first would be invalid according to QEMU
semantics, the second is a valid repeated access with one byte width. We
therefore accept reads of any size until we can solve this issue.

[0] https://github.com/rust-vmm/kvm/blob/b4c9ed8df95a9e10a68f50f5ef5e7d04108759ba/kvm-ioctls/src/ioctls/vcpu.rs#L1549

On-behalf-of: SAP pascal.scholz@sap.com
Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
@scholzp
scholzp merged commit 354fa5f into cyberus-technology:gardenlinux Aug 25, 2026
19 checks passed
@phip1611

Copy link
Copy Markdown
Member

I didn't have the capacity to review this - congratulations for merging :)

cyberus-renovate pushed a commit to cyberus-technology/libvirt that referenced this pull request Aug 25, 2026
cloud-hypervisor: 2026-08-25T11:11:34+02:00 -> 2026-08-25T11:27:42+02:00
nixpkgs: 2026-08-18T10:33:34+02:00 -> 2026-08-24T00:57:55+02:00

CH includes fixes for the fw_cfg device [0].

[0] cyberus-technology/cloud-hypervisor#190

This commit was generated via:
nix run github:phip1611/nixos-configs#flake-update-and-commit

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants