Files
gex/test/native/textpatch.test.js
velzie b3621e0204 browser: keep guarded translations of code made writable and executable
A program that patches its own code with mprotect(RWX) around each write
(an old MinGW runtime's pseudo-relocations: one per relocation, the whole
.text each time) retired the whole section per patch, the relocator's own
code with it, which then came back to translation once per patch: a
process that does this for thousands of relocations crawled through
startup for minutes.

The kernel now notes which retired ranges were retired by being made
writable while staying executable (MapLog::writable_code). On x86, the
browser host keeps the pages of those ranges whose translations were all
made while the page was writable and executable (volatile, so each block
checks its bytes against a snapshot) and that hold no unit code; every
other page is retired as before. The native host keeps no translation of
writable code and retires them all. The diagnostic retire counts now
count only pages retired.

test/native/textpatch: 300 patches of one function from code on the next
page of its section; the last patch is seen natively and on the page,
warm and cold, and the patching code does not come to translation again
for each patch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_e0aa53aa95eb4b86910519b4d14812a5
(cherry picked from commit 030864228b7a57299ada7408dac377148320c774)
2026-11-11 09:15:07 -05:00

38 lines
2.1 KiB
JavaScript

// A program that patches its own code one write at a time, its section made
// writable and executable around each write and the code doing it in that
// section (fixtures/native/textpatch: an old MinGW runtime's pseudo-
// relocations): the patched function's last patch is seen, natively and on
// the page; and on the page the code doing the patching is not translated
// again for each patch (translations made while its page was writable and
// executable check their bytes, and are kept when it is made so again),
// which said "came to translation 64 times" and ran each patch through a
// fresh translation of the loop.
import { native, trim, tail } from '../lib/native.mjs';
export const meta = { hosts: ['node'], arches: ['x86'], timeout: 600 };
const N = 300;
const SUM = `textpatch before 1234567 after ${N - 1}`;
const lines = (out) => out.split('\n').filter((l) => l.startsWith('textpatch ')).join('\n');
export default async function (t) {
const n = native(t);
const d = n.dir('textpatch');
await n.cc(['-O1', '-static', '-o', `${d}/textpatch`, n.src('textpatch/textpatch.c')]);
const r = await n.gexc(['run', `${d}/textpatch`, String(N)], { timeout: 180000 });
if (trim(r.stdout) === 'textpatch skip') t.skip('the compiler laid getter out otherwise');
t.eq('native: the last patch is seen', trim(r.stdout), SUM, tail(r.stderr));
const g = n.g;
for (const p of ['gex_browser.x86.shared.wasm', 'gex_kernel.x86.wasm']) g.pub(p);
// Warm: `getter` and the loop translated before the first patch; cold:
// only the loop.
for (const how of ['warm', 'cold']) {
const p = await g.run(n.seed(), `${d}/textpatch`, how === 'cold' ? [String(N), 'cold'] : [String(N)], { files: { '/g/textpatch': `${d}/textpatch` }, argv0: '/g/textpatch', env: { GEX_SHARED: '1' }, timeout: 300000 });
t.eq(`page, ${how}: the last patch is seen`, lines(p.stdout), SUM, tail(p.stderr));
t.check(`page, ${how}: the patching code is not translated again a patch`, !/came to translation \d+ times|retired \d+ times|LIVELOCK/.test(p.stdout + p.stderr), tail(p.stderr));
}
n.clean();
}