NetSurf: what GTK's images and libcurl's resolver needed

NetSurf (GTK 3) now starts under X and loads pages over HTTPS. Every icon
GTK draws goes gdk-pixbuf -> glycin -> a loader process -> a sealed memfd
-> GMappedFile, and each hop found something missing:

- mkaroot runs Alpine's package triggers in the root under bubblewrap.
  --no-scripts had skipped them, so there was no MIME cache, pixbuf loader
  list, compiled GSettings schemas, GIO module cache, font cache or busybox
  applet links.
- /usr/bin/bwrap is a stand-in that parses bubblewrap's options and runs
  the program with no sandbox: glycin starts its loaders only through bwrap,
  and real bwrap needs namespaces. schos-base replaces bubblewrap for it.
- ioctl: FIOCLEX, FIONCLEX, FIONBIO and FIOASYNC on any descriptor (Rust's
  std uses them; pipes and sockets answered ENOTTY).
- capget/capset: root has every capability, others none.
- socketpair accepts SOCK_DGRAM and SOCK_SEQPACKET, carried as a stream.
- waitpid and setpgid match children against the thread group leader, so a
  thread can wait for the process it spawned (ECHILD before).
- memfd: fstat reports the ftruncate length, not the page-rounded one; file
  seals (F_ADD_SEALS, F_GET_SEALS, MFD_ALLOW_SEALING) are recorded and
  enforced on later writes, truncation and writable mappings; MFD_CLOEXEC
  is honoured; read-only MAP_PRIVATE maps are allowed.
- recvfrom honours O_NONBLOCK and MSG_DONTWAIT. musl's resolver reads its
  socket until EAGAIN and blocked for ever after the first DNS answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-25 04:10:26 +01:00
commit 393bf674f5
11 changed files with 251 additions and 21 deletions

View file

@ -168,6 +168,49 @@ Section "ServerLayout"
EndSection
XEOF
# bwrap, without the sandbox. glycin -- which loads every image gdk-pixbuf is
# asked for, so every GTK icon -- runs its loaders under bubblewrap and has no
# other way to start them. Real bwrap needs user and mount namespaces, which
# SchOS does not have. This takes the same arguments, keeps what they say
# about the environment and the directory, and runs the program as it is:
# no isolation at all, and it says so in its name. It replaces bubblewrap's own
# /usr/bin/bwrap, which glycin depends on and which cannot run here.
mkdir -p "$P/usr/bin"
cat > "$P/usr/bin/bwrap" <<'BEOF'
#!/bin/sh
# SchOS: bubblewrap's command line, with no sandbox behind it. SchOS has no
# namespaces; what the options say about the environment and the working
# directory is honoured, and everything about isolation is ignored.
clear= envs=
while [ $# -gt 0 ]; do
case "$1" in
--clearenv) clear=1; shift ;;
--setenv) envs="$envs $2=$3"; shift 3 ;;
--unsetenv) unset "$2"; shift 2 ;;
--chdir) cd "$2" || exit 1; shift 2 ;;
--bind|--bind-try|--ro-bind|--ro-bind-try|--dev-bind|--dev-bind-try|\
--symlink|--bind-data|--ro-bind-data|--file|--chmod) shift 3 ;;
--dev|--proc|--tmpfs|--dir|--mqueue|--remount-ro|--seccomp|--add-seccomp-fd|\
--uid|--gid|--hostname|--cap-add|--cap-drop|--lock-file|--sync-fd|\
--info-fd|--json-status-fd|--block-fd|--userns-block-fd|--perms|--size|\
--argv0|--exec-label|--file-label|--userns|--userns2|--pidns) shift 2 ;;
--unshare-*|--share-net|--die-with-parent|--new-session|--as-pid-1|\
--disable-userns|--assert-userns-disabled) shift ;;
--) shift; break ;;
-*) echo "bwrap: $1: not understood (SchOS runs bwrap without a sandbox)" >&2
exit 1 ;;
*) break ;;
esac
done
[ $# -gt 0 ] || { echo "bwrap: no command" >&2; exit 1; }
# shellcheck disable=SC2086
if [ -n "$clear" ]; then exec env -i PATH="$PATH" $envs "$@"; fi
# shellcheck disable=SC2086
[ -n "$envs" ] && exec env $envs "$@"
exec "$@"
BEOF
chmod 755 "$P/usr/bin/bwrap"
# Mount points and runtime directories are alpine-baselayout's; an empty
# directory here only duplicates it.
for d in dev proc sys run tmp srv; do
@ -197,7 +240,7 @@ cp "$PKG/post-install" "$PKG/post-upgrade"
--info url:https://github.com/Nexoniarz/SchOS \
--info origin:schos-base \
--info "depends:alpine-baselayout busybox musl apk-tools zsh" \
--info "replaces:alpine-release alpine-baselayout zsh" \
--info "replaces:alpine-release alpine-baselayout zsh bubblewrap" \
--files "$P" \
--script "post-install:$PKG/post-install" \
--script "post-upgrade:$PKG/post-upgrade" \
@ -234,6 +277,41 @@ ROOT_PI=$ROOT sh -c '
done
grep -qw schos "$ROOT_PI/etc/hosts" || printf "127.0.1.1\tschos\n" >> "$ROOT_PI/etc/hosts"'
# The triggers --no-scripts skipped, and they are not optional: they build the
# caches half the desktop reads. Without shared-mime-info's, GLib cannot tell a
# PNG from anything else, so gdk-pixbuf -- which hands images to glycin by MIME
# type -- refused every icon and NetSurf died on its first toolbar button.
# Likewise gdk-pixbuf's loader list, GLib's compiled schemas and GIO modules,
# GTK's input methods, the font caches and busybox's applet links.
#
# They run in the root as /, which is what bubblewrap is for: no chroot, so no
# root needed. apk names each script by the hex form of the checksum the
# triggers database keys in base64, and passes the paths it watches as
# arguments, globs expanded.
if command -v bwrap >/dev/null 2>&1; then
TRIG=$(mktemp -d)
tar -xzf "$ROOT/lib/apk/db/scripts.tar.gz" -C "$TRIG" --wildcards '*.trigger' 2>/dev/null || true
mkdir -p "$ROOT/tmp"
for t in "$TRIG"/*.trigger; do
[ -e "$t" ] || continue
hex=${t##*.X1}; hex=${hex%.trigger}
q=Q1$(python3 -c 'import base64,sys; print(base64.b64encode(bytes.fromhex(sys.argv[1])).decode())' "$hex")
paths=$(grep "^$q " "$ROOT/lib/apk/db/triggers" | cut -d' ' -f2-)
cp "$t" "$ROOT/tmp/apk-trigger"
# --clearenv: the host's environment is not the root's. A Nix shell
# exports GDK_PIXBUF_MODULE_FILE, and the loader cache went there.
bwrap --bind "$ROOT" / --dev /dev --proc /proc --clearenv \
--setenv PATH /usr/sbin:/usr/bin:/sbin:/bin --setenv HOME /root \
/bin/sh -c 'a=; for g in '"$paths"'; do for p in $g; do [ -e "$p" ] && a="$a $p"; done; done
exec /bin/sh /tmp/apk-trigger $a' \
|| echo "mkaroot: trigger $(basename "$t") failed" >&2
rm -f "$ROOT/tmp/apk-trigger"
done
rm -rf "$TRIG"
else
echo "mkaroot: no bwrap -- triggers not run; image and MIME caches are missing" >&2
fi
# The shared scratch directories, world-writable and sticky as baselayout
# means them. apk running as an ordinary user on the build host lands them as
# 0755, and with the kernel reporting real modes that is what tmux and every