Harden QEMU input and credentials (#516 #519)

This commit is contained in:
Noah
2026-09-11 09:49:09 -06:00
parent bf39a74337
commit 7e0089826b
3 changed files with 123 additions and 10 deletions
+37 -8
View File
@@ -148,6 +148,8 @@ def _load_imgtools():
return imgtools
AXIS_MAX = 32767
TAP_HOLD_S = 0.35
TAP_OBSERVE_TIMEOUT_S = 2.0
# Bounds every blocking read on the QMP socket -- the greeting banner, the
# qmp_capabilities handshake, and every screendump/tap/quit round trip --
@@ -206,15 +208,37 @@ def to_axis(px, size):
return min(AXIS_MAX, math.ceil(px * AXIS_MAX / (size - 1)))
def do_tap(s, f, ax, ay, hold=0.2):
def do_tap(s, f, ax, ay, hold=TAP_HOLD_S):
send_events(s, f, [abs_ev("x", ax), abs_ev("y", ay), btn_ev(True)])
# Hold the press across several LVGL indev poll periods (33 ms each): an
# instantaneous press+release lands inside one poll and no click is ever
# registered.
# Hold the press across several LVGL indev poll periods (33 ms each), but
# stay below LVGL's 400 ms long-press threshold. A longer hold repeats
# controls such as Backspace and no longer represents a tap.
time.sleep(hold)
send_events(s, f, [btn_ev(False)])
def do_observed_tap(ctx, ax, ay):
"""Press until the guest reports consuming it, then release."""
before = parse_stats(ctx.ctl.send("stats")).get("presses")
if before is None:
do_tap(ctx.s, ctx.f, ax, ay)
return True
send_events(ctx.s, ctx.f, [abs_ev("x", ax), abs_ev("y", ay), btn_ev(True)])
observed = False
deadline = time.monotonic() + TAP_OBSERVE_TIMEOUT_S
try:
while time.monotonic() < deadline:
now = parse_stats(ctx.ctl.send("stats")).get("presses")
if now is not None and now != before:
observed = True
break
time.sleep(0.02)
finally:
send_events(ctx.s, ctx.f, [btn_ev(False)])
return observed
def do_swipe(s, f, x1, y1, x2, y2, size, ms=400, steps=None):
"""Drag with interpolated motion.
@@ -532,6 +556,11 @@ STATS_FIELD_RE = {
"fps": re.compile(r'^fps:\s*(-?\d+(?:\.\d+)?)\s*$'),
"render": re.compile(r'^render:\s*(-?\d+(?:\.\d+)?)\s*ms/frame\s*$'),
"idle": re.compile(r'^idle:\s*(\d+)\s*$'),
"presses": re.compile(r'^presses:\s*(\d+)\s*$'),
"termbusy": re.compile(r'^termbusy:\s*(\d+)\s*$'),
"termintr": re.compile(r'^termintr:\s*(\d+)\s*$'),
"termfg": re.compile(r'^termfg:\s*(-?\d+)\s*$'),
"termsig": re.compile(r'^termsig:\s*(\d+)\s*$'),
}
@@ -689,8 +718,8 @@ def verb_shot(ctx, lineno, cmd, args, line):
def verb_tap(ctx, lineno, cmd, args, line):
x, y = int(args[0]), int(args[1])
do_tap(ctx.s, ctx.f, to_axis(x, ctx.size), to_axis(y, ctx.size))
return "ok", ""
observed = do_observed_tap(ctx, to_axis(x, ctx.size), to_axis(y, ctx.size))
return ("ok", "") if observed else ("fail", "press was not consumed within 2 seconds")
def verb_swipe(ctx, lineno, cmd, args, line):
@@ -838,9 +867,9 @@ def verb_assert_stat(ctx, lineno, cmd, args, line):
# assert_stat FIELD OP VALUE: FIELD off a fresh `stats` reply.
ctx.need_ctl(lineno, cmd)
field, op, value = args[0], args[1], args[2]
if field not in ("cpu", "fps", "render", "idle"):
if field not in ("cpu", "fps", "render", "idle", "presses", "termbusy", "termintr", "termfg", "termsig"):
return "fail", (f"unknown stat field {field!r} "
f"(known: cpu, fps, render, idle)")
f"(known: cpu, fps, render, idle, presses, termbusy, termintr, termfg, termsig)")
stats = parse_stats(ctx.ctl.send("stats"))
if field not in stats:
return "fail", "no such field"