Flash pages no longer kill the daemon; a crash is no longer a loop; whole pages stay painted

Owner: "I can't click any links on lite.duckduckgo.com". The taps arrived -
the daemon log shows each one landing on its <A> and the navigation starting.
Both pages tapped were Flash pages, and loading either killed the daemon; the
card then reconnected to the new daemon and reopened its last address, so to
the user the results page just blinked and stayed.

Engine (third_party/uxp 0048d50fa9 and 628ee50c99): a plugin
embedded the ordinary way (wmode=window, the default) is "windowed"; the engine
then creates a child-window widget and calls into it unchecked, and this
offscreen build has none - null dereference, SIGSEGV. With no window to give,
the instance is now windowless. Every Flash test of ours had wmode=opaque.
Also: the Flash child no longer passes the bundled library path on to the shell
Flash spawns (one crash report per Flash page).

Daemon, crash guard (JihadCrashGuard.h): every top-level load that starts is
noted in the state directory with how many daemons in a row died on that
address; a clean exit removes the note. An address that took two daemons down
within ten minutes is not opened a third time on the card's request - a short
page says so and offers Try again. One refusal; the link, a reload or the
address bar loads it. The loop this breaks produced about forty crash reports
in a few minutes.

Daemon, paint region (owner: "pages only load when observed ... the entire page
should stay rendered at once unless it's very long"): the headroom around the
viewport was a fraction of the viewport, whatever the buffer could hold. Now a
page that fits the row budget is painted whole, so a scroll never uncovers
anything and needs no repaint, and a longer page gets the full budget, most of
it ahead of the pan. The 2048-row cap and the viewport-wide region are
unchanged; lifting them needs an adapter change and is next.

push-engine-update.sh also pushes plugin-container, and has --daemon-only.
Test hooks: page:<steps> runs steps on the launched page; zZ,X,Y zooms/scrolls
through the adapter.

On the device, all three variants on libxul ddf8cc70, daemon 70796680,
plugin-container 0c0e15d2: both Flash pages render their movies with the daemon
keeping its PID and no crash report; killing the daemon twice mid-load gets the
"This page stopped the browser" page on the third open; a 1583-row page is
painted as one 768x1583 frame and scripted scrolling over it logs no uncovered
rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcrVQFRMC2ANGDfVaRG9Ed
This commit is contained in:
NotAlexNoyleandClaude Fable 5.1 committed 2026-10-05 15:46:45 -04:00
1 parent b38a87fcd9
commit 3218caadd7
8 files changed
+227 -22

No files matched your search

+29 -14
View File
@@ -17,8 +17,12 @@
# (A changed script is picked up at the next start: the daemon drops the engine's compiled-
# script cache when the bundle's files change. See DropStaleStartupCache in EngineHost.cpp.)
#
# Usage: build/webos-oe/push-engine-update.sh [enyo] [mochi] [mojo]
# Usage: build/webos-oe/push-engine-update.sh [--daemon-only] [enyo] [mochi] [mojo]
# --daemon-only push just the daemon binary (and this project's chrome scripts): the loop
# after a daemon-only rebuild, without re-sending 45 MB of libxul per variant.
set -euo pipefail
DAEMON_ONLY=0
if [ "${1:-}" = "--daemon-only" ]; then DAEMON_ONLY=1; shift; fi
HERE=$(cd "$(dirname "$0")" && pwd)
DIST="$HERE/out-arm/obj-jihad-goanna-arm/dist"
@@ -57,7 +61,12 @@ for f in libmozavutil.so libmozavcodec.so; do
[ -e "$DIST/bin/$f" ] || die "no $f at $DIST/bin — run build-goanna-arm.sh build"
cp -L "$DIST/bin/$f" "$TMP/$f"
done
"$STRIP" "$TMP/libxul.so" "$TMP/jihad-browserserver" "$TMP/libmozavutil.so" "$TMP/libmozavcodec.so" 2>/dev/null || true
# plugin-container is the NPAPI (Flash) child process: its own binary, built with the engine.
# It was not pushed until 2026-10-05, when a fix in it would otherwise have stayed on the host.
[ -e "$DIST/bin/plugin-container" ] || die "no plugin-container at $DIST/bin — run build-goanna-arm.sh build"
cp -L "$DIST/bin/plugin-container" "$TMP/plugin-container"
"$STRIP" "$TMP/libxul.so" "$TMP/jihad-browserserver" "$TMP/libmozavutil.so" "$TMP/libmozavcodec.so" "$TMP/plugin-container" 2>/dev/null || true
PC_MD5=$(md5sum "$TMP/plugin-container" | cut -c1-32)
AVU_MD5=$(md5sum "$TMP/libmozavutil.so" | cut -c1-32)
AVC_MD5=$(md5sum "$TMP/libmozavcodec.so" | cut -c1-32)
XUL_MD5=$(md5sum "$TMP/libxul.so" | cut -c1-32)
@@ -93,21 +102,27 @@ for v in $VARIANTS; do
novacom run file://sbin/stop -- "$J" >/dev/null 2>&1 || true
# Push to a temp name then move: the daemon may respawn mid-push; an atomic-ish rename keeps
# a half-written libxul from ever being what upstart execs.
say "[$v] pushing libxul.so"
push_verified "$TMP/libxul.so" "$HL/.libxul.so.new" "$XUL_MD5"
say "[$v] pushing jihad-browserserver"
push_verified "$TMP/jihad-browserserver" "$HL/.jihad-browserserver.new" "$BS_MD5"
say "[$v] pushing goanna.js"
push_verified "$TMP/goanna.js" "$HL/.goanna.js.new" "$PREFS_MD5"
say "[$v] pushing ffvpx (libmozavutil, libmozavcodec)"
push_verified "$TMP/libmozavutil.so" "$HL/.libmozavutil.so.new" "$AVU_MD5"
push_verified "$TMP/libmozavcodec.so" "$HL/.libmozavcodec.so.new" "$AVC_MD5"
novacom run file://bin/mv -- "$HL/.libxul.so.new" "$HL/libxul.so"
if [ "$DAEMON_ONLY" = 0 ]; then
say "[$v] pushing libxul.so"
push_verified "$TMP/libxul.so" "$HL/.libxul.so.new" "$XUL_MD5"
say "[$v] pushing goanna.js"
push_verified "$TMP/goanna.js" "$HL/.goanna.js.new" "$PREFS_MD5"
say "[$v] pushing ffvpx (libmozavutil, libmozavcodec)"
push_verified "$TMP/libmozavutil.so" "$HL/.libmozavutil.so.new" "$AVU_MD5"
push_verified "$TMP/libmozavcodec.so" "$HL/.libmozavcodec.so.new" "$AVC_MD5"
say "[$v] pushing plugin-container"
push_verified "$TMP/plugin-container" "$HL/.plugin-container.new" "$PC_MD5"
novacom run file://bin/mv -- "$HL/.libxul.so.new" "$HL/libxul.so"
novacom run file://bin/mv -- "$HL/.goanna.js.new" "$HL/goanna.js"
novacom run file://bin/mv -- "$HL/.libmozavutil.so.new" "$HL/libmozavutil.so"
novacom run file://bin/mv -- "$HL/.libmozavcodec.so.new" "$HL/libmozavcodec.so"
novacom run file://bin/mv -- "$HL/.plugin-container.new" "$HL/plugin-container"
novacom run file://bin/chmod -- 755 "$HL/plugin-container"
novacom run file://bin/chmod -- 755 "$HL/libmozavutil.so" "$HL/libmozavcodec.so"
fi
novacom run file://bin/mv -- "$HL/.jihad-browserserver.new" "$HL/jihad-browserserver"
novacom run file://bin/mv -- "$HL/.goanna.js.new" "$HL/goanna.js"
novacom run file://bin/mv -- "$HL/.libmozavutil.so.new" "$HL/libmozavutil.so"
novacom run file://bin/mv -- "$HL/.libmozavcodec.so.new" "$HL/libmozavcodec.so"
novacom run file://bin/chmod -- 755 "$HL/libmozavutil.so" "$HL/libmozavcodec.so"
novacom run file://bin/chmod -- 755 "$HL/jihad-browserserver"
say "[$v] pushing this project's chrome scripts ($(echo $OWN | wc -w) files)"
for f in $OWN; do
+3
View File
@@ -25,6 +25,7 @@
#include "../goanna/JihadRuntimePaths.h" // the ONE runtime-state dir (T-057 / R8)
#include "../goanna/JihadCrashReport.h" // self-reporting fatal-signal dump
#include "JihadLunaService.h" // this variant's own palm:// service (IPC R4)
#include "../goanna/JihadCrashGuard.h" // a clean exit clears the crash guard's note
static JihadBrowserServer* g_server = nullptr;
@@ -352,6 +353,8 @@ int main(int argc, char** argv) {
server.run(); // GLib main loop (patched YapServer::run)
if (g_termSignal)
printf("[jihad-bs] signal %d — shutting the engine down cleanly\n", (int)g_termSignal);
// Leaving on purpose: whatever was loading is not to blame for this exit.
jihad::CrashGuardCleanExit();
jihad::LunaServiceStop();
g_server = nullptr;
} // every page destroyed here, before the runtime goes away
+63 -5
View File
@@ -12,6 +12,7 @@
#include "BrowserOffscreenInfo.h" // isis shmem header (from render/browserserver/Src)
#include "JihadLogo.h" // JIHAD_LOGO_B64 (app icon for about: pages)
#include "JihadRuntimePaths.h" // the ONE runtime-state dir (T-057 / R8)
#include "JihadCrashGuard.h" // do not reopen a page that just killed the daemon twice
#include "JihadCertStore.h" // webOS platform certificate store (R5)
#include <cstdio>
@@ -1012,6 +1013,47 @@ void BrowserPageGoanna::openUrl(const char* url) {
return;
}
mAliasUrl.clear();
// A page that took the last two daemons down is not opened a third time by itself: the
// card reopens its address whenever a daemon comes back, and that made a crash a loop
// (JihadCrashGuard.h). Shown once; the link on it, a reload or the address bar loads it.
if (jihad::CrashGuardShouldRefuse(url)) {
std::string safe;
for (const char* c = url; *c && safe.size() < 1200; ++c) {
switch (*c) {
case '&': safe += "&amp;"; break; case '<': safe += "&lt;"; break;
case '>': safe += "&gt;"; break; case '"': safe += "&quot;"; break;
default: safe += *c;
}
}
const std::string html =
"<!DOCTYPE html><html><head><meta charset=utf-8><title>Page stopped</title>"
"<meta name=viewport content='width=device-width, initial-scale=1'>"
"<style>body{font:18px/1.5 sans-serif;background:#1c1c1c;color:#e6e6e6;margin:0;padding:48px 36px}"
"h1{font-size:24px;margin:0 0 14px}p{margin:0 0 16px;color:#bdbdbd}"
"a{color:#7fb2e6}.u{word-wrap:break-word;color:#8cafbe;font-size:15px}</style></head><body>"
"<h1>This page stopped the browser</h1>"
"<p>It did so twice in a row, so it was not opened again automatically.</p>"
"<p class=u>" + safe + "</p>"
"<p><a id=retry href=\"" + safe + "\">Try again</a></p></body></html>";
// setHTML appends its argument to "data:text/html," as it is, so a '#' would start a
// fragment and cut the page off there (the first version of this page was blank for
// exactly that reason: its colours), and a '%' in the address would be decoded. Escape
// everything that is not plainly safe.
std::string enc; enc.reserve(html.size() * 2);
static const char* kHex = "0123456789ABCDEF";
for (unsigned char c : html) {
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
c == '-' || c == '_' || c == '.' || c == '~') {
enc += (char)c;
} else {
enc += '%'; enc += kHex[c >> 4]; enc += kHex[c & 15];
}
}
mAliasUrl = url;
setHTML(url, enc.c_str());
return;
}
jihad::CrashGuardNoteLoad(url);
// R6: a matching redirect rule hands the URL to the client (msgUrlRedirected)
// and, if it is a redirect, is not loaded in the browser at all.
if (applyRedirectRules(url)) return;
@@ -2990,14 +3032,30 @@ void BrowserPageGoanna::paintToSharedBuffer() {
// Bias the headroom toward the pan DIRECTION ("content blips in and out when I scroll",
// device 2026-08-02): the strip behind the pan is dead weight; the strip ahead is what
// the user is about to expose. Direction from the adapter scroll delta since last paint.
long ovAbove = h / 2, ovBelow = h;
if (adY > mPrevPaintScrollY + 8) { ovAbove = h / 8; ovBelow = h + h / 2; }
else if (adY < mPrevPaintScrollY - 8) { ovAbove = h + h / 2; ovBelow = h / 8; }
//
// HOW MUCH (owner, 2026-10-05: "pages only load when observed … the entire page should
// stay rendered at once unless it's very long"). The headroom used to be a fraction of the
// VIEWPORT (half above, one below), whatever the buffer could hold — so a page a screen
// and a half long was painted as a window that followed the finger, repainted on the way,
// and showed undrawn rows whenever the finger won. Now the whole row budget is spent:
// - a page that fits the budget is painted WHOLE, top to bottom. Nothing is ever
// uncovered and a scroll needs no repaint at all (setScrollPosition's coverage test
// finds every position covered) — the adapter just pans the frame it has;
// - a longer page gets the full budget around the viewport, most of it ahead of the pan.
long slackRows = maxRows - (long)h; if (slackRows < 0) slackRows = 0;
long ovAbove = slackRows / 3, ovBelow = slackRows - slackRows / 3;
if (adY > mPrevPaintScrollY + 8) { ovAbove = slackRows / 6; ovBelow = slackRows - ovAbove; }
else if (adY < mPrevPaintScrollY - 8) { ovBelow = slackRows / 6; ovAbove = slackRows - ovBelow; }
mPrevPaintScrollY = adY;
long lo = (bandY < adY ? bandY : adY) - ovAbove;
long hi = (bandY > adY ? bandY : adY) + h + ovBelow;
if (lo < 0) lo = 0;
if (contentRows > 0 && hi > contentRows) hi = contentRows;
if (contentRows > 0 && contentRows <= maxRows) {
lo = 0; hi = contentRows; // the whole page
} else {
// Headroom that falls off one end of the page is given to the other end.
if (lo < 0) { hi -= lo; lo = 0; }
if (contentRows > 0 && hi > contentRows) { lo -= (hi - contentRows); hi = contentRows; if (lo < 0) lo = 0; }
}
if (hi < lo + h) hi = lo + h; // never less than one viewport
if (hi - lo > maxRows) { // over budget: trim overscan…
lo = bandY - (maxRows - (long)h) / 3; // keep ~2/3 of the slack below the band
+4
View File
@@ -38,6 +38,7 @@
#include "nsIWebProgressListener.h"
#include "nsIURI.h"
#include "JihadRuntimePaths.h" // RuntimeProfileDir (Flash availability)
#include "JihadCrashGuard.h" // note each top-level load that starts
#include "nsIObserverService.h" // NotifyChrome: hand a page to a bundled JS component
#include "imgIEncoder.h" // PNG out of a frame (bookmark thumbnails)
#include "nsComponentManagerUtils.h" // do_CreateInstance
@@ -360,6 +361,9 @@ NS_IMETHODIMP PageChrome::OnStateChange(nsIWebProgress* aWebProgress, nsIRequest
nsCOMPtr<nsIChannel> ch = do_QueryInterface(aRequest);
if (ch) {
nsCOMPtr<nsIURI> u; ch->GetURI(getter_AddRefs(u)); if (u) u->GetSpec(mLinkUrl);
// A load the PAGE started (a tapped link, a redirect, script): the crash guard has to
// know about these too, or it would blame the address the card last asked for.
if (u && !mLinkUrl.IsEmpty()) jihad::CrashGuardNoteLoad(mLinkUrl.get());
// Capture the method: a POST content-nav is NOT re-driven (the body may already be on the
// wire — re-issuing would double the request, Codex F-262); only GET-class navs get the
// re-drive. The engine completes POSTs; the load watchdog clears the overlay if one stalls.
+109
View File
@@ -0,0 +1,109 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef JIHAD_CRASH_GUARD_H
#define JIHAD_CRASH_GUARD_H
// A page that kills the daemon must not be reopened forever.
//
// WHAT HAPPENED (2026-10-05). Two pages crashed the daemon while loading (a null dereference
// in the plugin code, since fixed). When the daemon dies the card reconnects to the new one
// and opens its address again — which is right for an ordinary restart, and here meant: load,
// crash, restart, load, crash … for as long as the card stayed open. About forty crash
// reports in a few minutes, and nothing on screen but a spinner.
//
// THE GUARD. Every top-level load that starts is noted in a small file in the state
// directory, with how many daemons in a row have died while that same address was the last
// one started. A clean shutdown removes the file. So a file found at start means the previous
// daemon did NOT exit cleanly, and names what it was loading. When the card then asks for an
// address that has already taken two daemons down within the last ten minutes, the daemon
// does not load it: it shows a short page saying so, with a link to try again. The refusal
// is used up by being shown — the link, a reload or a typed address loads the page for real.
//
// What it is not: a judgement about the page. One death is allowed to be coincidence (the
// second attempt is automatic); two in a row with the same address is a pattern.
#include "JihadRuntimePaths.h"
#include <stdio.h>
#include <string.h>
#include <string>
#include <time.h>
#include <unistd.h>
namespace jihad {
struct CrashGuardState {
bool loaded = false;
std::string prevUrl; // what the previous daemon was loading when it died uncleanly
int prevCount = 0; // how many daemons in a row died on it
long prevTime = 0;
bool refused = false; // the one refusal has been shown
};
inline CrashGuardState& CrashGuardGet() {
static CrashGuardState s;
if (!s.loaded) {
s.loaded = true;
const std::string path = RuntimeStatePath("last-load");
if (!path.empty()) {
if (FILE* f = fopen(path.c_str(), "r")) {
char head[64] = "", url[2048] = "";
if (fgets(head, sizeof head, f) && fgets(url, sizeof url, f)) {
url[strcspn(url, "\r\n")] = 0;
int n = 0; long t = 0;
if (sscanf(head, "%d %ld", &n, &t) == 2 && n > 0 && url[0]) {
s.prevUrl = url; s.prevCount = n; s.prevTime = t;
fprintf(stderr, "[jihad-bs] crash guard: the previous daemon did not exit cleanly; "
"it was loading a page (%d in a row on that address)\n", n);
}
}
fclose(f);
}
}
}
return s;
}
// A top-level load of `url` is starting. Records it as "what this daemon dies with, if it
// dies now". Never the fragment: the settings page rewrites its own.
inline void CrashGuardNoteLoad(const char* url) {
if (!url || !*url) return;
CrashGuardState& s = CrashGuardGet();
const std::string path = RuntimeStatePath("last-load");
if (path.empty()) return;
std::string u(url);
const size_t hash = u.find('#');
if (hash != std::string::npos) u.erase(hash);
if (u.size() > 2000) u.resize(2000);
const int n = (u == s.prevUrl) ? s.prevCount + 1 : 1;
if (FILE* f = fopen(path.c_str(), "w")) {
fprintf(f, "%d %ld\n%s\n", n, (long)time(nullptr), u.c_str());
fclose(f);
}
}
// Should the card's request for `url` be refused this once?
inline bool CrashGuardShouldRefuse(const char* url) {
if (!url || !*url) return false;
CrashGuardState& s = CrashGuardGet();
if (s.refused || s.prevCount < 2) return false;
std::string u(url);
const size_t hash = u.find('#');
if (hash != std::string::npos) u.erase(hash);
if (u != s.prevUrl) return false;
if ((long)time(nullptr) - s.prevTime > 600) return false;
s.refused = true;
fprintf(stderr, "[jihad-bs] crash guard: NOT reopening a page that stopped the browser %d times in a row\n",
s.prevCount);
return true;
}
// The daemon is exiting on purpose: nothing it had open is to blame.
inline void CrashGuardCleanExit() {
const std::string path = RuntimeStatePath("last-load");
if (!path.empty()) unlink(path.c_str());
}
} // namespace jihad
#endif // JIHAD_CRASH_GUARD_H
+9 -1
View File
@@ -59,6 +59,11 @@
bb.call("palm://com.palm.db/put", enyo.json.stringify({objects: [{_kind: "net.riverstonerelay.jihad-browser.bookmarks:1", title: "jihad test bookmark", url: "http://example.invalid/jihad-test", date: (new Date()).getTime()}]}));
window.__jihadTestBridges.push(bb);
log(name, "test bookmark added");
} else if (st.charAt(0) === "z") { // zZOOM,X,Y: zoom and scroll, as a pinch or a pan ends up
var zs = st.slice(1).split(",");
view.callBrowserAdapter("pageScaleAndScroll", [parseFloat(zs[0]), parseInt(zs[1], 10), parseInt(zs[2], 10)]);
log(name, "zoom/scroll " + st.slice(1));
wait = 1200;
} else if (st === "?") { // report the state now
state("now", function() {});
} else if (st.charAt(0) === "s") {
@@ -75,7 +80,9 @@
}
later(wait, function() { step(i + 1); });
}
app.openSettings();
// `page:<steps>` runs the same steps on the page the card was launched on, without
// opening the settings page first.
if (String(name).indexOf("page") !== 0) { app.openSettings(); }
later(8000, function() { state("opened", function() { step(0); }); });
},
// Print: open the dialog from the menu and report what the daemon finds. With an
@@ -124,6 +131,7 @@
later(12000, function() { try { app.$.appMenu.close(); } catch (e) {} look("12s"); log(name, "DONE"); });
}
};
scenarios.page = scenarios.settings;
function run() {
var p = enyo.windowParams || null;
if (!p || !p.jihadTest) { try { p = enyo.json.parse(window.PalmSystem.launchParams); } catch (e) { p = null; } }
+9 -1
View File
@@ -50,6 +50,11 @@
} else if (st === "b") { // a bookmark of the test's own, to have one to clear
enyo.jihad.dbPut([{_kind: enyo.jihad.kinds.bookmarks, title: "jihad test bookmark", url: "http://example.invalid/jihad-test", date: (new Date()).getTime()}]);
log(name, "test bookmark added");
} else if (st.charAt(0) === "z") { // zZOOM,X,Y: zoom and scroll, as a pinch or a pan ends up
var zs = st.slice(1).split(",");
app.$.view.callBrowserAdapter("pageScaleAndScroll", [parseFloat(zs[0]), parseInt(zs[1], 10), parseInt(zs[2], 10)]);
log(name, "zoom/scroll " + st.slice(1));
wait = 1200;
} else if (st === "?") { // report the state now
state("now", function() {});
} else if (st.charAt(0) === "s") {
@@ -64,7 +69,9 @@
}
later(wait, function() { step(i + 1); });
}
app.menuSettings();
// `page:<steps>` runs the same steps on the page the card was launched on, without
// opening the settings page first.
if (String(name).indexOf("page") !== 0) { app.menuSettings(); }
later(8000, function() { state("opened", function() { step(0); }); });
},
@@ -346,6 +353,7 @@
}
};
scenarios.page = scenarios.settings;
function run() {
var p = null;
try { p = enyo.json.parse(window.PalmSystem.launchParams); } catch (e) { p = null; }
+1 -1