Vestibule/BLOG.md

13 KiB
Executable File

From IEXPLORE.EXE to Rust: Rewriting a 2001 Kiosk Browser

By Jeremy Anderson — dcos.net — info@dcos.net

In December 2001, I was eighteen, coding on co-op while in school for my first employer — a Boston-area MSP and programming company. The company serviced medical offices, and one recurring problem was the lobby computer: patients waiting for appointments would sit down at it and immediately start looking for things they should not be looking at, or worse, closing the browser and poking around the rest of Windows. The receptionists were tired of babysitting it. My job was to make the computer bulletproof enough to leave alone.

I shipped a solution in Visual Basic 6. It was, in retrospect, a remarkable little artifact of late-90s teenage ingenuity — and twenty-five years later, I decided to rewrite it properly.

What the original did

The genius of the 2001 version was brute force. It embedded the Internet Explorer COM control (SHDocVw.dll) in a maximized VB6 form with ControlBox = False and WindowState = 2 (maximized). It called SetWindowPos with HWND_TOPMOST to keep the window on top of everything. And — this is the part that made it actually work — it called SystemParametersInfo(97, True), the legendary undocumented SPI_SCREENSAVERRUNNING flag that convinced Windows 9x it was running a screensaver, which had the side effect of disabling Ctrl+Alt+Del, Alt+Tab, and the Windows key.

That alone was not enough. A determined user could still close the VB6 app and reach the desktop. So the app did something beautifully ruthless: on startup, it copied C:\Program Files\Internet Explorer\IEXPLORE.EXE to IEXPLORE.bak, then deleted the original. If the user managed to escape the kiosk, they would find no browser to escape to. On unlock, the app copied the backup back into place.

The unlock code lived in plaintext — hardcoded in the form source of the lobby build, read from C:\windows\system\smt.txt by the earliest iteration. The UI was black background, white text, Comic Sans. The menu was invisible by default and appeared only when the mouse touched the top strip of the screen. The delay loop before unlock was a raw GoTo counting loop, a CPU-burner that pegged the processor at 100% for half a second.

The scrubbed source of all three iterations ships with this repository under history/vb6-2001/.

It ran on Windows 95, 98, 98 SE, and ME. It explicitly refused to run on NT, 2000, or XP because SPI_SCREENSAVERRUNNING does nothing on NT-family kernels.

It worked. It ran in that lobby for years.

Why rewrite it

The original program is unusable today for a list of reasons:

  • It is VB6. The toolchain is dead. The runtime ships in Windows 11 only for legacy compatibility, and Microsoft has announced its removal.
  • It depends on SHDocVw.dll, which is Internet Explorer. IE is gone.
  • SPI_SCREENSAVERRUNNING was a Win9x-only trick. Modern Windows (NT kernel) ignores it entirely.
  • The IEXPLORE.EXE shell game is impossible on modern Windows due to filesystem permissions and Windows Resource Protection.
  • Plaintext unlock-code storage — hardcoded in the form source, or a plaintext file on disk — is indefensible.
  • The Comic Sans UI is accessibility-hostile.

But the core idea — a public-facing browser that holds the perimeter and never leaks session data between users — is more relevant than ever. Medical lobbies still have lobby computers. Libraries still have public terminals. Trade shows still have demo kiosks. The problem shape has not changed; only the implementation needs to.

The architectural decisions

A modern kiosk browser has to answer four questions:

  1. What renders the web content?
  2. How is the perimeter enforced?
  3. How is the operator UI protected?
  4. How is session data sanitized?

Rendering: LibreWolf ESR

The constraint was no Chromium. Tauri was rejected because on Windows it uses WebView2, which is Edge, which is Chromium. Electron was rejected for the same reason and for its 150 MB binary size. Servo was rejected because its web compat is still too inconsistent for arbitrary patient-portal content.

