Files
bare-operating-system/kernel/lib/init/fragments/30-init-kernel-extensions.js
T
Raven Scott 071edccfb3 Expand bounded awk/expr/test toward Issue 7; refresh man, profile 1.0.18,
posix matrix/dashboard, and syscalls/process_table schema alignment (v8).

Booter: replication_operator_sketch/corestore hints, HRPC allowlist tests,
Protomux cap channel 65536-byte bound + export, Wasm posix_profile_peek,
swarm-disk and security_posture docs.

Coreutils/kernel: pkg-swarm-index pathCapabilityEnvelopeVerify on get;
pathcap-verify --trusted failure hint; rebuild bins and sync seeder.

Docs: KERNEL_CONTRACT, kernel-extensions, capabilities index, environment
appendix (warm-cache tuning, cap channel, Wasm env), handbook observability,
vault threat model (multisig), developer-guide ctx/HRPC/Wasm, DOCUMENTATION
release-checklist note, release-checklist optional tier1 drift.

Changelog maintenance in bare-os-booter and bare-os-protocol.
2026-04-05 23:01:54 -04:00

832 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Concatenated into /boot/init.js before init-main.js by scripts/bundle-kernel-init.mjs.
* kernel.ext.d: topological sort, signer pins, multisig gate, runKernelExtDropins.
* Depends on: BARE_OS_BOOT_TXN_STATE (boot preamble), bareOsPearMultisigShapeOk (00-pear-multisig-shape),
* semverGte from 20-init-boot-policy.js; bootStructuredLog/bootDryRun from init-main.js (same AsyncFunction body; hoisted).
* @see kernel/lib/init/STRUCTURE.md
*/
/**
* @param {{ file: string, extId: string, scripts: string[], dependsOn: string[], signaturePointer?: string }[]} entries
* @returns {{ ok: true, ordered: typeof entries } | { ok: false, cycleExtIds: string[], cycleEdges: string[] }}
*/
function topologicalOrderKernelExtEntries(entries) {
const sorted = [...entries].sort((a, b) => a.file.localeCompare(b.file))
const idToFile = new Map()
for (const e of sorted) {
if (!idToFile.has(e.extId)) idToFile.set(e.extId, e.file)
}
/** @type {Map<string, string[]>} */
const adj = new Map()
/** @type {Map<string, number>} */
const indeg = new Map()
for (const e of sorted) indeg.set(e.file, 0)
for (const e of sorted) {
for (const dep of e.dependsOn) {
const from = idToFile.get(dep)
if (!from || from === e.file) continue
if (!adj.has(from)) adj.set(from, [])
adj.get(from).push(e.file)
indeg.set(e.file, (indeg.get(e.file) || 0) + 1)
}
}
/** @type {string[]} */
const q = sorted
.map((e) => e.file)
.filter((f) => (indeg.get(f) || 0) === 0)
.sort()
/** @type {string[]} */
const out = []
while (q.length) {
const f = q.shift()
out.push(f)
for (const to of adj.get(f) || []) {
indeg.set(to, (indeg.get(to) || 0) - 1)
if (indeg.get(to) === 0) {
q.push(to)
q.sort()
}
}
}
if (out.length !== sorted.length) {
const stuck = sorted.filter((e) => !out.includes(e.file))
const stuckIds = new Set(stuck.map((e) => e.extId))
/** @type {string[]} */
const cycleEdges = []
for (const e of stuck) {
for (const dep of e.dependsOn) {
if (stuckIds.has(dep))
cycleEdges.push(String(e.extId) + ' -> dependsOn:' + String(dep))
}
}
cycleEdges.sort()
return {
ok: false,
cycleExtIds: stuck.map((e) => e.extId),
cycleEdges: cycleEdges.slice(0, 48)
}
}
const byFile = new Map(sorted.map((e) => [e.file, e]))
return {
ok: true,
ordered: out.map((f) => byFile.get(f)).filter(Boolean)
}
}
/**
* @param {{ extId: string, dependsOn: string[] }[]} entries
* @returns {number}
*/
function kernelExtDependencyDepth(entries) {
const idToEntry = new Map(entries.map((e) => [e.extId, e]))
const memo = new Map()
/**
* @param {string} id
* @param {Set<string>} stack
*/
function depth(id, stack) {
if (memo.has(id)) return memo.get(id)
if (stack.has(id)) return 99
const e = idToEntry.get(id)
if (!e) return 0
stack.add(id)
let d = 0
for (const dep of e.dependsOn) {
d = Math.max(d, depth(dep, stack) + 1)
}
stack.delete(id)
memo.set(id, d)
return d
}
let max = 0
for (const e of entries) {
max = Math.max(max, depth(e.extId, new Set()))
}
return max
}
/**
* Merge extension signer pin maps from boot policy env mirrors (V2V5; later JSON wins per key).
* @param {Record<string, unknown> | null | undefined} env
* @returns {Record<string, string | string[]>}
*/
function mergeExtensionSignerPinsFromEnv(env) {
if (!env || typeof env !== 'object') return {}
const keys = [
'BARE_OS_BOOT_POLICY_EXTENSION_SIGNER_PINS_V2_JSON',
'BARE_OS_BOOT_POLICY_EXTENSION_SIGNER_PINS_V3_JSON',
'BARE_OS_BOOT_POLICY_EXTENSION_SIGNER_PINS_V4_JSON',
'BARE_OS_BOOT_POLICY_EXTENSION_SIGNER_PINS_V5_JSON'
]
/** @type {Record<string, string | string[]>} */
const out = {}
for (const k of keys) {
const raw = String(env[k] ?? '').trim()
if (!raw) continue
try {
const o = JSON.parse(raw)
if (o && typeof o === 'object' && !Array.isArray(o)) {
for (const [ik, iv] of Object.entries(o)) {
out[String(ik)] = /** @type {string | string[]} */ (iv)
}
}
} catch {
/* ignore malformed JSON */
}
}
return out
}
/**
* When boot.policy pins an extension id to Ed25519 key(s), verify detached signature over script bytes.
* @param {Record<string, unknown>} ctx
* @param {{ extId: string, file: string, signaturePointer?: string }} ent
* @param {string} imgPath
* @param {Record<string, string | string[]>} pins
* @param {boolean} strictPol
* @returns {Promise<boolean>} true if the script may run
*/
async function verifyKernelExtSignerPinsForScript(
ctx,
ent,
imgPath,
pins,
strictPol
) {
const id = String(ent.extId || '').trim()
const need = pins[id]
if (need == null) return true
/** @type {string[]} */
const pubHexList = Array.isArray(need)
? need.map((x) =>
String(x)
.trim()
.toLowerCase()
.replace(/^0x/, '')
)
: [String(need).trim().toLowerCase().replace(/^0x/, '')]
const validKeys = pubHexList.filter(
(h) => h.length === 64 && /^[0-9a-f]+$/.test(h)
)
if (!validKeys.length) {
bootStructuredLog(
ctx,
'error',
'kernelExt.signerPinBadKey',
`[kernel.ext.d] extensionSignerPins for "${id}" must be 64-char hex pubkey(s)`
)
return !strictPol
}
const sigPtr = ent.signaturePointer
? String(ent.signaturePointer).trim()
: ''
if (!sigPtr.startsWith('/')) {
bootStructuredLog(
ctx,
'error',
'kernelExt.signerPinNoSigPath',
`[kernel.ext.d] "${id}": signaturePointer (absolute path to signature) required when extensionSignerPins lists this id`
)
return !strictPol
}
const { drive, b4a } = ctx
if (!drive || typeof drive.get !== 'function' || !b4a) {
bootStructuredLog(
ctx,
'error',
'kernelExt.signerPinNoDrive',
'[kernel.ext.d] signer pin verify requires ctx.drive.get and ctx.b4a'
)
return !strictPol
}
let scriptBuf
let sigBuf
try {
scriptBuf = await drive.get(imgPath)
sigBuf = await drive.get(sigPtr)
} catch (e) {
bootStructuredLog(
ctx,
'error',
'kernelExt.signerPinRead',
`[kernel.ext.d] signer pin read: ${(e && e.message) || String(e)}`
)
return !strictPol
}
if (!scriptBuf || !scriptBuf.byteLength || !sigBuf || !sigBuf.byteLength) {
bootStructuredLog(
ctx,
'error',
'kernelExt.signerPinMissingBytes',
`[kernel.ext.d] "${id}": script or signature file missing/empty for pin verify`
)
return !strictPol
}
const verifyFn = ctx.bareOsVerifyBootManifestSignature
if (typeof verifyFn !== 'function') {
bootStructuredLog(
ctx,
'error',
'kernelExt.signerPinNoVerify',
'[kernel.ext.d] ctx.bareOsVerifyBootManifestSignature unavailable; cannot enforce extensionSignerPins'
)
return !strictPol
}
for (const pk of validKeys) {
try {
if (verifyFn.call(ctx, scriptBuf, sigBuf, pk) === true) return true
} catch {
/* try next pubkey */
}
}
bootStructuredLog(
ctx,
'error',
'kernelExt.signerPinVerifyFailed',
`[kernel.ext.d] "${id}": Ed25519 verify failed for pinned key(s)`
)
return !strictPol
}
/**
* Append one NDJSON line to `/run/bare-os/kernel-ext-audit.ndjson` (strict boot diagnostics).
* @param {Record<string, unknown>} ctx
* @param {Record<string, unknown>} row
*/
async function appendKernelExtAuditNdjson(ctx, row) {
if (
!ctx.vfs ||
typeof ctx.vfs.readFile !== 'function' ||
typeof ctx.vfs.writeFile !== 'function' ||
!ctx.b4a
) {
return
}
const line =
JSON.stringify({
schema: 1,
type: 'kernelExtAudit',
atMs: Date.now(),
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''),
...row
}) + '\n'
try {
let prev = ''
try {
prev = ctx.b4a.toString(
await ctx.vfs.readFile('/run/bare-os/kernel-ext-audit.ndjson')
)
} catch {
/* absent */
}
const maxBytes = 96 * 1024
let next = prev + line
if (next.length > maxBytes) next = next.slice(-maxBytes)
await ctx.vfs.writeFile(
'/run/bare-os/kernel-ext-audit.ndjson',
ctx.b4a.from(next)
)
} catch {
/* ignore */
}
}
/**
* Optional `/etc/bare-os/kernel.ext.d/*.json` with `{ "scripts": ["/lib/bare-os/extensions/foo.js"] }`.
* @param {Record<string, unknown>} ctx
* @param {{ incremental?: boolean, ranScripts?: string[] }} [opts]
* @returns {Promise<boolean>}
*/
async function runKernelExtDropins(ctx, opts = {}) {
const incremental = opts.incremental === true
const ranScripts = opts.ranScripts
const { drive } = ctx
const run = ctx.bareOsRunImageScript
if (typeof run !== 'function') return true
if (!ctx.bareOsLoadedKernelExtScripts) {
ctx.bareOsLoadedKernelExtScripts = new Set()
}
/** @type {Set<string>} */
const loadedSet = /** @type {Set<string>} */ (ctx.bareOsLoadedKernelExtScripts)
const denyRaw = String(
ctx.env?.BARE_OS_BOOT_POLICY_DENY_KERNEL_EXT_IDS || ''
).trim()
const deny = new Set(
denyRaw
.split(',')
.map((s) => s.trim())
.filter(Boolean)
)
const strictPol =
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === '1' ||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === 'true'
const extSignerPins = mergeExtensionSignerPinsFromEnv(ctx.env)
const multisigExtGate =
ctx.env?.BARE_OS_EXTENSION_MULTISIG_VERIFY === '1' ||
ctx.env?.BARE_OS_EXTENSION_MULTISIG_VERIFY === 'true' ||
ctx.env?.BARE_OS_BOOT_POLICY_REQUIRE_PEAR_MULTISIG === '1' ||
ctx.env?.BARE_OS_BOOT_POLICY_REQUIRE_PEAR_MULTISIG === 'true'
if (multisigExtGate) {
const requireFile =
ctx.env?.BARE_OS_BOOT_POLICY_REQUIRE_PEAR_MULTISIG === '1' ||
ctx.env?.BARE_OS_BOOT_POLICY_REQUIRE_PEAR_MULTISIG === 'true'
try {
const mbuf = await drive.get('/etc/bare-os/pear.multisig.json')
if (!mbuf || mbuf.byteLength === 0) {
if (strictPol && requireFile) {
bootStructuredLog(
ctx,
'error',
'kernelExt.multisigMissing',
'[kernel.ext.d] strict boot requires /etc/bare-os/pear.multisig.json (BARE_OS_BOOT_POLICY_REQUIRE_PEAR_MULTISIG)'
)
if (typeof ctx.bareOsRequestBooterExit === 'function')
ctx.bareOsRequestBooterExit(1)
return false
}
} else {
const mj = JSON.parse(ctx.b4a.toString(mbuf))
if (!bareOsPearMultisigShapeOk(mj)) {
bootStructuredLog(
ctx,
'error',
'kernelExt.multisigInvalid',
'[kernel.ext.d] pear.multisig.json must be { signers: string[], quorum: number } with 1 ≤ quorum ≤ signers.length'
)
if (strictPol) {
if (typeof ctx.bareOsRequestBooterExit === 'function')
ctx.bareOsRequestBooterExit(1)
return false
}
}
}
} catch (e) {
bootStructuredLog(
ctx,
'warn',
'kernelExt.multisigRead',
'[kernel.ext.d] pear.multisig.json: ' + ((e && e.message) || String(e))
)
if (strictPol && requireFile) {
if (typeof ctx.bareOsRequestBooterExit === 'function')
ctx.bareOsRequestBooterExit(1)
return false
}
}
}
const maxDepthRaw = String(
ctx.env?.BARE_OS_BOOT_POLICY_MAX_KERNEL_EXT_DEPTH || ''
).trim()
const maxDepthCap =
maxDepthRaw && Number.parseInt(maxDepthRaw, 10) > 0
? Math.min(32, Number.parseInt(maxDepthRaw, 10))
: 0
/** @type {string[]} */
const names = []
try {
for await (const n of drive.readdir('/etc/bare-os/kernel.ext.d'))
names.push(n)
} catch {
return true
}
names.sort()
/** @type {{ file: string, extId: string, scripts: string[], dependsOn: string[], beforeIds: string[], conflictsWith: string[], signaturePointer?: string, provides: { name: string, version: string }[] }[]} */
const collected = []
/** @type {Map<string, string[]>} */
const extIdFiles = new Map()
for (const name of names) {
if (!name.endsWith('.json')) continue
const p = `/etc/bare-os/kernel.ext.d/${name}`
try {
const buf = await drive.get(p)
if (!buf) continue
const pol = JSON.parse(ctx.b4a.toString(buf))
if (!pol || typeof pol !== 'object') continue
const extId =
pol.id != null
? String(pol.id).trim()
: name.replace(/\.json$/i, '')
if (deny.has(extId)) {
bootStructuredLog(
ctx,
'error',
'kernelExt.denyId',
`[kernel.ext.d] denied by policy id: ${extId}`
)
await appendKernelExtAuditNdjson(ctx, {
event: 'deny_id',
extId,
dropin: name
})
continue
}
const scripts = pol.scripts
if (!Array.isArray(scripts)) continue
/** @type {string[]} */
let dependsOn = Array.isArray(pol.dependsOn)
? pol.dependsOn.map((d) => String(d).trim()).filter(Boolean).slice(0, 16)
: []
if (Array.isArray(pol.requires)) {
dependsOn = dependsOn.concat(
pol.requires.map((d) => String(d).trim()).filter(Boolean).slice(0, 16)
)
}
if (Array.isArray(pol.after)) {
dependsOn = dependsOn.concat(
pol.after.map((d) => String(d).trim()).filter(Boolean).slice(0, 16)
)
}
const beforeIds = Array.isArray(pol.before)
? pol.before.map((d) => String(d).trim()).filter(Boolean).slice(0, 16)
: []
const sig =
typeof pol.signaturePointer === 'string'
? pol.signaturePointer.trim().slice(0, 512)
: undefined
const minExtCtx =
typeof pol.minCtxApiVersion === 'string'
? pol.minCtxApiVersion.trim()
: ''
if (minExtCtx) {
const haveS =
typeof ctx.bareOsCtxApiVersion === 'string'
? ctx.bareOsCtxApiVersion.trim()
: String(ctx.bareOsCtxApiVersion || '').trim()
if (!haveS || !semverGte(haveS, minExtCtx)) {
bootStructuredLog(
ctx,
'error',
'kernelExt.minCtxApiVersion',
`[kernel.ext.d] ${name}: minCtxApiVersion ${minExtCtx} not satisfied (have ${haveS || 'none'})`
)
if (strictPol) return false
continue
}
}
const conflictsWith = Array.isArray(pol.conflictsWith)
? pol.conflictsWith.map((d) => String(d).trim()).filter(Boolean).slice(0, 16)
: []
/** @type {{ name: string, version: string }[]} */
const provides = Array.isArray(pol.provides)
? pol.provides
.map((x) => {
if (!x || typeof x !== 'object') return null
const nm = String(x.name || '').trim()
const ver = String(x.version ?? x.semver ?? '').trim()
if (!nm || !ver) return null
return { name: nm, version: ver }
})
.filter(Boolean)
.slice(0, 16)
: []
collected.push({
file: name,
extId,
scripts: scripts.map((s) => String(s).trim()).filter(Boolean),
dependsOn,
beforeIds,
conflictsWith,
provides,
signaturePointer: sig || undefined
})
if (!extIdFiles.has(extId)) extIdFiles.set(extId, [])
extIdFiles.get(extId).push(name)
} catch (e) {
bootStructuredLog(
ctx,
'error',
'kernelExt.dropinParse',
`kernel.ext.d/${name}: ` + ((e && e.message) || String(e))
)
}
}
/** @type {string[]} */
const duplicateExtIds = []
for (const [eid, files] of extIdFiles) {
if (files.length > 1) duplicateExtIds.push(eid)
}
duplicateExtIds.sort()
if (duplicateExtIds.length) {
bootStructuredLog(
ctx,
'error',
'kernelExt.duplicateId',
`[kernel.ext.d] duplicate extension id(s): ${duplicateExtIds.join(', ')} — each id must appear in at most one drop-in`
)
if (strictPol) {
await appendKernelExtAuditNdjson(ctx, {
event: 'duplicate_ext_id',
duplicateExtIds
})
return false
}
}
/** @param {string} ver */
function bareOsKernelExtSemverValid(ver) {
const s = String(ver || '').trim()
return /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/.test(
s
)
}
/** @type {string[]} */
const providesInvalidSemver = []
for (const e of collected) {
for (const pr of e.provides || []) {
if (!bareOsKernelExtSemverValid(pr.version)) {
providesInvalidSemver.push(`${e.extId}:${pr.name}@${pr.version}`)
}
}
}
providesInvalidSemver.sort()
if (providesInvalidSemver.length) {
bootStructuredLog(
ctx,
'warn',
'kernelExt.providesInvalidSemver',
`[kernel.ext.d] provides[].version must look like semver (major.minor.patch): ${providesInvalidSemver.join('; ')}`
)
if (strictPol) {
await appendKernelExtAuditNdjson(ctx, {
event: 'provides_invalid_semver',
items: providesInvalidSemver
})
return false
}
}
/** @type {Map<string, { extId: string, version: string }>} */
const provideNameToOwner = new Map()
/** @type {string[]} */
const providesVersionConflicts = []
for (const e of collected) {
for (const pr of e.provides || []) {
const prev = provideNameToOwner.get(pr.name)
if (prev && prev.version !== pr.version) {
providesVersionConflicts.push(
`${pr.name}: ${prev.extId}@${prev.version} vs ${e.extId}@${pr.version}`
)
} else if (!prev) {
provideNameToOwner.set(pr.name, { extId: e.extId, version: pr.version })
}
}
}
providesVersionConflicts.sort()
if (providesVersionConflicts.length) {
bootStructuredLog(
ctx,
'error',
'kernelExt.providesConflict',
`[kernel.ext.d] conflicting provides version(s): ${providesVersionConflicts.join('; ')}`
)
if (strictPol) {
await appendKernelExtAuditNdjson(ctx, {
event: 'provides_version_conflict',
conflicts: providesVersionConflicts
})
return false
}
}
const idToCollected = new Map(collected.map((e) => [e.extId, e]))
for (const e of collected) {
for (const bid of e.beforeIds) {
const target = idToCollected.get(bid)
if (target && target.extId !== e.extId) {
const next = new Set(target.dependsOn)
next.add(e.extId)
target.dependsOn = [...next].slice(0, 24)
}
}
}
for (const e of collected) {
e.dependsOn = [...new Set(e.dependsOn)]
}
const idSet = new Set(collected.map((e) => e.extId))
for (const e of collected) {
for (const c of e.conflictsWith) {
if (idSet.has(c)) {
bootStructuredLog(
ctx,
'error',
'kernelExt.conflict',
`[kernel.ext.d] conflict: extension "${e.extId}" conflictsWith "${c}"`
)
if (strictPol) {
await appendKernelExtAuditNdjson(ctx, {
event: 'conflict',
extId: e.extId,
conflictsWith: c
})
return false
}
}
}
}
if (maxDepthCap > 0) {
const d = kernelExtDependencyDepth(collected)
if (d > maxDepthCap) {
bootStructuredLog(
ctx,
'error',
'kernelExt.maxDepth',
`[kernel.ext.d] maxKernelExtensionDepth exceeded (${d} > ${maxDepthCap})`
)
if (strictPol) return false
}
}
const topo = topologicalOrderKernelExtEntries(collected)
/** @type {typeof collected} */
let ordered
if (!topo.ok) {
const edgeHint =
topo.cycleEdges && topo.cycleEdges.length
? '; cycle edges (stuck->dep): ' + topo.cycleEdges.join('; ')
: ''
bootStructuredLog(
ctx,
'error',
'kernelExt.cycle',
'[kernel.ext.d] dependency cycle in extension drop-ins; ext ids: ' +
topo.cycleExtIds.join(', ') +
edgeHint
)
if (strictPol) {
await appendKernelExtAuditNdjson(ctx, {
event: 'dependency_cycle',
cycleExtIds: [...topo.cycleExtIds].sort(),
cycleEdges: topo.cycleEdges || []
})
return false
}
ordered = [...collected].sort((a, b) => a.file.localeCompare(b.file))
} else {
ordered = topo.ordered
}
const dry = bootDryRun(ctx)
const traceOn =
strictPol &&
(ctx.env?.BARE_OS_BOOT_EXT_RESOLUTION_TRACE === '1' ||
ctx.env?.BARE_OS_BOOT_EXT_RESOLUTION_TRACE === 'true')
const resolutionAlways =
ctx.env?.BARE_OS_KERNEL_EXT_RESOLUTION_JSON_ALWAYS === '1' ||
ctx.env?.BARE_OS_KERNEL_EXT_RESOLUTION_JSON_ALWAYS === 'true'
const extResolutionFailure = strictPol && !topo.ok
if (
ctx.vfs &&
typeof ctx.vfs.writeFile === 'function' &&
ctx.b4a &&
(extResolutionFailure || traceOn || resolutionAlways)
) {
try {
const cycleSorted = topo.ok
? []
: [...topo.cycleExtIds].sort((a, b) => a.localeCompare(b))
const provideSnapshot = {}
for (const [k, v] of provideNameToOwner) {
provideSnapshot[k] = v
}
const body =
JSON.stringify({
schema: 3,
atMs: Date.now(),
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''),
bootPolicyStrict: !!strictPol,
failure: extResolutionFailure
? {
kind: 'dependency_cycle',
cycleExtIds: cycleSorted,
cycleEdges: topo.cycleEdges || [],
provenance: 'kernel.ext.d topological sort (init-main)'
}
: null,
ordered: ordered.map((e) => ({
extId: e.extId,
file: e.file,
dependsOn: e.dependsOn,
provides: e.provides || []
})),
cycleExtIds: topo.ok ? [] : cycleSorted,
providesInvalidSemver,
providesVersionConflicts:
providesVersionConflicts.length > 0
? providesVersionConflicts
: undefined,
provideNameToOwner: provideSnapshot
}) + '\n'
await ctx.vfs.writeFile(
'/run/bare-os/kernel-ext-resolution.json',
ctx.b4a.from(body)
)
} catch {
/* ignore */
}
}
const extGraphOn =
ctx.env?.BARE_OS_KERNEL_EXT_GRAPH === '1' ||
ctx.env?.BARE_OS_KERNEL_EXT_GRAPH === 'true'
const extGraphDefer =
ctx.env?.BARE_OS_INIT_DEFER_KERNEL_EXT_GRAPH === '1' ||
ctx.env?.BARE_OS_INIT_DEFER_KERNEL_EXT_GRAPH === 'true'
if (
extGraphOn &&
!extGraphDefer &&
ctx.vfs &&
typeof ctx.vfs.writeFile === 'function' &&
ctx.b4a
) {
let bareModuleTraverse = 'unresolved'
try {
await import('bare-module-traverse')
bareModuleTraverse = 'import_ok'
} catch {
bareModuleTraverse = 'import_failed'
}
try {
const body =
JSON.stringify({
schema: 1,
atMs: Date.now(),
bareModuleTraverse,
note: 'Static drop-in DAG; deeper static edges require host tooling on extension sources.',
nodes: ordered.map((e) => ({
extId: e.extId,
file: e.file,
scripts: e.scripts,
dependsOn: e.dependsOn
}))
}) + '\n'
await ctx.vfs.writeFile(
'/run/bare-os/kernel-ext-graph.json',
ctx.b4a.from(body)
)
} catch {
/* ignore */
}
}
for (const ent of ordered) {
for (const sp of ent.scripts) {
const imgPath = String(sp).trim()
if (!imgPath.startsWith('/lib/bare-os/extensions/')) {
bootStructuredLog(
ctx,
'error',
'kernelExt.rejectedPath',
`[kernel.ext.d] rejected script path: ${imgPath}`
)
continue
}
if (incremental && loadedSet.has(imgPath)) {
continue
}
if (dry) {
bootStructuredLog(
ctx,
'error',
'bootDryRunKernelExt',
`[boot-dry-run] skip kernel.ext.d script: ${imgPath}`
)
continue
}
if (
!(await verifyKernelExtSignerPinsForScript(
ctx,
ent,
imgPath,
extSignerPins,
strictPol
))
) {
if (strictPol) {
if (typeof ctx.bareOsRequestBooterExit === 'function')
ctx.bareOsRequestBooterExit(1)
return false
}
continue
}
try {
const extT0 = Date.now()
await run(imgPath)
const loadMs = Date.now() - extT0
loadedSet.add(imgPath)
if (Array.isArray(ranScripts)) ranScripts.push(imgPath)
if (typeof ctx.bareOsRegisterKernelExtensionRecord === 'function') {
ctx.bareOsRegisterKernelExtensionRecord({
dropin: ent.file,
script: imgPath,
id: ent.extId,
dependsOn: ent.dependsOn,
signaturePointer: ent.signaturePointer,
loadMs
})
}
} catch (e) {
bootStructuredLog(
ctx,
'error',
'kernelExt.scriptThrown',
`[kernel.ext.d] ${ent.file}: ` + ((e && e.message) || String(e))
)
}
}
}
return true
}