3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
215 lines
7.2 KiB
Python
215 lines
7.2 KiB
Python
# SPDX-License-Identifier: MIT
|
|
"""Synthetic SMBIOS disguise blob (#246, T1.5) — encode / validate / write."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import winpodx.core.pod.compose as _compose_module
|
|
from winpodx.core.pod.compose import _disguise_smbios_args, _smbios_safe
|
|
from winpodx.core.pod.smbios import build_disguise_smbios_blob, validate_blob
|
|
|
|
|
|
def _walk_types(blob: bytes) -> list[int]:
|
|
types: list[int] = []
|
|
i = 0
|
|
while i < len(blob):
|
|
types.append(blob[i])
|
|
length = blob[i + 1]
|
|
end = blob.find(b"\x00\x00", i + length)
|
|
i = end + 2
|
|
return types
|
|
|
|
|
|
def test_blob_builds_and_validates():
|
|
blob = build_disguise_smbios_blob()
|
|
assert isinstance(blob, bytes) and len(blob) > 0
|
|
validate_blob(blob) # raises on any encoding slip
|
|
|
|
|
|
def test_blob_contains_sensor_descriptor_types():
|
|
# The types al-khaser checks for existence: voltage(26), temperature(28),
|
|
# cooling(27), cache(7), slot(9), port connector(8), physical memory
|
|
# array(16), memory device(17), memory module(6), OEM strings(11).
|
|
types = _walk_types(build_disguise_smbios_blob())
|
|
for t in (26, 28, 27, 7, 9, 8, 16, 17, 6, 11):
|
|
assert t in types, f"missing SMBIOS type {t}"
|
|
|
|
|
|
def test_memory_device_references_array_handle():
|
|
# Type 17's first formatted field is the parent type-16 array handle; a
|
|
# dangling reference would make the WMI memory tree look synthetic.
|
|
from winpodx.core.pod.smbios import _H_MEMARRAY, _H_MEMDEV
|
|
|
|
blob = build_disguise_smbios_blob()
|
|
i = 0
|
|
handles_seen = set()
|
|
array_ref = None
|
|
while i < len(blob):
|
|
stype = blob[i]
|
|
length = blob[i + 1]
|
|
handle = int.from_bytes(blob[i + 2 : i + 4], "little")
|
|
handles_seen.add(handle)
|
|
if stype == 17:
|
|
array_ref = int.from_bytes(blob[i + 4 : i + 6], "little")
|
|
i = blob.find(b"\x00\x00", i + length) + 2
|
|
assert _H_MEMARRAY in handles_seen
|
|
assert _H_MEMDEV in handles_seen
|
|
assert array_ref == _H_MEMARRAY
|
|
|
|
|
|
def test_blob_has_no_vm_marker_strings():
|
|
# A raw SMBIOS string scan must not surface a VM / project marker.
|
|
blob = build_disguise_smbios_blob()
|
|
lowered = blob.lower()
|
|
for marker in (b"qemu", b"bochs", b"seabios", b"winpodx", b"virtual"):
|
|
assert marker not in lowered, f"leaked marker {marker!r} into SMBIOS blob"
|
|
|
|
|
|
def test_validate_rejects_truncated_header():
|
|
with pytest.raises(ValueError):
|
|
validate_blob(b"\x1c") # too short for a 4-byte header
|
|
|
|
|
|
def test_validate_rejects_unterminated_string_set():
|
|
# type 8 header (len 9) + 5 formatted bytes but no double-null terminator.
|
|
bad = bytes([8, 9, 0, 0]) + bytes([1, 0xFF, 2, 0xFF, 0xFF]) + b"J1"
|
|
with pytest.raises(ValueError):
|
|
validate_blob(bad)
|
|
|
|
|
|
def test_write_blob_writes_and_returns_container_path(tmp_path):
|
|
path = _compose_module._write_disguise_smbios_blob(str(tmp_path))
|
|
assert path == "/oem/winpodx-smbios.bin"
|
|
written = tmp_path / "winpodx-smbios.bin"
|
|
assert written.exists()
|
|
validate_blob(written.read_bytes()) # what we wrote is valid
|
|
|
|
|
|
def test_write_blob_failsafe_returns_none_on_bad_dir(tmp_path):
|
|
# A non-existent OEM dir → None, never a dangling `-smbios file=` arg.
|
|
bad = tmp_path / "does" / "not" / "exist"
|
|
assert _compose_module._write_disguise_smbios_blob(str(bad)) is None
|
|
|
|
|
|
def test_blob_has_over_40_structures():
|
|
"""al-khaser's number_SMBIOS_tables() flags a guest whose SMBIOS structure
|
|
count is <= 40; the synthetic blob alone must clear that (QEMU adds ~9 more)."""
|
|
from winpodx.core.pod.smbios import build_disguise_smbios_blob
|
|
|
|
blob = build_disguise_smbios_blob()
|
|
i = 0
|
|
count = 0
|
|
while i < len(blob):
|
|
length = blob[i + 1]
|
|
end = blob.find(b"\x00\x00", i + length)
|
|
i = end + 2
|
|
count += 1
|
|
assert count > 40, f"only {count} synthetic SMBIOS structures (need > 40)"
|
|
|
|
|
|
# --- _smbios_safe unit tests ---
|
|
|
|
|
|
def test_smbios_safe_strips_unsafe_chars():
|
|
result = _smbios_safe("Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz")
|
|
assert result is not None
|
|
assert " " not in result
|
|
assert "," not in result
|
|
assert "=" not in result
|
|
assert '"' not in result
|
|
assert "'" not in result
|
|
|
|
|
|
def test_smbios_safe_no_qemu_string():
|
|
result = _smbios_safe("Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz")
|
|
assert result is not None
|
|
assert "QEMU" not in result
|
|
assert "qemu" not in result.lower()
|
|
|
|
|
|
def test_smbios_safe_spaces_collapse_to_single_dash():
|
|
result = _smbios_safe("Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz")
|
|
assert result is not None
|
|
assert "--" not in result
|
|
|
|
|
|
def test_smbios_safe_none_returns_none():
|
|
assert _smbios_safe(None) is None
|
|
|
|
|
|
def test_smbios_safe_empty_string_returns_none():
|
|
assert _smbios_safe("") is None
|
|
|
|
|
|
# --- _disguise_smbios_args type=4 tests ---
|
|
|
|
|
|
def test_disguise_smbios_args_type4_present(monkeypatch):
|
|
monkeypatch.setattr(
|
|
_compose_module,
|
|
"_host_cpu_smbios",
|
|
lambda: ("GenuineIntel", "Intel-Core-i7-8550U"),
|
|
)
|
|
args = _disguise_smbios_args()
|
|
type4_values = [
|
|
v for flag, v in zip(args, args[1:]) if flag == "-smbios" and v.startswith("type=4,")
|
|
]
|
|
assert len(type4_values) >= 1, "no -smbios type=4 arg produced"
|
|
t4 = type4_values[0]
|
|
assert "manufacturer=GenuineIntel" in t4
|
|
assert "version=Intel-Core-i7-8550U" in t4
|
|
|
|
|
|
def test_disguise_smbios_args_no_qemu_in_any_value(monkeypatch):
|
|
monkeypatch.setattr(
|
|
_compose_module,
|
|
"_host_cpu_smbios",
|
|
lambda: ("GenuineIntel", "Intel-Core-i7-8550U"),
|
|
)
|
|
monkeypatch.setattr(_compose_module, "_host_dmi_field", lambda name: None)
|
|
args = _disguise_smbios_args()
|
|
for elem in args:
|
|
assert "QEMU" not in elem, f"element {elem!r} contains 'QEMU'"
|
|
assert "qemu" not in elem.lower(), f"element {elem!r} contains 'qemu'"
|
|
|
|
|
|
# -- disguise blob SELinux relabel (crash-loop fix) -----------------------
|
|
|
|
|
|
def test_relabel_blob_noop_without_chcon(monkeypatch, tmp_path):
|
|
import shutil
|
|
import subprocess
|
|
|
|
monkeypatch.setattr(shutil, "which", lambda _n: None)
|
|
called = []
|
|
monkeypatch.setattr(subprocess, "run", lambda *a, **k: called.append(a))
|
|
p = tmp_path / "winpodx-smbios.bin"
|
|
p.write_bytes(b"x")
|
|
_compose_module._relabel_blob_for_container(p)
|
|
assert called == [] # no chcon on PATH -> no subprocess call
|
|
|
|
|
|
def test_relabel_blob_invokes_chcon(monkeypatch, tmp_path):
|
|
import shutil
|
|
import subprocess
|
|
|
|
monkeypatch.setattr(shutil, "which", lambda _n: "/usr/bin/chcon")
|
|
calls = []
|
|
monkeypatch.setattr(subprocess, "run", lambda cmd, **k: calls.append(cmd))
|
|
p = tmp_path / "winpodx-smbios.bin"
|
|
p.write_bytes(b"x")
|
|
_compose_module._relabel_blob_for_container(p)
|
|
assert calls, "chcon should be invoked when present"
|
|
assert calls[0][:3] == ["/usr/bin/chcon", "-t", "container_file_t"]
|
|
assert calls[0][-1] == str(p)
|
|
|
|
|
|
def test_write_blob_relabels_after_write(monkeypatch, tmp_path):
|
|
seen = []
|
|
monkeypatch.setattr(_compose_module, "_relabel_blob_for_container", lambda p: seen.append(p))
|
|
path = _compose_module._write_disguise_smbios_blob(str(tmp_path))
|
|
assert path == "/oem/winpodx-smbios.bin"
|
|
assert (tmp_path / "winpodx-smbios.bin").exists()
|
|
assert len(seen) == 1 # relabel attempted right after the write
|