|
1 | 1 | import base64 |
2 | 2 | import contextlib |
| 3 | +import errno |
3 | 4 | import json |
4 | 5 | import logging |
5 | 6 | import os |
|
8 | 9 | import random |
9 | 10 | import re |
10 | 11 | import shutil |
| 12 | +import struct |
11 | 13 | import subprocess |
12 | 14 | import tempfile |
| 15 | +import threading |
13 | 16 | import time |
14 | 17 | from typing import Iterable, Iterator, List, Optional, Sequence |
15 | 18 |
|
|
22 | 25 |
|
23 | 26 | CHALLENGE_SEED = int(os.environ.get("CHALLENGE_SEED", "0")) |
24 | 27 |
|
| 28 | +_KATA_TRANSITION_LOCK = threading.Lock() |
| 29 | +KATA_KVM_ACL_TIMEOUT = 10 |
| 30 | +KATA_KVM_ACL_STABLE_TIME = 0.25 |
| 31 | +KATA_KVM_POLL_INTERVAL = 0.05 |
| 32 | +KVM_DEVICE_PATH = pathlib.Path("/dev/kvm") |
| 33 | +KVM_ACL_XATTR = "system.posix_acl_access" |
| 34 | + |
| 35 | +_POSIX_ACL_XATTR_VERSION = 2 |
| 36 | +_POSIX_ACL_USER = 2 |
| 37 | +_POSIX_ACL_MASK = 16 |
| 38 | +_POSIX_ACL_READ_WRITE = 6 |
| 39 | +_POSIX_ACL_HEADER = struct.Struct("<I") |
| 40 | +_POSIX_ACL_ENTRY = struct.Struct("<HHI") |
| 41 | + |
25 | 42 | clang_format = shutil.which("clang-format") |
26 | 43 | if not clang_format: |
27 | 44 | logger.warning("clang-format not found; C templates will not be formatted") |
@@ -100,6 +117,79 @@ def image_path(challenge_image: str) -> str: |
100 | 117 | return "" |
101 | 118 |
|
102 | 119 |
|
| 120 | +def _mapped_root_kvm_acl_is_healthy() -> Optional[bool]: |
| 121 | + """Check whether the outer user namespace grants mapped root KVM access.""" |
| 122 | + try: |
| 123 | + acl = os.getxattr(KVM_DEVICE_PATH, KVM_ACL_XATTR) |
| 124 | + except OSError as error: |
| 125 | + if error.errno in { |
| 126 | + errno.EACCES, |
| 127 | + errno.ENODATA, |
| 128 | + errno.ENOENT, |
| 129 | + errno.ENOTSUP, |
| 130 | + errno.EOPNOTSUPP, |
| 131 | + errno.EPERM, |
| 132 | + }: |
| 133 | + return None |
| 134 | + raise RuntimeError(f"Could not inspect {KVM_DEVICE_PATH} ACL: {error}") from error |
| 135 | + |
| 136 | + if len(acl) < _POSIX_ACL_HEADER.size: |
| 137 | + return False |
| 138 | + (version,) = _POSIX_ACL_HEADER.unpack_from(acl) |
| 139 | + entries = acl[_POSIX_ACL_HEADER.size :] |
| 140 | + if version != _POSIX_ACL_XATTR_VERSION or len(entries) % _POSIX_ACL_ENTRY.size: |
| 141 | + return False |
| 142 | + |
| 143 | + parsed_entries = list(_POSIX_ACL_ENTRY.iter_unpack(entries)) |
| 144 | + mapped_root_permissions = next( |
| 145 | + (permissions for tag, permissions, user_id in parsed_entries if tag == _POSIX_ACL_USER and user_id == 0), |
| 146 | + None, |
| 147 | + ) |
| 148 | + mask_permissions = next( |
| 149 | + (permissions for tag, permissions, _ in parsed_entries if tag == _POSIX_ACL_MASK), |
| 150 | + None, |
| 151 | + ) |
| 152 | + if mapped_root_permissions is None or mask_permissions is None: |
| 153 | + return False |
| 154 | + return mapped_root_permissions & mask_permissions & _POSIX_ACL_READ_WRITE == _POSIX_ACL_READ_WRITE |
| 155 | + |
| 156 | + |
| 157 | +def _wait_for_stable_mapped_root_kvm_acl() -> None: |
| 158 | + deadline = time.monotonic() + KATA_KVM_ACL_TIMEOUT |
| 159 | + healthy_since = None |
| 160 | + |
| 161 | + while True: |
| 162 | + now = time.monotonic() |
| 163 | + if _mapped_root_kvm_acl_is_healthy() is True: |
| 164 | + if healthy_since is None: |
| 165 | + healthy_since = now |
| 166 | + elif now - healthy_since >= KATA_KVM_ACL_STABLE_TIME: |
| 167 | + return |
| 168 | + else: |
| 169 | + healthy_since = None |
| 170 | + |
| 171 | + if now >= deadline: |
| 172 | + raise RuntimeError( |
| 173 | + f"Timed out after {KATA_KVM_ACL_TIMEOUT}s waiting for {KVM_DEVICE_PATH} " |
| 174 | + "to retain its mapped-root read/write ACL" |
| 175 | + ) |
| 176 | + time.sleep(min(KATA_KVM_POLL_INTERVAL, max(0, deadline - now))) |
| 177 | + |
| 178 | + |
| 179 | +def _run_with_transition_lock(runtime: str, function, *args, wait_before: bool = True, **kwargs): |
| 180 | + if runtime != "kata": |
| 181 | + return function(*args, **kwargs) |
| 182 | + with _KATA_TRANSITION_LOCK: |
| 183 | + acl_is_observable = _mapped_root_kvm_acl_is_healthy() is not None |
| 184 | + if acl_is_observable and wait_before: |
| 185 | + _wait_for_stable_mapped_root_kvm_acl() |
| 186 | + try: |
| 187 | + return function(*args, **kwargs) |
| 188 | + finally: |
| 189 | + if acl_is_observable: |
| 190 | + _wait_for_stable_mapped_root_kvm_acl() |
| 191 | + |
| 192 | + |
103 | 193 | @contextlib.contextmanager |
104 | 194 | def run_challenge( |
105 | 195 | challenge_path: pathlib.Path, |
@@ -135,7 +225,9 @@ def run_challenge( |
135 | 225 | ) |
136 | 226 | container = None |
137 | 227 | try: |
138 | | - container = subprocess.check_output( |
| 228 | + container = _run_with_transition_lock( |
| 229 | + runtime, |
| 230 | + subprocess.check_output, |
139 | 231 | [ |
140 | 232 | "docker", |
141 | 233 | "run", |
@@ -203,8 +295,11 @@ def run_challenge( |
203 | 295 | finally: |
204 | 296 | if container: |
205 | 297 | logger.debug("removing container %s", container[:12]) |
206 | | - subprocess.run( |
| 298 | + _run_with_transition_lock( |
| 299 | + runtime, |
| 300 | + subprocess.run, |
207 | 301 | ["docker", "rm", "--force", container], |
| 302 | + wait_before=False, |
208 | 303 | stdout=subprocess.DEVNULL, |
209 | 304 | stderr=subprocess.DEVNULL, |
210 | 305 | ) |
|
0 commit comments