#!/bin/sh
# Mirasim's Linux entry point. The AppImage's AppRun, the deb's .desktop entry and
# `mirasim-desktop` on $PATH all exec THIS file; the real Electron binary sits beside
# it as `<name>.bin`. It decides exactly one thing and then gets out of the way:
# whether Chromium can sandbox itself on this machine.
#
# Why it has to. Chromium sandboxes a renderer through one of two mechanisms:
#   1. an unprivileged user namespace it holds CAP_SYS_ADMIN inside, or
#   2. a setuid-root `chrome-sandbox` helper sitting next to the binary.
# An AppImage can never have (2) — its squashfs is mounted nosuid and its files are
# owned by whoever built it — so it lives entirely on (1). Ubuntu 24.04 took (1)
# away by default: with `kernel.apparmor_restrict_unprivileged_userns=1`, an
# UNCONFINED process that creates a user namespace is transitioned into the
# `unprivileged_userns` profile, which denies CAP_SYS_ADMIN inside it. Chromium
# falls back to (2), finds the helper un-setuid, and aborts the whole process:
#
#   FATAL:setuid_sandbox_host.cc(163)] The SUID sandbox helper binary was found,
#   but is not configured correctly. Rather than run without sandboxing I'm
#   aborting now. ...                                        (core dumped)
#
# It is a LAUNCH-PATH bug, not a machine-wide one, which is why it looked so
# arbitrary: double-clicking the AppImage in GNOME Files starts fine (the child
# inherits Files' own apparmor profile, which may create user namespaces), while
# the same file from a terminal — plain `unconfined` — dies before the first frame.
#
# So: keep the sandbox wherever it actually works, and spend --no-sandbox only where
# the alternative is not starting at all. The decision is a handful of file reads —
# no probe process, nothing that can hang a launch, nothing that touches the network.
#
# Overrides: MIRASIM_SANDBOX=1 keeps the sandbox no matter what, =0 always drops it.
# MIRASIM_LAUNCHER_DEBUG=1 prints the verdict. MIRASIM_LAUNCHER_PROC repoints the
# /proc reads (tests only — it is how the matrix below is exercised off-Ubuntu).

self=$(readlink -f "$0" 2>/dev/null) || self=$0
[ -n "$self" ] || self=$0
dir=$(dirname "$self")
bin="$self.bin"

if [ ! -x "$bin" ]; then
  echo "mirasim: missing $bin (the launcher must sit beside the Electron binary)" >&2
  exit 1
fi

# Node mode — the app spawns its own binary this way to run server.cjs and the
# askpass helper. argv is then a NODE command line, where a chromium switch is a
# hard error ("bad option"), so this path must stay byte-for-byte passthrough.
if [ -n "${ELECTRON_RUN_AS_NODE:-}" ]; then
  exec "$bin" "$@"
fi

proc=${MIRASIM_LAUNCHER_PROC:-/proc}

# First line of a file, without the NUL /proc/self/attr/current ends with; empty
# when the file is absent or unreadable.
first_line() {
  [ -r "$1" ] || return 0
  tr -d '\000' < "$1" 2>/dev/null | head -n 1
}

sandbox_works() {
  # (2) A setuid-root helper answers for every kernel: this is the deb's path on a
  # host without usable user namespaces (its postinst sets the bit). Inside an
  # AppImage the bit can never be set, so the test is simply false there.
  if [ -u "$dir/chrome-sandbox" ] && [ "$(stat -c %u "$dir/chrome-sandbox" 2>/dev/null)" = "0" ]; then
    return 0
  fi

  # (1) Otherwise it is the namespace sandbox or nothing.
  [ -e "$proc/self/ns/user" ] || return 1
  # Debian's toggle, and the per-user ceiling: either at 0 means no namespace at all.
  if [ "$(first_line "$proc/sys/kernel/unprivileged_userns_clone")" = "0" ]; then return 1; fi
  if [ "$(first_line "$proc/sys/user/max_user_namespaces")" = "0" ]; then return 1; fi
  # Ubuntu 24.04+. The namespace is still created — it just comes back stripped of
  # CAP_SYS_ADMIN, which is the half Chromium needs. A process already covered by a
  # profile (a file manager, an app launcher) keeps its privileges and is fine; a
  # plain unconfined one — every terminal — is not. An unreadable label reads as
  # unconfined on purpose: guessing "confined" here costs the user the whole app.
  if [ "$(first_line "$proc/sys/kernel/apparmor_restrict_unprivileged_userns")" = "1" ]; then
    case "$(first_line "$proc/self/attr/current")" in
      unconfined | '') return 1 ;;
    esac
  fi
  return 0
}

# An explicit --no-sandbox from the caller (the AppImage's own .desktop entry writes
# one) already says everything this script could add.
for arg in "$@"; do
  if [ "$arg" = "--no-sandbox" ]; then exec "$bin" "$@"; fi
done

case "${MIRASIM_SANDBOX:-}" in
  1) exec "$bin" "$@" ;;
  0) exec "$bin" --no-sandbox "$@" ;;
esac

if sandbox_works; then
  [ -n "${MIRASIM_LAUNCHER_DEBUG:-}" ] && echo "mirasim: chromium sandbox available — starting sandboxed" >&2
  exec "$bin" "$@"
fi

[ -n "${MIRASIM_LAUNCHER_DEBUG:-}" ] && echo "mirasim: no usable chromium sandbox (restricted user namespaces, no setuid helper) — starting with --no-sandbox" >&2
exec "$bin" --no-sandbox "$@"
