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.
832 lines
25 KiB
JavaScript
832 lines
25 KiB
JavaScript
/**
|
||
* 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 (V2–V5; 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
|
||
}
|