Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions build/guest-tests/gui-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# GT KEY-MENU press Alt+p (the chrome menu)
# GT CONSENT-CODE 1 NN type NN and Enter (the question's code)
# GT CONSENT-WAIT 2 type y and Enter (not the code: refused)
# GT POINTER-ZONE0, GT POINTER-UNTRUSTED move the pointer onto the newest window
# GT END
# Verdicts: "GT PASS|FAIL|INFO name - detail".
set -u
Expand Down Expand Up @@ -336,6 +337,36 @@ n=20; while [[ "$n" -gt 0 && -n "$(questions)" ]]; do n=$((n - 1)); sleep 1; don
[[ -z "$(questions)" ]] && pass "consent-cleaned" "no question left behind" || fail "consent-cleaned" "$(questions | tr '
' ' ')"

# --- a zone's cursor image is never drawn ---------------------------------------------
# wlprobe cursor, once the pointer enters its window, asks for an image that,
# drawn, covers the screen. The compositor tells a client which output each
# of its surfaces is shown on, a cursor image among them once it is in use:
# zone 0's image is told, so the same word missing for a zone's means the
# compositor never took it. (A screenshot from the host holds no cursor.)
as_user "/usr/libexec/kryptik/wlprobe cursor 12" > "$LOG/cursor-zone0.out" 2>&1 &
probe0=$!
wait_for 20 grep -q committed "$LOG/cursor-zone0.out"
echo "GT POINTER-ZONE0"
wait_for 20 grep -q 'set a ' "$LOG/cursor-zone0.out" && pass "zone0-cursor-set" "$(grep 'set a ' "$LOG/cursor-zone0.out")" || fail "zone0-cursor-set" "$(tr '\n' ' ' < "$LOG/cursor-zone0.out")"
wait_for 10 grep -q 'the cursor image entered an output' "$LOG/cursor-zone0.out" && pass "zone0-cursor-shown" "the compositor took zone 0's image: it entered an output" || fail "zone0-cursor-shown" "zone 0's cursor image entered no output, so a zone's proves nothing: $(tr '\n' ' ' < "$LOG/cursor-zone0.out")"
wait "$probe0" 2>/dev/null
stop_zone untrusted
mark cursor untrusted
launch_plain untrusted "/usr/libexec/kryptik/wlprobe cursor 20" > "$LOG/launch-cursor.out" 2>&1
cursor_asked() { since_mark cursor untrusted | grep -q 'set a '; }
wait_for 20 probe_committed cursor || fail "cursor-mapped" "the probe did not draw its window: $(tr '\n' ' ' < "$LOG/launch-cursor.out")"
echo "GT POINTER-UNTRUSTED"
wait_for 20 cursor_asked && pass "zone-cursor-asked" "$(since_mark cursor untrusted | grep 'set a ')" || fail "zone-cursor-asked" "$(since_mark cursor untrusted | tail -3 | tr '\n' ' '); $(zone_why untrusted)"
sleep 6
# The zone's client hears of its window through its proxy, so it would hear of its image.
since_mark cursor untrusted | grep -q 'the window entered an output' && pass "zone-hears-of-outputs" "the zone's client was told its window entered an output" || fail "zone-hears-of-outputs" "$(since_mark cursor untrusted | tail -3 | tr '\n' ' ')"
if since_mark cursor untrusted | grep -q 'the cursor image entered an output'; then
fail "zone-cursor-not-shown" "the compositor took the zone's cursor image: it entered an output"
else
pass "zone-cursor-not-shown" "six seconds after the zone asked, its cursor image has entered no output"
fi
stop_zone untrusted

# --- teardown ------------------------------------------------------------------------------
# The runtime tmpfs does not survive power-off: copy the session log and the
# last focus record to the state partition (p4, log/kryptik/) for a post-mortem.
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ No zone can reach the compositor's socket. A zone started from the desktop
gets its own `kryptik-wlproxy`, which hides the capture, clipboard,
input-injection and similar Wayland globals and stamps each window with its
zone; the compositor draws the zone's border and title prefix from that.
Over a zone's window the pointer is the compositor's own cursor: an image
the zone set would be drawn above every window, wherever it chose.

