Improve security and error handling for VM state and guest file operations; clarify permissions requirements in documentation.

This commit is contained in:
2026-09-04 21:51:04 +00:00
parent a8dbc704e9
commit 4ce14c3378
6 changed files with 117 additions and 7 deletions
+59 -3
View File
@@ -24,6 +24,8 @@ _FIRECRACKER_DEMO_PUBLIC_KEY = (
)
_MODE_PATTERN = re.compile(r"Mode:\s+0*([0-7]+)")
_OWNER_PATTERN = re.compile(r"User:\s+(\d+)\s+Group:\s+(\d+)")
_FLAGS_PATTERN = re.compile(r"Flags:\s+0x([0-9a-fA-F]+)")
_LINKS_PATTERN = re.compile(r"Links:\s+(\d+)")
_HOST_KEY_TYPES = (
("rsa", "3072"),
("ecdsa", "256"),
@@ -43,6 +45,9 @@ class _GuestFile:
mode: int
uid: int
gid: int
flags: int | None = None
links: int = 1
has_extended_attributes: bool = False
class ImageStore:
@@ -184,7 +189,14 @@ class ImageStore:
)
mode_match = _MODE_PATTERN.search(stat_result.stdout)
owner_match = _OWNER_PATTERN.search(stat_result.stdout)
if mode_match is None or owner_match is None:
flags_match = _FLAGS_PATTERN.search(stat_result.stdout)
links_match = _LINKS_PATTERN.search(stat_result.stdout)
if (
mode_match is None
or owner_match is None
or flags_match is None
or links_match is None
):
output = f"{stat_result.stdout}\n{stat_result.stderr}".lower()
if not required and "file not found" in output:
return None
@@ -198,14 +210,35 @@ class ImageStore:
)
if not destination.is_file():
raise UvmError(f"could not read guest rootfs file: {guest_path}")
attributes = self._runner.run(
("debugfs", "-R", f"ea_list {guest_path}", disk),
check=False,
capture=True,
)
diagnostics = [
line
for line in attributes.stderr.splitlines()
if line and not line.startswith("debugfs ")
]
if attributes.returncode != 0 or diagnostics:
raise UvmError(f"could not inspect guest rootfs metadata: {guest_path}")
return _GuestFile(
path=destination,
mode=int(mode_match.group(1), 8),
uid=int(owner_match.group(1)),
gid=int(owner_match.group(2)),
flags=int(flags_match.group(1), 16),
links=int(links_match.group(1)),
has_extended_attributes=bool(attributes.stdout.strip()),
)
def _write_guest_file(self, disk: Path, guest_path: str, source: _GuestFile) -> None:
if source.links != 1:
raise UvmError(f"cannot safely replace hard-linked guest file: {guest_path}")
if source.has_extended_attributes:
raise UvmError(
f"cannot safely replace guest file with extended attributes: {guest_path}"
)
self._runner.run(("debugfs", "-w", "-R", f"rm {guest_path}", disk), capture=True)
self._runner.run(
("debugfs", "-w", "-R", f"write {source.path} {guest_path}", disk),
@@ -226,6 +259,17 @@ class ImageStore:
),
capture=True,
)
if source.flags is not None:
self._runner.run(
(
"debugfs",
"-w",
"-R",
f"set_inode_field {guest_path} flags 0x{source.flags:x}",
disk,
),
capture=True,
)
verification = source.path.with_name(f"{source.path.name}.verify")
written = self._read_guest_file(disk, guest_path, verification)
@@ -235,6 +279,7 @@ class ImageStore:
or written.mode != source.mode
or written.uid != source.uid
or written.gid != source.gid
or (source.flags is not None and written.flags != source.flags)
):
raise UvmError(f"could not verify updated guest rootfs file: {guest_path}")
@@ -315,7 +360,14 @@ def _set_shadow_password(contents: str, username: str, password_hash: str) -> st
def _enable_ssh_password_authentication(contents: str, username: str) -> str:
managed_directives = {"hostkey", "passwordauthentication", "permitrootlogin"}
managed_directives = {
"challengeresponseauthentication",
"hostkey",
"kbdinteractiveauthentication",
"passwordauthentication",
}
if username == "root":
managed_directives.add("permitrootlogin")
unmanaged_lines = []
for line in contents.splitlines():
stripped = line.lstrip()
@@ -328,13 +380,17 @@ def _enable_ssh_password_authentication(contents: str, username: str) -> str:
"HostKey /etc/ssh/ssh_host_ecdsa_key",
"HostKey /etc/ssh/ssh_host_ed25519_key",
"PasswordAuthentication no",
"KbdInteractiveAuthentication no",
"ChallengeResponseAuthentication no",
]
if username == "root":
directives.append("PermitRootLogin yes")
return (
"# Managed by uvm\n"
+ "\n".join(directives)
+ f"\nMatch User {username}\n PasswordAuthentication yes\nMatch all\n"
+ f"\nMatch User {username}\n"
+ " PasswordAuthentication yes\n"
+ "Match all\n"
+ "\n".join(unmanaged_lines).rstrip()
+ "\n"
)
+4
View File
@@ -98,6 +98,10 @@ class StateStore:
os.chmod(path, 0o600)
with path.open(encoding="utf-8") as state_file:
raw = json.load(state_file)
except PermissionError as error:
raise StateError(
f"cannot access protected VM registry {path}. Run this command with sudo."
) from error
except (OSError, json.JSONDecodeError) as error:
raise StateError(f"cannot read {path}: {error}") from error
return State.from_dict(raw)