Summary
BytesToStringMB (pkg/unikontainers/hypervisors/utils.go) is intended to guard against invalid or excessively small memory requests by falling back to DefaultMemory (256MB). In practice, the fallback only occurs when the converted value truncates to 0 MB (i.e. inputs smaller than 1MB). Any value of 1MB or greater is passed directly to the hypervisor without any lower-bound validation, even if it is insufficient for the guest to boot.
This behavior is shared across all hypervisor backends that use BytesToStringMB (qemu, hvt, spt, and cloud-hypervisor). firecracker does not call this helper, but contains the same pattern locally (fcMem == 0).
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 {
stringMem := strconv.FormatUint(DefaultMemory, 10)
if argMem != 0 {
userMem := bytesToMB(argMem)
// Check for too low memory
if userMem == 0 {
userMem = DefaultMemory
}
stringMem = strconv.FormatUint(userMem, 10)
}
return stringMem
}
The comment (// Check for too low memory) suggests this was intended as a minimum-memory guard. However, the implementation only catches the integer-truncation case rather than enforcing an actual lower bound.
Reachability
unikontainers.go forwards the OCI memory limit directly:
vmmArgs.MemSizeB = uint64(*u.Spec.Linux.Resources.Memory.Limit)
Neither Docker, containerd, nor Kubernetes enforces a runtime-specific minimum memory limit before this value reaches urunc. As a result, values such as 2Mi are accepted and eventually reach BytesToStringMB.
Reproduction
Parser behavior
| Input |
Output |
Behavior |
| 500,000 bytes |
256 |
Falls back |
| 999,000 bytes |
256 |
Falls back |
| 1,000,000 bytes |
1 |
Passed through |
| 2,000,000 bytes |
2 |
Passed through |
| 4,000,000 bytes |
4 |
Passed through |
Empirical boot floor
Using the redis-qemu-unikraft-initrd image, I performed a binary search over guest memory values while checking for "Ready to accept connections" within a 3-second serial output window.
| Memory (MB) |
Result |
| 130 |
Success |
| 98 |
Success |
| 82 |
Success |
| 78 |
Success |
| 76 |
Success |
| 75 |
Failed |
| 74 |
Failed |
| 66 |
Failed |
The measured boot floor for this image was 76MB.
At 75MB and below, the guest panics during boot:
Cannot handle palloc request of order 6: Out of memory
main returned 11, halting system
At 76MB and above, Redis starts successfully.
Note: This measurement was taken before fixing the unit-conversion issue described in #818 . Since QEMU currently interprets -m 76M as 76 MiB, the exact threshold should be revalidated once that issue is resolved.
Existing test coverage
utils_test.go contains tests for bytesToMB and bytesToMiB, but there are currently no tests covering BytesToStringMB's fallback behavior or minimum-memory validation.
Impact
Memory values between 1MB and the actual minimum boot requirement are accepted without validation and eventually cause guest boot failures. This includes realistic configuration mistakes such as specifying 2Mi or 4Mi, resulting in a guest panic instead of an earlier fallback or validation error.
Suggested fix
Short term
Replace the truncation-based check with an explicit minimum:
const MinMemoryMB uint64 = 128
if userMem < MinMemoryMB {
userMem = DefaultMemory
// or return an error
}
The measured Redis boot floor is approximately 76MB, but a larger default provides headroom for real workloads.
Longer term
Instead of a runtime-wide constant, consider extending UnikernelConfig (for example via an image annotation such as com.urunc.unikernel.minMemory) so each unikernel image can declare its own minimum supported memory.
Related issues
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
Binary search script
import subprocess
import time
def test_boot(mem_mb):
cmd = [
"qemu-system-x86_64", "-cpu", "host", "-enable-kvm",
"-m", str(mem_mb),
"-kernel", "dist/unikernel/redis_unikraft_kvm_x86_64.qmu",
"-initrd", "dist/unikernel/initrd",
"-display", "none", "-vga", "none",
"-serial", "stdio", "-monitor", "null"
]
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
start = time.time()
while time.time() - start < 3:
if p.poll() is not None:
break
time.sleep(0.1)
try:
p.terminate()
out, _ = p.communicate(timeout=1)
except Exception:
p.kill()
out, _ = p.communicate()
return "Ready to accept connections" in out
low, high, best = 4, 256, None
while low <= high:
mid = (low + high) // 2
if test_boot(mid):
best = mid
high = mid - 1
else:
low = mid + 1
print(best)
Summary
BytesToStringMB(pkg/unikontainers/hypervisors/utils.go) is intended to guard against invalid or excessively small memory requests by falling back toDefaultMemory(256MB). In practice, the fallback only occurs when the converted value truncates to 0 MB (i.e. inputs smaller than 1MB). Any value of 1MB or greater is passed directly to the hypervisor without any lower-bound validation, even if it is insufficient for the guest to boot.This behavior is shared across all hypervisor backends that use
BytesToStringMB(qemu,hvt,spt, andcloud-hypervisor).firecrackerdoes not call this helper, but contains the same pattern locally (fcMem == 0).Root cause
In
pkg/unikontainers/hypervisors/utils.go:The comment (
// Check for too low memory) suggests this was intended as a minimum-memory guard. However, the implementation only catches the integer-truncation case rather than enforcing an actual lower bound.Reachability
unikontainers.goforwards the OCI memory limit directly:Neither Docker, containerd, nor Kubernetes enforces a runtime-specific minimum memory limit before this value reaches
urunc. As a result, values such as2Miare accepted and eventually reachBytesToStringMB.Reproduction
Parser behavior
Empirical boot floor
Using the
redis-qemu-unikraft-initrdimage, I performed a binary search over guest memory values while checking for"Ready to accept connections"within a 3-second serial output window.The measured boot floor for this image was 76MB.
At 75MB and below, the guest panics during boot:
At 76MB and above, Redis starts successfully.
Existing test coverage
utils_test.gocontains tests forbytesToMBandbytesToMiB, but there are currently no tests coveringBytesToStringMB's fallback behavior or minimum-memory validation.Impact
Memory values between 1MB and the actual minimum boot requirement are accepted without validation and eventually cause guest boot failures. This includes realistic configuration mistakes such as specifying
2Mior4Mi, resulting in a guest panic instead of an earlier fallback or validation error.Suggested fix
Short term
Replace the truncation-based check with an explicit minimum:
The measured Redis boot floor is approximately 76MB, but a larger default provides headroom for real workloads.
Longer term
Instead of a runtime-wide constant, consider extending
UnikernelConfig(for example via an image annotation such ascom.urunc.unikernel.minMemory) so each unikernel image can declare its own minimum supported memory.Related issues
BytesToStringMB; the measured boot floor should be revalidated after that issue is fixed.Environment
2785749875a1b13f543b6ad4137c68903a9ec87e8.2.2(1:8.2.2+ds-0ubuntu1.17)harbor.nbfc.io/nubificus/urunc/redis-qemu-unikraft-initrd:latestBinary search script