A window's border colour is how the user tells which zone it belongs to. If
they cannot tell at a glance which zone a password prompt belongs to, the
Expand Down
21 changes: 21 additions & 0 deletions tools/desktop/dwl-zone-borders.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,27 @@
\telse
\t\tfocusclient(focustop(selmon), 1);
}
"""),
# setcursor: a client's cursor image is drawn above every layer, anywhere
# its hotspot puts it, so a zone's pointer shows dwl's default instead.
("""\tstruct wlr_seat_pointer_request_set_cursor_event *event = data;
""",
"""\tstruct wlr_seat_pointer_request_set_cursor_event *event = data;
\tClient *c = NULL;
"""),
("""\tif (event->seat_client == seat->pointer_state.focused_client)
\t\twlr_cursor_set_surface(cursor, event->surface,
\t\t\t\tevent->hotspot_x, event->hotspot_y);
""",
"""\tif (event->seat_client != seat->pointer_state.focused_client)
\t\treturn;
\t/* Kryptik: a zone never gets its own cursor image, which would draw over the chrome. */
\ttoplevel_from_wlr_surface(seat->pointer_state.focused_surface, &c, NULL);
\tif (c && c->zoneborder != unzonedcolor)
\t\twlr_cursor_set_xcursor(cursor, cursor_mgr, "default");
\telse
\t\twlr_cursor_set_surface(cursor, event->surface,
\t\t\t\tevent->hotspot_x, event->hotspot_y);
"""),
]

Expand Down
3 changes: 2 additions & 1 deletion tools/desktop/kryptik-chrome
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
# shows comes from the compositor and the launch daemon, never from a zone, and
# dwl gives its windows the UNZONED border (build/desktop/zone-colours.h). No
# zone can draw over it, read its input or capture it: the proxy passes no
# layer shell, global input or screencopy.
# layer shell, global input or screencopy, and dwl never draws a zone's
# cursor image.
#
# kryptik-chrome dwl's startup command (`dwl -s`): reads
# dwl's status stream on stdin, records the
Expand Down
81 changes: 78 additions & 3 deletions tools/desktop/wlprobe.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
* ("oversize" by default, at most 255 bytes)
* wlprobe charge map one unwritten 4 MiB shmem buffer for 10 s;
* compare zone and compositor cgroup memory.current
* wlprobe cursor SECONDS as oversize 0, and when the pointer enters, set a
* cursor image twice the screen's size, hotspot in
* its middle: drawn, it covers the whole screen.
* Says when the window, and when the cursor image,
* enters an output: the second only if the
* compositor took the image
*
* Exit: 0 listed, bind accepted or window held; 3 refused (wl_display.error,
* or closed); 1 any other failure. The socket is $WAYLAND_DISPLAY, absolute
Expand Down Expand Up @@ -153,6 +159,55 @@ static void draw(void)
fflush(stdout);
}

/* cursor: the image is 2560x1600, for a 1280x800 screen, in a colour nothing
* else draws. */
static const uint32_t cursor_rgb = 0x13f7a5;
static uint32_t seat_id, pointer_id, cursor_surf;
static int cursor;

/* On every entry, as a client does; the image is made on the first. */
static void set_cursor(uint32_t serial)
{
int w = 2560, h = 1600, stride = w * 4, fd = -1;
size_t size = (size_t)stride * (size_t)h;
unsigned char b[24];
if (!cursor_surf) {
fd = memfd_create("wlprobe-cursor", MFD_CLOEXEC);
if (fd < 0 || ftruncate(fd, (off_t)size) < 0) {
printf("memfd: %s\n", strerror(errno));
if (fd >= 0) close(fd);
return;
}
uint32_t *px = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (px == MAP_FAILED) { printf("mmap: %s\n", strerror(errno)); close(fd); return; }
for (size_t i = 0; i < size / 4; i++) px[i] = cursor_rgb;
munmap(px, size);
cursor_surf = next_id++;
put32(b, cursor_surf);
send_msg(COMPOSITOR, 0, b, 4); /* wl_compositor.create_surface */
}
put32(b, serial); put32(b + 4, cursor_surf); put32(b + 8, (uint32_t)(w / 2)); put32(b + 12, (uint32_t)(h / 2));
send_msg(pointer_id, 0, b, 16); /* wl_pointer.set_cursor */
if (fd >= 0) {
uint32_t pool = next_id++, buffer = next_id++;
put32(b, pool);
put32(b + 4, (uint32_t)size);
int r = send_msg_fd(SHM, 0, b, 8, fd); /* wl_shm.create_pool(id, fd, size) */
close(fd);
if (r) { printf("create_pool: %s\n", strerror(errno)); return; }
put32(b, buffer); put32(b + 4, 0); put32(b + 8, (uint32_t)w); put32(b + 12, (uint32_t)h);
put32(b + 16, (uint32_t)stride); put32(b + 20, 1);
send_msg(pool, 0, b, 24); /* wl_shm_pool.create_buffer */
put32(b, buffer); put32(b + 4, 0); put32(b + 8, 0);
send_msg(cursor_surf, 1, b, 12); /* wl_surface.attach */
put32(b, 0); put32(b + 4, 0); put32(b + 8, (uint32_t)w); put32(b + 12, (uint32_t)h);
send_msg(cursor_surf, 2, b, 16); /* wl_surface.damage */
send_msg(cursor_surf, 6, b, 0); /* wl_surface.commit */
}
printf("set a %dx%d cursor, hotspot %d,%d\n", w, h, w / 2, h / 2);
fflush(stdout);
}

/* Consume one message if present. Returns 1 consumed, 0 need more, -1 malformed. */
static int handle_one(void)
{
Expand Down Expand Up @@ -197,6 +252,12 @@ static int handle_one(void)
put32(b, get32(body));
send_msg(XDG_SURFACE, 4, b, 4); /* xdg_surface.ack_configure */
draw();
} else if (cursor && object == pointer_id && opcode == 0) {
set_cursor(get32(body)); /* wl_pointer.enter(serial, surface, x, y) */
} else if (cursor && opcode == 0 && (object == SURFACE || (cursor_surf && object == cursor_surf))) {
/* wl_surface.enter(output) */
printf("%s entered an output\n", object == SURFACE ? "the window" : "the cursor image");
fflush(stdout);
} else if (!oversize) {
printf("event object=%u opcode=%u size=%u\n", object, opcode, size);
}
Expand Down Expand Up @@ -251,6 +312,17 @@ static int hold_oversize(int more, int seconds, const char *title)
send_msg(TOPLEVEL, 2, b, n); /* xdg_toplevel.set_title */
n = put_string(b, "wlprobe");
send_msg(TOPLEVEL, 3, b, n); /* xdg_toplevel.set_app_id */
if (cursor) {
seat_id = next_id++;
if (bind_global("wl_seat", seat_id)) return 1;
pointer_id = next_id++;
put32(b, pointer_id);
send_msg(seat_id, 0, b, 4); /* wl_seat.get_pointer */
/* A client that holds an output is told when a surface of its own
* enters it: the window when it is shown, a cursor image when the
* compositor takes it. */
if (bind_global("wl_output", next_id++)) return 1;
}
send_msg(SURFACE, 6, b, 0); /* wl_surface.commit: ask for a configure */
time_t end = time(NULL) + seconds;
while (time(NULL) < end && !closed) {
Expand All @@ -261,11 +333,13 @@ static int hold_oversize(int more, int seconds, const char *title)

int main(int argc, char **argv)
{
if (argc < 2 || (strcmp(argv[1], "list") && strcmp(argv[1], "bind") && strcmp(argv[1], "oversize") && strcmp(argv[1], "charge"))
if (argc < 2 || (strcmp(argv[1], "list") && strcmp(argv[1], "bind") && strcmp(argv[1], "oversize") && strcmp(argv[1], "charge")
&& strcmp(argv[1], "cursor"))
|| (!strcmp(argv[1], "bind") && argc < 3) || (!strcmp(argv[1], "oversize") && argc < 4)
|| (!strcmp(argv[1], "oversize") && argc > 4 && strlen(argv[4]) > 255)
|| (!strcmp(argv[1], "charge") && argc != 2)) {
fprintf(stderr, "usage: wlprobe list | bind INTERFACE | oversize EXTRA SECONDS [TITLE] | charge\n");
|| (!strcmp(argv[1], "charge") && argc != 2)
|| (!strcmp(argv[1], "cursor") && argc != 3)) {
fprintf(stderr, "usage: wlprobe list | bind INTERFACE | oversize EXTRA SECONDS [TITLE] | charge | cursor SECONDS\n");
return 2;
}
const char *disp = getenv("WAYLAND_DISPLAY");
Expand Down Expand Up @@ -294,6 +368,7 @@ int main(int argc, char **argv)
if (!strcmp(argv[1], "list")) return errored ? 3 : 0;
if (!strcmp(argv[1], "oversize")) return hold_oversize(atoi(argv[2]), atoi(argv[3]), argc > 4 ? argv[4] : "oversize");
if (!strcmp(argv[1], "charge")) { charge = 1; return hold_oversize(0, 10, "shm-charge"); }
if (!strcmp(argv[1], "cursor")) { cursor = 1; return hold_oversize(0, atoi(argv[2]), "cursor"); }

/* A filtered client cannot know a hidden global's name, so guess 1; the
* proxy must refuse either way. */
Expand Down
8 changes: 7 additions & 1 deletion tools/image/gui-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# and the yes/no to the transfer
# questions, delivered as keystrokes on the guest's keyboard, so the
# trusted windows are exercised by input, not by writing answer files.
# With the pointer moved onto a zone's window, the cursor image the zone asks
# for must not be taken by the compositor; zone 0's, which is, shows the
# guest's check can tell.
set -uo pipefail
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
Expand Down Expand Up @@ -70,6 +73,8 @@ python3 "$DRV" --serial "$SER" --qmp "$QMP" --timeout 600 \
"expect:GT KEY-FOCUS-PERSONAL" "key:alt+j" \
"type-from:GT CONSENT-CODE 1 ([0-9]+)" \
"expect:GT CONSENT-WAIT 2" "key:y" "key:ret" \
"expect:GT POINTER-ZONE0" "pointer:-4000,-4000" "pointer:120,120" \
"expect:GT POINTER-UNTRUSTED" "pointer:-4000,-4000" "pointer:120,120" \
"expect:GT END" "expect:GCHECK-DONE" \
"send:su - root -c 'poweroff'" "expect:Password: ?" "send:${RPASS}" \
"expect:Power down" "wait-exit"
Expand All @@ -84,7 +89,8 @@ if [[ -n "$summary" && "${gf:-1}" -eq 0 && "${gp:-0}" -ge 25 ]]; then green "eve
grep 'GT FAIL' <<<"$T" | sed 's/^/ /'
for name in session-socket compositor-running chrome-focus-record chrome-window-is-zone0 zone0-sees-capture zone-proxy-path zone-sees-needed zone-hidden-globals zone-bind-refused proxy-logged-refusal \
map-keeps-zone0-focus focus-shows-zone focus-shows-label title-prefixed last-zone-recorded menu-opens-on-key menu-keeps-last-zone zone-fullscreen-refused compositor-survives-close oversize-window forged-title-named-by-zone second-zone-window zone0-own-programs-only zone-app-in-cgroup no-virtual-input clipboard-isolated clipboard-move-gesture clipboard-moved \
transfer-policy no-question-for-policy-refusal consent-code-shown transfer-approved transfer-landed plain-y-refused denied-file-absent; do
transfer-policy no-question-for-policy-refusal consent-code-shown transfer-approved transfer-landed plain-y-refused denied-file-absent \
zone0-cursor-set zone0-cursor-shown zone-cursor-asked zone-hears-of-outputs zone-cursor-not-shown; do
grep -q "GT PASS ${name}" <<<"$T" && green "guest: ${name}" || red "guest: ${name} (not passed)"
done

Expand Down
11 changes: 11 additions & 0 deletions tools/image/vm-drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
screendump:FILE ask QEMU (QMP) for a PPM screenshot
key:NAME[+NAME...] press keys on the guest's keyboard through QMP
(qcodes, e.g. key:y key:ret key:alt+e)
pointer:DX,DY move the guest's mouse by DX,DY through QMP; it is
relative and accelerated, so a large move ends at
the screen's edge
type-from:REGEX wait for REGEX on a line that has ended (REGEX
names no line end), then type what its first
group matched (digits and letters) and Enter
Expand Down Expand Up @@ -273,6 +276,14 @@ def main():
r = qmp(qmpsock, "send-key", {"keys": keys, "hold-time": 80})
if "error" in r: raise RuntimeError(f"send-key {rest}: {r['error']}")
time.sleep(0.3)
elif kind == "pointer":
if not qmpsock: raise RuntimeError("pointer needs --qmp")
dx, dy = (int(v) for v in rest.split(","))
events = [{"type": "rel", "data": {"axis": "x", "value": dx}},
{"type": "rel", "data": {"axis": "y", "value": dy}}]
r = qmp(qmpsock, "input-send-event", {"events": events})
if "error" in r: raise RuntimeError(f"input-send-event {rest}: {r['error']}")
time.sleep(0.3)
elif kind == "type-from":
if not qmpsock: raise RuntimeError("type-from needs --qmp")
text = d.line_value(rest)
Expand Down
Loading