Skip to content

Decimal bytes-to-MB conversion causes VM memory allocation inflation, risking host cgroup OOM kills #818

Description

@Nachiket-Roy

Summary

The OCI Runtime Specification defines container memory limits in bytes. When translating this value for most hypervisor backends, BytesToStringMB performs a decimal conversion (bytes / 1,000,000) instead of a binary conversion (bytes / (1024 * 1024)).

Since QEMU, Solo5, and Cloud Hypervisor interpret memory values in MiB, the resulting VM memory can exceed the original OCI byte limit.

Root cause

In pkg/unikontainers/hypervisors/utils.go:

func bytesToMB(bytes uint64) uint64 {
	const bytesInMB = 1000 * 1000
	return bytes / bytesInMB
}

func BytesToStringMB(argMem uint64) string {
	...
	userMem := bytesToMB(argMem)
	...
}

BytesToStringMB is used by the QEMU, Solo5 (hvt/spt), and Cloud Hypervisor backends when constructing their memory arguments.

However, these VMMs interpret memory values as binary MiB, not decimal MB.

Backend behavior

  • QEMU (verified): Passing -m 256M results in exactly 268,435,456 bytes (256 MiB) of guest RAM.
  • Solo5: Documentation indicates --mem is interpreted in MiB.
  • Cloud Hypervisor: Documentation indicates size=<N>M is interpreted in MiB.
  • Firecracker: Not affected, since firecracker.go already uses bytesToMiB (1024 * 1024).

Reproduction

  1. Configure an OCI memory limit of 256,000,000 bytes.
  2. Launch the container using the QEMU backend.
  3. BytesToStringMB converts the value to 256.
  4. urunc invokes QEMU with:
-m 256M
  1. QEMU allocates 256 MiB (268,435,456 bytes) of guest RAM.

This can be verified from the guest boot memory map, where RAM ends at 0x10000000 (268,435,456 bytes).

Impact

For a configured limit of 256,000,000 bytes, the guest receives 268,435,456 bytes, approximately 12.4 MiB (~4.9%) more memory than requested.

As a result, the guest memory configured by the VMM no longer matches the OCI memory limit. Depending on how memory limits are enforced and the workload, this discrepancy may contribute to memory pressure or unexpected OOM behavior.

Suggested fix

Use binary conversion consistently for all hypervisor backends:

const bytesInMiB = 1024 * 1024

func bytesToMiB(bytes uint64) uint64 {
	return bytes / bytesInMiB
}

Then update BytesToStringMB (or rename it accordingly) to use this conversion.

Related issues

Note: DefaultMemory = 256 remains correct after this change, since the supported hypervisors already interpret 256 as 256 MiB.

Environment

  • urunc commit: 2785749875a1b13f543b6ad4137c68903a9ec87e
  • QEMU: 8.2.2 (1:8.2.2+ds-0ubuntu1.17)
  • Test image: harbor.nbfc.io/nubificus/urunc/redis-qemu-unikraft-initrd:latest

Metadata

Metadata

Assignees

No one assigned

    Labels

    MonitorsMonitors and their executiondevInvolves development

    Type

    Fields

    No fields configured for Task.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions