diff --git a/build/guest-tests/gui-check.sh b/build/guest-tests/gui-check.sh index fa3a2785..189ba2c2 100755 --- a/build/guest-tests/gui-check.sh +++ b/build/guest-tests/gui-check.sh @@ -39,9 +39,8 @@ wait_for() { # wait_for SECONDS CMD... return 1 } zone_log() { cat "/var/log/kryptik/zone-$1.log" 2>/dev/null; } -# A process the zone filter kills below the zone's pid 1 leaves nothing in the -# zone log; the kernel's audit line (type=1326) names it and the syscall. -zone_why() { # zone_why ZONE: the registry's view, the zone's and its proxy's logs, what runs, the filter's last kills +# A filter kill below the zone's pid 1 shows only in the kernel's audit line (type=1326). +zone_why() { # zone_why ZONE: the registry, both logs, what runs and the filter's last kills echo "entries: $(ls /run/kryptik/zones 2>&1 | tr '\n' ' ')| $1: $(ls -la --time-style=full-iso /run/kryptik/zones/"$1" 2>&1 | tr '\n' ' ')| running: $("$KD" list --running 2>&1 | tr '\n' ' ')| log: $(zone_log "$1" | tail -10 | tr '\n' ' ')| proxy: $(tail -4 "$RT/kryptik/$1/proxy.log" 2>&1 | tr '\n' ' ')| procs: $(pgrep -af "havoc|kryptikd run $1" 2>/dev/null | cut -c1-90 | tr '\n' ';')| seccomp: $(dmesg 2>/dev/null | grep -a 'type=1326' | tail -3 | tr '\n' ' ')" } mark() { echo "--- $1 ---" >> "/var/log/kryptik/zone-$2.log" 2>/dev/null; } @@ -61,15 +60,12 @@ echo "GT BEGIN $(date -Iseconds 2>/dev/null)" for z in work dev personal; do "$KD" volume init "$z" --size 64M --passphrase-file "$PP" > "$LOG/vol-$z.out" 2>&1 || fail "volume-$z" "$(tail -1 "$LOG/vol-$z.out")" done -# Exactly one DRM device, the native driver's: the GPU module must replace the -# built-in simpledrm at coldplug, as two cards put wlroots on a multi-GPU path -# the pixman renderer cannot serve. The card left is not card0 (simpledrm's). -# virtio-gpu hangs its card on the PCI function, whose driver is the transport -# (virtio-pci); the GPU driver is the virtio device's, under it. +# One DRM device, the GPU's: beside simpledrm, wlroots takes a multi-GPU path pixman cannot serve. cards=() for c in /sys/class/drm/card[0-9]*; do [[ -e "$c" && "${c##*/}" != *-* ]] || continue d="$(readlink -f "$c/device")" + # The PCI function's driver is virtio-pci; the GPU driver is on the virtio device under it. drv="$(readlink -f "$d"/virtio*/driver 2>/dev/null | head -1)" cards+=("${c##*/}=$(basename "${drv:-$(readlink -f "$d/driver")}" 2>/dev/null)") done @@ -77,7 +73,7 @@ done [[ "$(s6-svstat -o up /run/service/seatd 2>/dev/null)" = true ]] && pass "seatd-up" || fail "seatd-up" [[ "$(s6-svstat -o up /run/service/kryptikd-serve 2>/dev/null)" = true ]] && pass "launch-daemon-up" || fail "launch-daemon-up" -# --- the session -------------------------------------------------------------- +# --- the session ------------------------------------------------------------ su -s /bin/bash "$USER_NAME" -c 'setsid /usr/bin/kryptik-session /dev/null 2>&1 &' if wait_for 30 test -S "$RT/wayland-0"; then pass "session-socket" "dwl listening at $RT/wayland-0"; else fail "session-socket" "$(cat "$RT/kryptik/session.log" 2>/dev/null | tail -3 | tr '\n' ' ')"; fi pgrep -u "$USER_NAME" -x dwl >/dev/null && pass "compositor-running" || fail "compositor-running" "$(tail -3 "$RT/kryptik/session.log" 2>/dev/null | tr '\n' ' ')" @@ -85,7 +81,7 @@ wait_for 20 test -f "$RT/kryptik/focus" && pass "chrome-focus-record" "$(tr '\n' # The record exists before the chrome's own window has mapped: wait for the window. wait_for 20 grep -q '^zone=0' "$RT/kryptik/focus" && pass "chrome-window-is-zone0" "the launcher window is recorded as zone 0 (trusted)" || fail "chrome-window-is-zone0" "$(cat "$RT/kryptik/focus" 2>/dev/null | tr '\n' ' '); terminals: $(pgrep -u "$USER_NAME" -a havoc 2>/dev/null | tr '\n' ';'); session.log: $(tail -4 "$RT/kryptik/session.log" 2>/dev/null | tr '\n' ' ')" -# --- what zone 0 sees, and what a zone sees --------------------------------------- +# --- what zone 0 sees, and what a zone sees --------------------------------- as_user "/usr/libexec/kryptik/wlprobe list" > "$LOG/probe-zone0.out" 2>&1 grep -q 'zwlr_screencopy_manager_v1' "$LOG/probe-zone0.out" && grep -q 'wl_data_device_manager' "$LOG/probe-zone0.out" \ && pass "zone0-sees-capture" "the compositor offers screencopy and the data device to zone 0's own client (positive control)" \ @@ -228,7 +224,7 @@ else fi wait_for 20 test ! -e /run/kryptik/zones/untrusted/init.pid; sleep 1 -# A real terminal: focus it with an explicit user key. +# A terminal, focused by an explicit user key. launch_plain untrusted "havoc" > "$LOG/launch-havoc-untrusted.out" 2>&1 echo "GT KEY-FOCUS-ZONE" if wait_for 20 grep -q '^zone=untrusted' "$RT/kryptik/focus"; then @@ -254,8 +250,7 @@ echo "GT SCREENSHOT-FULLSCREEN" sleep 6 echo "GT KEY-FULLSCREEN-AGAIN" wait_for 20 grep -q '^fullscreen=0' "$RT/kryptik/focus" && pass "fullscreen-off-again" || fail "fullscreen-off-again" -# Alt+p opens one more menu window: the chrome's text menu in a zone 0 -# terminal of its own, as at login. Closed again once seen. +# Alt+p opens another menu window (the chrome's text menu in a zone 0 terminal), closed once seen. menu_windows() { pgrep -u "$USER_NAME" -f 'havoc /usr/bin/kryptik-chrome --menu' | wc -l; } menus_before="$(menu_windows)" more_menus() { [[ "$(menu_windows)" -gt "$menus_before" ]]; } @@ -266,8 +261,7 @@ if wait_for 20 more_menus; then else fail "menu-opens-on-key" "no new menu window after Alt+p: $(pgrep -u "$USER_NAME" -af 'kryptik-chrome --menu' | cut -c1-80 | tr '\n' ';')" fi -# A zone 0 window taking focus, as the menu does when opened, leaves the last -# zone window's record alone: that record is what the menu's f shows. +# A zone 0 window taking focus keeps the last zone window's record, which the menu's f shows. as_user "/usr/libexec/kryptik/wlprobe oversize 0 15 zone-0" > "$LOG/zone0-window.out" 2>&1 & if wait_for 20 grep -q '^zone=0' "$RT/kryptik/focus"; then grep -q '^zone=untrusted' "$RT/kryptik/focus.zone" 2>/dev/null \ @@ -281,15 +275,14 @@ pkill -u "$USER_NAME" -f 'wlprobe oversize 0 15 zone-0' 2>/dev/null # A zone runs one command at a time, so its window is stopped before the next. stop_zone() { as_user "kryptik-launch --stop $1" > /dev/null 2>&1; wait_for 15 test ! -e "/run/kryptik/zones/$1/init.pid"; sleep 1; } stop_zone untrusted -# A window closing must not take the compositor with it: every later window, -# in any zone, would find no display behind its proxy. +# A closing window must not kill the compositor: every later window would find no display. if pgrep -u "$USER_NAME" -x dwl > /dev/null; then pass "compositor-survives-close" "dwl still runs after the untrusted window closed" else fail "compositor-survives-close" "dwl is gone after the untrusted window closed; session.log: $(tail -4 "$RT/kryptik/session.log" 2>/dev/null | tr '\n' ' ')" fi -# --- a window cannot cover its own frame ---------------------------------- +# --- a window cannot cover its own frame ------------------------------------ # wlprobe answers every configure with a buffer 40 px larger than asked. dwl # clips a surface only to (w - bw) x (h - bw), so the excess lies under the # right and bottom borders; the host measures all four in its screenshot. @@ -306,8 +299,7 @@ sleep 2 echo "GT SCREENSHOT-OVERSIZE" sleep 6 stop_zone untrusted -# A window titled as another zone's is named by its own zone, from the app_id -# the proxy stamps, never from its title. +# A window titled as another zone's is named by the app_id the proxy stamps, never its title. mark forged untrusted launch_plain untrusted "/usr/libexec/kryptik/wlprobe oversize 0 20 '[vault] forged'" > "$LOG/launch-forged.out" 2>&1 wait_for 20 probe_configured forged || fail "forged-mapped" "probe did not draw its configured window" @@ -321,24 +313,17 @@ else fi stop_zone untrusted -# --- a second zone with a window; no virtual input for either ------------- -# personal gets its probe before its window. +# --- a second zone with a window; no virtual input for either --------------- mark probe personal launch personal "/usr/libexec/kryptik/wlprobe list" > "$LOG/launch-probe-personal.out" 2>&1 -# An encrypted zone can be launched again only once its volume has closed, -# which follows its launcher's exit. +# An encrypted zone relaunches only once its volume has closed, after its launcher exits. wait_for 30 test ! -e /run/kryptik/zones/personal/init.pid; sleep 1 out="$(since_mark probe personal)" if [[ "$out" == *"global "* && "$out" != *"virtual_keyboard"* && "$out" != *"virtual_pointer"* && "$out" != *"input_method"* ]]; then pass "no-virtual-input" "no virtual keyboard/pointer or input-method global in personal either"; else fail "no-virtual-input" "$(echo "$out" | grep -c global) globals; virtual input: $(echo "$out" | grep -o 'virtual_[a-z]*' | tr '\n' ' '); $(tr '\n' ' ' < "$LOG/launch-probe-personal.out")"; fi launch personal "havoc" > "$LOG/launch-havoc-personal.out" 2>&1 echo "GT KEY-FOCUS-PERSONAL" wait_for 20 grep -q '^zone=personal' "$RT/kryptik/focus" && pass "second-zone-window" "$(tr '\n' ' ' < "$RT/kryptik/focus")" || fail "second-zone-window" "$(cat "$LOG/launch-havoc-personal.out" | tr '\n' ' '); $(zone_why personal)" -# --- zone 0 runs no user application (ADR-003) ---------------------------- -# With personal's terminal up, every process is read. One in a zone's cgroup -# is that zone's. Any other must be one of zone 0's own programs (s6 and its -# services, the session, a zone's launcher and proxy) or this test's own -# shell tree. An interpreter counts only running one of Kryptik's scripts, the -# terminal only as the chrome's, around its menu or a launch. +# --- zone 0 runs no user application (ADR-003) ------------------------------ ppid_of() { awk '/^PPid:/ { print $2 }' "/proc/$1/status" 2>/dev/null; } lineage=" $$ "; p="$(ppid_of "$$")" while [[ -n "$p" && "$p" -gt 1 ]]; do lineage="${lineage}${p} "; p="$(ppid_of "$p")"; done @@ -347,6 +332,8 @@ in_test_tree() { # in_test_tree PID: this shell, an ancestor of it, or below o while [[ -n "$p" && "$p" -gt 1 ]]; do [[ "$lineage" == *" $p "* ]] && return 0; p="$(ppid_of "$p")"; done return 1 } +# With personal's terminal up: outside the zones' cgroups only zone 0's own +# programs and this test's shell tree may run. foreign=(); zoned="" for d in /proc/[0-9]*; do p="${d#/proc/}" @@ -358,6 +345,7 @@ for d in /proc/[0-9]*; do in_test_tree "$p" && continue cmd="$(tr '\0' ' ' < "$d/cmdline" 2>/dev/null)" read -r _ script _ <<<"$cmd" + # An interpreter only as one of Kryptik's scripts; havoc only around the chrome's menu or a launch. case "$exe" in /usr/bin/s6-*|/usr/sbin/s6-*|/usr/libexec/s6-*|/usr/sbin/udevd|/usr/sbin/agetty|/usr/bin/seatd|/usr/bin/kryptikd|/usr/bin/kryptik-wlproxy|/usr/bin/kryptik-launch|/usr/bin/dwl|/usr/bin/sleep) ;; /usr/bin/bash|/usr/bin/python3) @@ -372,11 +360,9 @@ done stop_zone personal -# --- clipboards: per zone, until the zone 0 gesture ----------------------- -# A zone's clipboard lives in its launcher, so both zones stay up across the -# gesture, each running one command that talks to its broker through -# broker-client.py and logs the answers; untrusted waits for its payload to leave. +# --- clipboards: per zone, until the zone 0 gesture ------------------------- BC=/usr/lib/kryptik/guest-tests/broker-client.py +# A zone's clipboard lives in its launcher, so both zones stay up across the gesture. mark clip untrusted launch_plain untrusted "sh -c 'python3 $BC clipboard-set text/plain from-untrusted; echo SET-DONE; python3 $BC clipboard-wait-empty 90; echo WAIT-DONE'" > "$LOG/clip-set.out" 2>&1 wait_for 15 grep -q SET-DONE /var/log/kryptik/zone-untrusted.log @@ -393,14 +379,13 @@ wait_for 30 grep -q 'clipboard-empty' /var/log/kryptik/zone-untrusted.log; empti if [[ "$second" == *from-untrusted* && "$emptied" = 0 ]]; then pass "clipboard-moved" "personal holds the one payload the gesture moved, and untrusted's own broker answers empty: moved, not copied"; else fail "clipboard-moved" "personal: $(echo "$second" | tail -2 | tr '\n' ' '); untrusted: $(since_mark clip untrusted | tail -3 | tr '\n' ' ')"; fi stop_zone untrusted; stop_zone personal -# --- transfers: the user decides -------------------------------------------------------- +# --- transfers: the user decides -------------------------------------------- launch work "havoc" > /dev/null 2>&1 # work must be running to receive # policy first: untrusted names no destination mark trf0 untrusted launch_plain untrusted "sh -c 'echo nope > \$HOME/x.txt; python3 $BC transfer work x.txt \$HOME/x.txt'" > /dev/null 2>&1; sleep 3 [[ "$(since_mark trf0 untrusted)" == *"does not name"* ]] && pass "transfer-policy" "untrusted -> work refused by policy, with no question asked" || fail "transfer-policy" "$(since_mark trf0 untrusted | tail -2 | tr '\n' ' ')" -# watcher.lock is the chrome's watcher (kryptikd consent.rs), not a question. -questions() { # every entry in the consent directory except the watcher lock +questions() { # every consent entry except the chrome's watcher.lock (consent.rs) local f for f in /run/kryptik-consent/* /run/kryptik-consent/.[!.]*; do [[ -e "$f" ]] || continue @@ -410,9 +395,8 @@ questions() { # every entry in the consent directory except the watcher lock } [[ -z "$(questions)" ]] && pass "no-question-for-policy-refusal" || fail "no-question-for-policy-refusal" "$(questions | tr ' ' ' ')" -# dev -> work: allowed by policy, asked of the user, who types the code the -# question's window shows. Zone 0 finds it beside the question, which no zone sees. -consent_code() { # consent_code FROM TO: the code the question FROM asks about TO shows, once it shows one +# dev -> work: allowed by policy, so the user is asked and types the code the question shows. +consent_code() { # consent_code FROM TO: that question's code, from the file beside it, which no zone can read local a n=30 while [[ "$n" -gt 0 ]]; do for a in /run/kryptik-consent/*.ask; do @@ -448,8 +432,7 @@ n=40; while [[ "$n" -gt 0 ]] && [[ "$(since_mark trf2 dev)" != *ok* && "$(since_ out="$(since_mark trf2 dev)" [[ "$out" == *"refused by the user"* ]] && pass "plain-y-refused" "y without the code is a refusal" || fail "plain-y-refused" "$(echo "$out" | tail -2 | tr '\n' ' ')" [[ -e "$R/work/incoming/report2.txt" ]] && fail "denied-file-absent" "the refused file landed anyway" || pass "denied-file-absent" "nothing landed" -# The broker removes .ask and .answer at once; the chrome's watcher removes its -# .dialog on its next one-second pass, so wait for that. +# The chrome's watcher removes its .dialog on its next one-second pass: wait for it. n=20; while [[ "$n" -gt 0 && -n "$(questions)" ]]; do n=$((n - 1)); sleep 1; done [[ -z "$(questions)" ]] && pass "consent-cleaned" "no question left behind" || fail "consent-cleaned" "$(questions | tr ' ' ' ')" diff --git a/tools/desktop/wlprobe.c b/tools/desktop/wlprobe.c index ea780a17..193ed3f8 100644 --- a/tools/desktop/wlprobe.c +++ b/tools/desktop/wlprobe.c @@ -1,7 +1,7 @@ /* wlprobe: list the Wayland globals a client is offered, and try to bind one. - * A raw-socket client without libwayland (wire format: compositor/wlproxy's - * wire.rs). In a zone it shows what the zone's proxy lets through; in zone 0, - * the compositor's full set. + * A raw-socket client without libwayland (wire format: compositor/wlproxy/src/wire.rs). + * In a zone it shows what the zone's proxy lets through; in zone 0, the + * compositor's full set. * * wlprobe list print "global " per global * wlprobe bind INTERFACE list, then bind INTERFACE by its offered name @@ -119,8 +119,7 @@ static struct global globals[256]; static int nglobals; static int errored; -/* oversize: the window's objects, by the ids this client gives them. A new - * id must be the next unused one (the registry is 2, its sync 3). */ +/* oversize's object ids: each new id is the next unused one (the registry is 2, its sync 3). */ enum { COMPOSITOR = 4, SHM, WM_BASE, SURFACE, XDG_SURFACE, TOPLEVEL }; static int oversize, charge, drawn, draw_failed, extra, conf_w, conf_h, closed; static int askfs, conf_fs; @@ -368,8 +367,7 @@ static int bind_global(const char *iface, uint32_t id) return send_msg(2, 0, b, n); /* wl_registry.bind */ } -/* Map one window and answer each configure with an oversized buffer until - * `seconds` pass or the compositor closes it. */ +/* Map a window and answer each configure oversized, for `seconds` or until it is closed. */ static int hold_oversize(int more, int seconds, const char *title) { unsigned char b[280]; /* a 255-byte title, its length and padding */ @@ -469,8 +467,7 @@ int main(int argc, char **argv) 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. */ + /* A hidden global's name is unknown to a filtered client: guess 1, refused either way. */ const char *want = argv[2]; uint32_t name = 1, version = 1; for (int i = 0; i < nglobals; i++) if (!strcmp(globals[i].iface, want)) { name = globals[i].name; version = globals[i].version; }