LibreWolf ESR is the right answer. It is Gecko-based (no Chromium), MPL 2.0 licensed, community-maintained, tracks Firefox ESR for security, has Mozilla telemetry stripped, and ships enterprise policies.json support for lockdown. It also navigated Mozilla's trademark policy already, which means redistribution is straightforward.

Perimeter: OS-level lockdown, not app-level

The original VB6 fought the OS from inside the app. It used Win32 API calls to disable Ctrl+Alt+Del. It renamed system executables. This was necessary on Windows 9x because the OS had no kiosk mode.

Modern operating systems do. Windows 10+ has AssignedAccess, which configures a device as a dedicated kiosk at the OS level — single auto-logon account, no shell, no escape. Linux has cage, a 3 MB wlroots-based compositor that boots straight into a single client with no window manager, no shell, no desktop.

The right architecture moves the perimeter enforcement out of the app and into the OS, where it belongs. Vestibule does not try to disable Ctrl+Alt+Del from inside the browser. AssignedAccess does that at the session level, far more reliably than any app-level hook ever could.

Operator UI: extension + Rust helper

The browser-level logic — URL filtering, hidden menu, session reset, unlock dialog — lives in a WebExtension. This is the natural unit of composition for Firefox-family browsers and gives us webRequest blocking, browsingData sanitization, and idle detection for free.

The OS-level logic — password verification, power event detection, signaling the supervisor — lives in a tiny Rust binary called usher. It speaks Firefox Native Messaging (a stable, supported stdio-JSON protocol) to the extension. The binary is 1.2 MB. The extension is 6 KB. Together they replace what would have been a 150 MB Electron bundle.

Session sanitization: three layers, defense in depth

The product contract is: a kiosk must never persist patient data across sessions. Three enforcement layers:

  1. Block all save paths. policies.json disables form history, password manager, master password creation, Pocket, Screenshots, Firefox Accounts, search suggestions, extension installs, popup blocking override, and protocol handler registration. SanitizeOnShutdown wipes everything on browser exit.

  2. Reset on trigger events. The extension's resetSession() invokes browser.browsingData.remove() with all eleven data types on idle timeout, wake-from-sleep, unlock, and manual reset.

  3. OS-level defense. AssignedAccess and cage ensure the browser cannot be closed or escaped. The dedicated vestibule-profile directory isolates the kiosk from any user profile data.

The 2001 gestures that survived

Two original design decisions carry forward unchanged.

The top-edge hidden menu. The original VB6 form toggled its menu in picAddress_MouseMove, based on Y <= 10 (twips — effectively the top edge of the screen). The modern equivalent is a content script that listens for mousemove and shows a fixed-position overlay when clientY <= 3 pixels. Same gesture. Same UX rationale: the menu is invisible by default, appears on a deliberate gesture, and disappears on mouse-leave. Patients never see it. Operators always find it.

The password unlock. The original prompted for an unlock code and compared it to the contents of smt.txt. The modern equivalent prompts for an unlock code and verifies it against an Argon2id hash stored at 0600 by usher. The flow is identical; the storage and verification are not.

What did not survive

The IEXPLORE.EXE shell game. Modern Windows filesystem permissions make this impossible. The replacement is a URL allowlist enforced via webRequest.onBeforeRequest in the extension. Stronger, safer, and does not require renaming system executables.

The CPU-melting delay loop. The original used a GoTo counting loop (beginn: time = time + 1 ... GoTo beginn) to burn half a second before unlock. The modern equivalent is tokio::time::sleep(Duration::from_millis(500)). Same delay, 0% CPU.

Plaintext password storage. usher stores the Argon2id hash (64 MiB memory cost) in a file owned and readable only by the kiosk user — mode 0600 on Linux, default user ACL on Windows. Keyring daemons are unavailable on minimal kiosk compositors like cage, so file permissions are the enforcement boundary. The password itself is never written anywhere.

The Comic Sans UI. The hidden menu uses system fonts (-apple-system, "Segoe UI", system-ui, sans-serif). A retro 2001 theme is a Phase 5 option for those who want the nostalgia.

Power state awareness

The original VB6 did not handle sleep because Windows 9x kiosks typically did not sleep. Modern hardware does. Laptops used as kiosks suspend on lid close. Mini-PCs suspend on idle. Tablets suspend on inactivity.

Vestibule subscribes to the OS's power events. On Linux, usher connects to D-Bus and listens for org.freedesktop.login1.Manager.PrepareForSleep(false), which fires about two seconds after the system wakes. On Windows, usher registers for WM_POWERBROADCAST and listens for PBT_APMRESUMEAUTOMATIC.

On wake, usher emits a Wake event to the extension, which calls resetSession("wake:suspend"). The same path fires on idle timeout. Both go through one code path, so behavior is identical regardless of trigger.

The decision to detect rather than block sleep is deliberate. Blocking sleep fights the OS — it causes battery drain on mobile kiosks, thermal issues on fanless hardware, and interferes with Windows Update overnight reboots. The OS power profile is the right place to decide whether a device sleeps; that is a deployment-time decision, not an app-runtime one. Vestibule reacts correctly when the OS does sleep, and stays out of the way when it does not.

What I learned doing this

The 2001 version took me about three weeks of evenings. I knew nothing about Win32 API calls, COM interop, or filesystem security. I learned all of it by reading Declare Function examples on Planet Source Code and copying patterns I did not fully understand. The code was procedural, repetitive, and full of On Error GoTo cascades that existed to patch around the IEXPLORE.EXE rename failing halfway. It worked because the problem was small and the OS was permissive.

The 2026 version took about a week of focused work. I know Rust, I know Firefox extension APIs, and I know what the OS primitives actually do. The code is threaded, table-driven, and structured around a message-passing channel that makes the data flow obvious. It works because the abstractions are right, not because I patched around enough edge cases.

The instinct that survived intact is the one the original reviewer called out: dig underneath the standard UI and manipulate the environment directly. The 2001 version did this with Win32 API calls. The 2026 version does it with D-Bus subscriptions, enterprise policy files, and a message-passing channel between browser and helper. The implementation is completely different. The instinct is the same.

What comes next

Vestibule 1.2 ships the working core: real Argon2id unlock with file storage at 0600, a dedicated unlock popup window, Windows power event detection, the librewolf.overrides.cfg that selectively relaxes LibreWolf's resistFingerprinting for SSO flows, per-origin preservation for the data persistence allowlist — and, on top of that, the OS-level provisioning wizards (an AssignedAccess setup wizard on Windows with a Shell Launcher step-down, a cage systemd unit on Linux where the unit IS the graphical session — no display manager at all), full deprovisioning on both platforms, and the packaging itself: Inno Setup on Windows and a Flatpak on Linux that acts as a self-describing deployment kit. Since 1.2.2 the perimeter also extends to the web itself: the URL policy blocks every domain by default and opens only what the operator safelists — domain-based matching, with the home page guaranteed reachable so the kiosk can never lock itself out.

Phase 3 adds crash auto-restart integration testing, telemetry hooks, and the always-on power-inhibit mode. Later phases: Flathub distribution, remote management, and the retro 2001 theme.

One platform decision is already settled: Vestibule targets Linux first. The Windows stack works and stays CI-validated, but its binaries ship unsigned — a code-signing certificate is an unfunded line item unless a donation covers it — and Linux has no signing gate to care about.

Phase 5+, deferred: webcam presence detection. When a webcam is present, usher captures a baseline face embedding at session start and watches for two conditions: no face visible for N seconds (user walked away) or face embedding mismatch (different user approached). Either triggers an immediate session reset, faster than the idle timeout. Privacy review precedes any biometric code. Frames are processed in-memory only; embeddings never leave the local process; the webcam LED is respected.

Jeremy Anderson, 2026. dcos.net. info@dcos.net.