4367 lines
136 KiB
JavaScript
4367 lines
136 KiB
JavaScript
/**
|
||
* Keep algorithm in sync with `packages/bare-os-protocol/lib/bare-os-pear-multisig-shape.js`
|
||
* (`pearMultisigShapeOk`). Kernel bundle cannot import npm packages.
|
||
* @param {unknown} parsed
|
||
* @returns {boolean}
|
||
*/
|
||
function bareOsPearMultisigShapeOk(parsed) {
|
||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false
|
||
const signers = /** @type {{ signers?: unknown }} */ (parsed).signers
|
||
const quorum = /** @type {{ quorum?: unknown }} */ (parsed).quorum
|
||
if (!Array.isArray(signers) || typeof quorum !== 'number') return false
|
||
if (quorum < 1 || quorum > signers.length) return false
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* Boot preamble: cooperative lock FSM constants + `invokeCtxBootHooks`.
|
||
* This file is the **only** source for these symbols; `kernel/init.js` is generated by
|
||
* `node scripts/bundle-kernel-init.mjs` (concat `lib/boot/*.js`, `lib/init/fragments/*.js`, then `lib/init/init-main.js`).
|
||
* CI: `scripts/verify-init-bundle-recipe.mjs` / `verify-kernel-seeder-parity.mjs`.
|
||
*/
|
||
/** Boot transaction journal row state (resume / audit / rollback FSM). */
|
||
const BARE_OS_BOOT_TXN_STATE = Object.freeze({
|
||
STAGE_STARTED: 'stage_started',
|
||
STAGE_COMMITTED: 'stage_committed',
|
||
STAGE_ROLLBACK: 'stage_rollback',
|
||
STAGE_SKIPPED: 'stage_skipped'
|
||
})
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {Record<string, unknown>} ev
|
||
*/
|
||
async function invokeCtxBootHooks(ctx, ev) {
|
||
const label = String(
|
||
ev.phase !== undefined && ev.phase !== null
|
||
? ev.phase
|
||
: ev.step !== undefined && ev.step !== null
|
||
? ev.step
|
||
: ''
|
||
)
|
||
const merged = { ...ev, phase: label, step: label }
|
||
if (typeof ctx.bareOsInvokeBootStepHooks === 'function') {
|
||
await ctx.bareOsInvokeBootStepHooks(merged)
|
||
return
|
||
}
|
||
if (typeof ctx.bareOsInvokeBootPhaseHooks === 'function') {
|
||
await ctx.bareOsInvokeBootPhaseHooks(merged)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Concatenated into /boot/init.js before init-main.js by scripts/bundle-kernel-init.mjs.
|
||
* Boot policy merge: semver helpers, skip merge, kernel.d env guards, applyBootPolicyFile.
|
||
* @see kernel/lib/init/STRUCTURE.md
|
||
*/
|
||
|
||
/**
|
||
* Optional `/etc/bare-os/boot.policy.json` when `BARE_OS_BOOT_POLICY=1`:
|
||
* `{ "skipBootStages": ["rc.d"], "denyBootStages": ["onboot"], "minKernelCapabilitiesPrimary": 1, "requireSeedCaps": 1,
|
||
* "maxExecLineDepth": 32, "denyEnvKeys": ["FOO"], "requireProcNodes": ["/proc/bare_os/features"] }`.
|
||
* Boot policy v5 (optional): `requireKernelCapabilitiesHostTransportDelegates`, `requireInitJsSha256` (hex sha256 of `/boot/init.js` raw bytes).
|
||
* Boot policy v6 (optional): `requireKernelCapabilitiesReplicationOperatorSurface`, `requireBooterSemver`, `requireCtxApiMin`, `denyKernelExtensionIds`,
|
||
* `kernelExtensionHashPins`, `offlineLkgIntegrityStrict`.
|
||
* Boot policy v7 (optional): `requireKernelCapabilitiesPearCorestoreHrpc`, `requirePearRuntimeMin`, `requireProtocolPackageMin`,
|
||
* `denyCtxMethodPrefixes`, `maxKernelExtensionDepth`, `gitPartialClonePolicy`.
|
||
* Boot policy v8 (optional): `requireKernelCapabilitiesBareRuntimeProtoMux`, `requireBareRuntimeMin`, `denySeedRpcMethods`,
|
||
* `maxProtomuxChannelNameLength`.
|
||
* Boot policy v9 (optional): `requireKernelCapabilitiesBareModuleCryptoStaging`, `requirePearRuntimeRange` (`{ min?, max? }` semver),
|
||
* `denyBareModuleSpecifierPatterns`, `requireBareCryptoMin`, `denyKernelSyscalls`, `requirePearIpcMin`,
|
||
* `extensionSignerPinsV2`, `offlineLkgManifestMaxAgeSec`, `bootStagesRequireProcIndexMinSchema` (legacy `bootPhasesRequireProcIndexMinSchema`).
|
||
* Boot policy v10 (optional): `requireKernelCapabilitiesPearInspectLoggerTls`, `requireBareBootMin`, `denyBareRpcMethodPatterns`,
|
||
* `maxPearInspectDepth`, `requireBareLoggerMin`, `denyAutobaseDiscoveryChannels`, `requireBareTlsMin`,
|
||
* `extensionSignerPinsV3`, `offlineLkgRequirePearStamp`, `bootStagesRequireLifecycleMinSchema` (legacy `bootPhasesRequireLifecycleMinSchema`).
|
||
* Boot policy v11 (optional): `requireKernelCapabilitiesHypercorePackHrpcLifecycle`, `extensionSignerPinsV4`, `requireBarePackMin`,
|
||
* `requireBareAddonPolicyMin`, `maxHrpcAllowlistDepth`, `offlineLkgRequireHypercorePackHrpcLifecycle`.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<boolean>} false when strict policy fails (caller should abort boot)
|
||
*/
|
||
/**
|
||
* @param {string} s
|
||
* @returns {number[] | null}
|
||
*/
|
||
function parseSemverTriplet(s) {
|
||
const m = String(s)
|
||
.trim()
|
||
.match(/^(\d+)\.(\d+)\.(\d+)/)
|
||
if (!m) return null
|
||
return [Number(m[1]), Number(m[2]), Number(m[3])]
|
||
}
|
||
|
||
/**
|
||
* @param {string} have
|
||
* @param {string} need
|
||
*/
|
||
function semverGte(have, need) {
|
||
const a = parseSemverTriplet(have)
|
||
const b = parseSemverTriplet(need)
|
||
if (!a || !b) return false
|
||
for (let i = 0; i < 3; i++) {
|
||
if (a[i] > b[i]) return true
|
||
if (a[i] < b[i]) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {string} have
|
||
* @param {string} need
|
||
*/
|
||
function semverLte(have, need) {
|
||
const a = parseSemverTriplet(have)
|
||
const b = parseSemverTriplet(need)
|
||
if (!a || !b) return false
|
||
for (let i = 0; i < 3; i++) {
|
||
if (a[i] < b[i]) return true
|
||
if (a[i] > b[i]) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} m
|
||
* @param {string} k
|
||
* @returns {number | undefined}
|
||
*/
|
||
function ctxKernelCapWord(m, k) {
|
||
if (!m || typeof m !== 'object') return undefined
|
||
const v = /** @type {Record<string, unknown>} */ (m)[k]
|
||
return typeof v === 'number' ? v : undefined
|
||
}
|
||
|
||
/**
|
||
* Merge only boot-stage skip fields from a policy fragment (tiered fallbacks / rollback marker).
|
||
* Honors canonical `skipBootStages` / `denyBootStages` and legacy `skipPhases` / `denyBootPhases`.
|
||
* @param {Set<string>} merge
|
||
* @param {Record<string, unknown>} pol
|
||
*/
|
||
function mergeBootPolicySkipStagesIntoSet(merge, pol) {
|
||
if (!pol || typeof pol !== 'object') return
|
||
if (Array.isArray(pol.skipBootStages)) {
|
||
for (const p of pol.skipBootStages) merge.add(String(p).toLowerCase())
|
||
}
|
||
if (Array.isArray(pol.skipPhases)) {
|
||
for (const p of pol.skipPhases) merge.add(String(p).toLowerCase())
|
||
}
|
||
if (Array.isArray(pol.denyBootStages)) {
|
||
for (const p of pol.denyBootStages) merge.add(String(p).toLowerCase())
|
||
}
|
||
if (Array.isArray(pol.denyBootPhases)) {
|
||
for (const p of pol.denyBootPhases) merge.add(String(p).toLowerCase())
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Optional dry-run — parse boot snippets but skip trusted execLine / extension scripts.
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function bootDryRun(ctx) {
|
||
const v = ctx.env && ctx.env.BARE_OS_BOOT_DRY_RUN
|
||
return v === '1' || v === 'true'
|
||
}
|
||
|
||
/**
|
||
* Named boot stage for journals / checkpoints.
|
||
* @param {string} label
|
||
*/
|
||
function bootStageForPhase(label) {
|
||
const l = String(label).toLowerCase()
|
||
if (l === 'boot.policy') return 'policy'
|
||
if (l === 'os-release' || l === 'motd') return 'preflight'
|
||
if (
|
||
l === 'rc' ||
|
||
l.startsWith('rc.') ||
|
||
l === 'rc.d' ||
|
||
l === 'rc.local'
|
||
)
|
||
return 'rc'
|
||
if (l === 'kernel.d' || l === 'kernel.ext.d') return 'extensions'
|
||
if (l === 'onboot' || l === 'selftest') return 'services'
|
||
if (l === 'banner') return 'shell'
|
||
return 'misc'
|
||
}
|
||
|
||
/**
|
||
* kernel.d: leading `# ConditionEnvironment=KEY=VAL` / `# AssertEnvironment=…` guards.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} text
|
||
*/
|
||
function kernelSnippetEnvGuardsOk(ctx, text) {
|
||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||
for (const raw of String(text).split(/\r?\n/)) {
|
||
const line = raw.trim()
|
||
if (!line.startsWith('#')) break
|
||
const m = line.match(
|
||
/^#\s*ConditionEnvironment=(.+)$/i
|
||
) || line.match(/^#\s*AssertEnvironment=(.+)$/i)
|
||
if (!m) continue
|
||
const expr = String(m[1]).trim()
|
||
const eq = expr.indexOf('=')
|
||
if (eq < 1) return false
|
||
const key = expr.slice(0, eq).trim()
|
||
const need = expr.slice(eq + 1).trim()
|
||
const have =
|
||
env[key] != null && env[key] !== undefined ? String(env[key]) : ''
|
||
if (have !== need) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
async function applyBootPolicyFile(ctx) {
|
||
const en = ctx.env && ctx.env.BARE_OS_BOOT_POLICY
|
||
if (en !== '1' && en !== 'true') return true
|
||
const merge =
|
||
ctx.bareOsBootPolicySkipStages instanceof Set
|
||
? ctx.bareOsBootPolicySkipStages
|
||
: ctx.bareOsBootPolicySkipPhases
|
||
if (!(merge instanceof Set)) return true
|
||
const { drive, b4a } = ctx
|
||
const strictPol =
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === 'true'
|
||
const advWords = ctx.bareOsAdvertisedKernelCapabilityWords
|
||
try {
|
||
const rb = ctx.env?.BARE_OS_BOOT_ROLLBACK_APPLY
|
||
if ((rb === '1' || rb === 'true') && ctx.vfs && typeof ctx.vfs.readFile === 'function') {
|
||
try {
|
||
const mbuf = await ctx.vfs.readFile('/run/bare-os/boot-rollback.marker')
|
||
const mobj = JSON.parse(b4a.toString(mbuf))
|
||
mergeBootPolicySkipStagesIntoSet(merge, mobj)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
const policyPathRaw = String(ctx.env?.BARE_OS_BOOT_POLICY_PATH || '').trim()
|
||
const policyPath =
|
||
policyPathRaw && policyPathRaw.startsWith('/etc/bare-os/')
|
||
? policyPathRaw
|
||
: '/etc/bare-os/boot.policy.json'
|
||
const buf = await drive.get(policyPath)
|
||
if (!buf) return true
|
||
const pol = JSON.parse(b4a.toString(buf))
|
||
if (!pol || typeof pol !== 'object') return true
|
||
const legacySkip =
|
||
(Array.isArray(pol.skipPhases) && pol.skipPhases.length > 0) ||
|
||
(Array.isArray(pol.denyBootPhases) && pol.denyBootPhases.length > 0)
|
||
const modernSkip =
|
||
(Array.isArray(pol.skipBootStages) && pol.skipBootStages.length > 0) ||
|
||
(Array.isArray(pol.denyBootStages) && pol.denyBootStages.length > 0)
|
||
if (legacySkip && !modernSkip) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'warn',
|
||
'deprecatedSkipPhases',
|
||
'[boot-policy] deprecated: skipPhases/denyBootPhases in boot.policy.json — prefer skipBootStages/denyBootStages'
|
||
)
|
||
}
|
||
mergeBootPolicySkipStagesIntoSet(merge, pol)
|
||
if (Array.isArray(pol.policyFallbackPaths)) {
|
||
for (const rel of pol.policyFallbackPaths) {
|
||
const p2 = String(rel).trim()
|
||
if (!p2.startsWith('/etc/bare-os/')) continue
|
||
try {
|
||
const fb = await drive.get(p2)
|
||
if (!fb) continue
|
||
const pol2 = JSON.parse(b4a.toString(fb))
|
||
mergeBootPolicySkipStagesIntoSet(merge, pol2)
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'policyFallbackPaths',
|
||
'[boot-policy] policyFallbackPaths ' +
|
||
p2 +
|
||
': ' +
|
||
((e && e.message) || String(e))
|
||
)
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireBootBundleSha256Hex === 'string' &&
|
||
String(pol.requireBootBundleSha256Hex).trim()
|
||
) {
|
||
const need = String(pol.requireBootBundleSha256Hex).trim().toLowerCase()
|
||
const have = String(ctx.env?.BARE_OS_BOOT_BUNDLE_DIGEST_HEX || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (!have || have !== need) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireBootBundleSha256Hex',
|
||
'[boot-policy] requireBootBundleSha256Hex not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (pol.initdAdmission && typeof pol.initdAdmission === 'object' && ctx.env) {
|
||
const a = /** @type {Record<string, unknown>} */ (pol.initdAdmission)
|
||
if (typeof a.maxParallel === 'number' && a.maxParallel >= 1) {
|
||
ctx.env.BARE_OS_INITD_MAX_PARALLEL = String(
|
||
Math.min(32, Math.floor(a.maxParallel))
|
||
)
|
||
}
|
||
if (typeof a.memoryHintMb === 'number' && a.memoryHintMb >= 1) {
|
||
ctx.env.BARE_OS_INITD_MEMORY_HINT_MB = String(
|
||
Math.min(1e6, Math.floor(a.memoryHintMb))
|
||
)
|
||
}
|
||
if (typeof a.wallBudgetMsDefault === 'number' && a.wallBudgetMsDefault >= 1) {
|
||
ctx.env.BARE_OS_INITD_WALL_BUDGET_MS_DEFAULT = String(
|
||
Math.min(3.6e6, Math.floor(a.wallBudgetMsDefault))
|
||
)
|
||
}
|
||
}
|
||
if (pol.p2pAdmission && typeof pol.p2pAdmission === 'object' && ctx.env) {
|
||
const p2p = /** @type {Record<string, unknown>} */ (pol.p2pAdmission)
|
||
const allow = typeof p2p.peerAllowlistHex === 'string' ? p2p.peerAllowlistHex.trim() : ''
|
||
if (allow && !String(ctx.env.BARE_OS_PEER_ALLOWLIST_HEX || '').trim()) {
|
||
ctx.env.BARE_OS_PEER_ALLOWLIST_HEX = allow
|
||
}
|
||
const boot =
|
||
typeof p2p.hyperswarmBootstrap === 'string' ? p2p.hyperswarmBootstrap.trim() : ''
|
||
if (boot && !String(ctx.env.HYPERSWARM_BOOTSTRAP || '').trim()) {
|
||
ctx.env.HYPERSWARM_BOOTSTRAP = boot
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.minKernelCapabilitiesPrimary === 'number' &&
|
||
pol.minKernelCapabilitiesPrimary > 0
|
||
) {
|
||
const adv = ctxKernelCapWord(advWords, 'primary')
|
||
const need = pol.minKernelCapabilitiesPrimary >>> 0
|
||
if (typeof adv === 'number') {
|
||
const ok = ((adv >>> 0) & need) === need
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'minKernelCapabilitiesPrimary',
|
||
'[boot-policy] minKernelCapabilitiesPrimary not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (typeof pol.requireSeedCaps === 'number' && pol.requireSeedCaps > 0) {
|
||
const seed = ctxKernelCapWord(ctx.bareOsSeedKernelCapabilityWords, 'primary')
|
||
const need = pol.requireSeedCaps >>> 0
|
||
const ok = typeof seed === 'number' && ((seed >>> 0) & need) === need
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireSeedCaps',
|
||
'[boot-policy] requireSeedCaps not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesExtendedSeedingPlatform === 'number' &&
|
||
pol.requireKernelCapabilitiesExtendedSeedingPlatform > 0
|
||
) {
|
||
const adv2 = ctxKernelCapWord(advWords, 'extendedSeedingPlatform')
|
||
const need2 = pol.requireKernelCapabilitiesExtendedSeedingPlatform >>> 0
|
||
const ok = typeof adv2 === 'number' && ((adv2 >>> 0) & need2) === need2
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesExtendedSeedingPlatform',
|
||
'[boot-policy] requireKernelCapabilitiesExtendedSeedingPlatform not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesRlimitsDelegatesShell === 'number' &&
|
||
pol.requireKernelCapabilitiesRlimitsDelegatesShell > 0
|
||
) {
|
||
const adv3 = ctxKernelCapWord(advWords, 'rlimitsDelegatesShell')
|
||
const need3 = pol.requireKernelCapabilitiesRlimitsDelegatesShell >>> 0
|
||
const ok = typeof adv3 === 'number' && ((adv3 >>> 0) & need3) === need3
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesRlimitsDelegatesShell',
|
||
'[boot-policy] requireKernelCapabilitiesRlimitsDelegatesShell not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesOfflineNetExtensions === 'number' &&
|
||
pol.requireKernelCapabilitiesOfflineNetExtensions > 0
|
||
) {
|
||
const adv4 = ctxKernelCapWord(advWords, 'offlineNetExtensions')
|
||
const need4 = pol.requireKernelCapabilitiesOfflineNetExtensions >>> 0
|
||
const ok = typeof adv4 === 'number' && ((adv4 >>> 0) & need4) === need4
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesOfflineNetExtensions',
|
||
'[boot-policy] requireKernelCapabilitiesOfflineNetExtensions not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesHostTransportDelegates === 'number' &&
|
||
pol.requireKernelCapabilitiesHostTransportDelegates > 0
|
||
) {
|
||
const adv5 = ctxKernelCapWord(advWords, 'hostTransportDelegates')
|
||
const need5 = pol.requireKernelCapabilitiesHostTransportDelegates >>> 0
|
||
const ok = typeof adv5 === 'number' && ((adv5 >>> 0) & need5) === need5
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesHostTransportDelegates',
|
||
'[boot-policy] requireKernelCapabilitiesHostTransportDelegates not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesReplicationOperatorSurface === 'number' &&
|
||
pol.requireKernelCapabilitiesReplicationOperatorSurface > 0
|
||
) {
|
||
const adv6 = ctxKernelCapWord(advWords, 'replicationOperatorSurface')
|
||
const need6 = pol.requireKernelCapabilitiesReplicationOperatorSurface >>> 0
|
||
const ok = typeof adv6 === 'number' && ((adv6 >>> 0) & need6) === need6
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesReplicationOperatorSurface',
|
||
'[boot-policy] requireKernelCapabilitiesReplicationOperatorSurface not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesPearCorestoreHrpc === 'number' &&
|
||
pol.requireKernelCapabilitiesPearCorestoreHrpc > 0
|
||
) {
|
||
const adv7 = ctxKernelCapWord(advWords, 'pearCorestoreHrpc')
|
||
const need7 = pol.requireKernelCapabilitiesPearCorestoreHrpc >>> 0
|
||
const ok = typeof adv7 === 'number' && ((adv7 >>> 0) & need7) === need7
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesPearCorestoreHrpc',
|
||
'[boot-policy] requireKernelCapabilitiesPearCorestoreHrpc not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesBareRuntimeProtoMux === 'number' &&
|
||
pol.requireKernelCapabilitiesBareRuntimeProtoMux > 0
|
||
) {
|
||
const adv8 = ctxKernelCapWord(advWords, 'bareRuntimeProtoMux')
|
||
const need8 = pol.requireKernelCapabilitiesBareRuntimeProtoMux >>> 0
|
||
const ok = typeof adv8 === 'number' && ((adv8 >>> 0) & need8) === need8
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesBareRuntimeProtoMux',
|
||
'[boot-policy] requireKernelCapabilitiesBareRuntimeProtoMux not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesBareModuleCryptoStaging === 'number' &&
|
||
pol.requireKernelCapabilitiesBareModuleCryptoStaging > 0
|
||
) {
|
||
const adv9 = ctxKernelCapWord(advWords, 'bareModuleCryptoStaging')
|
||
const need9 = pol.requireKernelCapabilitiesBareModuleCryptoStaging >>> 0
|
||
const ok = typeof adv9 === 'number' && ((adv9 >>> 0) & need9) === need9
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesBareModuleCryptoStaging',
|
||
'[boot-policy] requireKernelCapabilitiesBareModuleCryptoStaging not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesPearInspectLoggerTls === 'number' &&
|
||
pol.requireKernelCapabilitiesPearInspectLoggerTls > 0
|
||
) {
|
||
const adv10 = ctxKernelCapWord(advWords, 'pearInspectLoggerTls')
|
||
const need10 = pol.requireKernelCapabilitiesPearInspectLoggerTls >>> 0
|
||
const ok =
|
||
typeof adv10 === 'number' && ((adv10 >>> 0) & need10) === need10
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesPearInspectLoggerTls',
|
||
'[boot-policy] requireKernelCapabilitiesPearInspectLoggerTls not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireKernelCapabilitiesHypercorePackHrpcLifecycle === 'number' &&
|
||
pol.requireKernelCapabilitiesHypercorePackHrpcLifecycle > 0
|
||
) {
|
||
const adv11 = ctxKernelCapWord(advWords, 'hypercorePackHrpcLifecycle')
|
||
const need11 = pol.requireKernelCapabilitiesHypercorePackHrpcLifecycle >>> 0
|
||
const ok =
|
||
typeof adv11 === 'number' && ((adv11 >>> 0) & need11) === need11
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireKernelCapabilitiesHypercorePackHrpcLifecycle',
|
||
'[boot-policy] requireKernelCapabilitiesHypercorePackHrpcLifecycle not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
pol.requirePearRuntimeRange &&
|
||
typeof pol.requirePearRuntimeRange === 'object'
|
||
) {
|
||
const r = pol.requirePearRuntimeRange
|
||
const minS = typeof r.min === 'string' ? r.min.trim() : ''
|
||
const maxS = typeof r.max === 'string' ? r.max.trim() : ''
|
||
const haveS =
|
||
typeof ctx.bareOsPearRuntimeVersion === 'string'
|
||
? ctx.bareOsPearRuntimeVersion.trim()
|
||
: ''
|
||
let ok = Boolean(haveS)
|
||
if (ok && minS) ok = semverGte(haveS, minS)
|
||
if (ok && maxS) ok = semverLte(haveS, maxS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requirePearRuntimeRange',
|
||
'[boot-policy] requirePearRuntimeRange not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireBareRuntimeMin === 'string' &&
|
||
String(pol.requireBareRuntimeMin).trim()
|
||
) {
|
||
const needS = String(pol.requireBareRuntimeMin).trim()
|
||
const haveS =
|
||
typeof ctx.bareOsBareRuntimeVersion === 'string'
|
||
? ctx.bareOsBareRuntimeVersion.trim()
|
||
: ''
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireBareRuntimeMin',
|
||
'[boot-policy] requireBareRuntimeMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requirePearRuntimeMin === 'string' &&
|
||
String(pol.requirePearRuntimeMin).trim()
|
||
) {
|
||
const needS = String(pol.requirePearRuntimeMin).trim()
|
||
const haveS =
|
||
typeof ctx.bareOsPearRuntimeVersion === 'string'
|
||
? ctx.bareOsPearRuntimeVersion.trim()
|
||
: ''
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requirePearRuntimeMin',
|
||
'[boot-policy] requirePearRuntimeMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireProtocolPackageMin === 'string' &&
|
||
String(pol.requireProtocolPackageMin).trim()
|
||
) {
|
||
const needS = String(pol.requireProtocolPackageMin).trim()
|
||
const haveS =
|
||
typeof ctx.bareOsProtocolPackageVersion === 'string'
|
||
? ctx.bareOsProtocolPackageVersion.trim()
|
||
: ''
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireProtocolPackageMin',
|
||
'[boot-policy] requireProtocolPackageMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireBooterSemver === 'string' &&
|
||
String(pol.requireBooterSemver).trim()
|
||
) {
|
||
const needS = String(pol.requireBooterSemver).trim()
|
||
const haveS =
|
||
typeof ctx.bareOsBooterPackageVersion === 'string'
|
||
? ctx.bareOsBooterPackageVersion.trim()
|
||
: ''
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireBooterSemver',
|
||
'[boot-policy] requireBooterSemver not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireCtxApiMin === 'string' &&
|
||
String(pol.requireCtxApiMin).trim()
|
||
) {
|
||
const needS = String(pol.requireCtxApiMin).trim()
|
||
const haveS =
|
||
typeof ctx.bareOsCtxApiVersion === 'string'
|
||
? ctx.bareOsCtxApiVersion.trim()
|
||
: String(ctx.bareOsCtxApiVersion || '').trim()
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireCtxApiMin',
|
||
'[boot-policy] requireCtxApiMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (Array.isArray(pol.denyKernelExtensionIds) && ctx.env) {
|
||
const parts = pol.denyKernelExtensionIds
|
||
.map((x) => String(x).trim())
|
||
.filter(Boolean)
|
||
if (parts.length)
|
||
ctx.env.BARE_OS_BOOT_POLICY_DENY_KERNEL_EXT_IDS = parts.join(',')
|
||
}
|
||
if (
|
||
pol.kernelExtensionHashPins &&
|
||
typeof pol.kernelExtensionHashPins === 'object' &&
|
||
ctx.env
|
||
) {
|
||
try {
|
||
ctx.env.BARE_OS_BOOT_POLICY_EXTENSION_HASH_PINS_JSON = JSON.stringify(
|
||
pol.kernelExtensionHashPins
|
||
)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (pol.offlineLkgIntegrityStrict === true && ctx.env) {
|
||
ctx.env.BARE_OS_OFFLINE_LKG_INTEGRITY_STRICT = '1'
|
||
}
|
||
if (
|
||
typeof pol.requireInitJsSha256 === 'string' &&
|
||
String(pol.requireInitJsSha256).trim()
|
||
) {
|
||
const need = String(pol.requireInitJsSha256).trim().toLowerCase()
|
||
const buf = await drive.get('/boot/init.js')
|
||
const hashFn = ctx.bareOsBootFileSha256Hex
|
||
if (typeof hashFn !== 'function') {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireInitJsSha256MissingHasher',
|
||
'[boot-policy] requireInitJsSha256 needs ctx.bareOsBootFileSha256Hex'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
} else {
|
||
const hex = hashFn(buf || new Uint8Array())
|
||
if (hex !== need) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireInitJsSha256Mismatch',
|
||
'[boot-policy] requireInitJsSha256 mismatch for /boot/init.js'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (Array.isArray(pol.allowedPearIpcChannels) && ctx.env) {
|
||
const parts = pol.allowedPearIpcChannels
|
||
.map((c) => String(c).trim())
|
||
.filter(Boolean)
|
||
if (parts.length)
|
||
ctx.env.BARE_OS_BOOT_POLICY_PEAR_IPC_CHANNELS = parts.join(',')
|
||
}
|
||
if (Array.isArray(pol.denyVfsPrefixes) && ctx.env) {
|
||
const parts = pol.denyVfsPrefixes
|
||
.map((p) => String(p).trim())
|
||
.filter(Boolean)
|
||
if (parts.length) ctx.env.BARE_OS_BOOT_POLICY_DENY_VFS = parts.join(',')
|
||
}
|
||
if (Array.isArray(pol.denyExecLineBuiltins) && ctx.env) {
|
||
const parts = pol.denyExecLineBuiltins
|
||
.map((b) => String(b).trim())
|
||
.filter(Boolean)
|
||
if (parts.length) {
|
||
ctx.env.BARE_OS_BOOT_POLICY_DENY_EXEC_LINE_BUILTINS =
|
||
parts.join(',')
|
||
}
|
||
}
|
||
if (Array.isArray(pol.allowedCtxMethods) && ctx.env) {
|
||
const parts = pol.allowedCtxMethods
|
||
.map((m) => String(m).trim())
|
||
.filter(Boolean)
|
||
if (parts.length) {
|
||
ctx.env.BARE_OS_BOOT_POLICY_ALLOWED_CTX_METHODS = parts.join(',')
|
||
}
|
||
}
|
||
if (Array.isArray(pol.denyCtxMethodPrefixes) && ctx.env) {
|
||
const parts = pol.denyCtxMethodPrefixes
|
||
.map((m) => String(m).trim())
|
||
.filter(Boolean)
|
||
if (parts.length) {
|
||
ctx.env.BARE_OS_BOOT_POLICY_DENY_CTX_PREFIXES = parts.join(',')
|
||
}
|
||
}
|
||
if (Array.isArray(pol.denySeedRpcMethods) && ctx.env) {
|
||
const parts = pol.denySeedRpcMethods
|
||
.map((m) => String(m).trim())
|
||
.filter(Boolean)
|
||
const existing = String(
|
||
ctx.env.BARE_OS_BOOT_POLICY_DENY_SEED_RPC_METHODS || ''
|
||
)
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
const merged = [...new Set([...existing, ...parts])]
|
||
if (merged.length) {
|
||
ctx.env.BARE_OS_BOOT_POLICY_DENY_SEED_RPC_METHODS = merged.join(',')
|
||
}
|
||
}
|
||
if (
|
||
Array.isArray(pol.denyBareModuleSpecifierPatterns) &&
|
||
ctx.env
|
||
) {
|
||
try {
|
||
ctx.env.BARE_OS_BOOT_POLICY_DENY_BARE_MODULE_SPECS_JSON =
|
||
JSON.stringify(pol.denyBareModuleSpecifierPatterns)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (
|
||
Array.isArray(pol.denyKernelSyscalls) &&
|
||
ctx.env
|
||
) {
|
||
const parts = pol.denyKernelSyscalls
|
||
.map((s) => String(s).trim())
|
||
.filter(Boolean)
|
||
if (parts.length) {
|
||
ctx.env.BARE_OS_BOOT_POLICY_DENY_KERNEL_SYSCALLS = parts.join(',')
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireBareCryptoMin === 'string' &&
|
||
String(pol.requireBareCryptoMin).trim()
|
||
) {
|
||
const needS = String(pol.requireBareCryptoMin).trim()
|
||
const haveS =
|
||
ctx.env && String(ctx.env.BARE_OS_BARE_CRYPTO_VERSION || '').trim()
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireBareCryptoMin',
|
||
'[boot-policy] requireBareCryptoMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requirePearIpcMin === 'string' &&
|
||
String(pol.requirePearIpcMin).trim()
|
||
) {
|
||
const needS = String(pol.requirePearIpcMin).trim()
|
||
const haveS =
|
||
ctx.env && String(ctx.env.BARE_OS_PEAR_IPC_PACKAGE_VERSION || '').trim()
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requirePearIpcMin',
|
||
'[boot-policy] requirePearIpcMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
pol.extensionSignerPinsV2 &&
|
||
typeof pol.extensionSignerPinsV2 === 'object' &&
|
||
ctx.env
|
||
) {
|
||
try {
|
||
ctx.env.BARE_OS_BOOT_POLICY_EXTENSION_SIGNER_PINS_V2_JSON =
|
||
JSON.stringify(pol.extensionSignerPinsV2)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (
|
||
pol.extensionSignerPinsV3 &&
|
||
typeof pol.extensionSignerPinsV3 === 'object' &&
|
||
ctx.env
|
||
) {
|
||
try {
|
||
ctx.env.BARE_OS_BOOT_POLICY_EXTENSION_SIGNER_PINS_V3_JSON =
|
||
JSON.stringify(pol.extensionSignerPinsV3)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (
|
||
pol.extensionSignerPinsV4 &&
|
||
typeof pol.extensionSignerPinsV4 === 'object' &&
|
||
ctx.env
|
||
) {
|
||
try {
|
||
ctx.env.BARE_OS_BOOT_POLICY_EXTENSION_SIGNER_PINS_V4_JSON =
|
||
JSON.stringify(pol.extensionSignerPinsV4)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (
|
||
pol.extensionSignerPinsV5 &&
|
||
typeof pol.extensionSignerPinsV5 === 'object' &&
|
||
ctx.env
|
||
) {
|
||
try {
|
||
ctx.env.BARE_OS_BOOT_POLICY_EXTENSION_SIGNER_PINS_V5_JSON =
|
||
JSON.stringify(pol.extensionSignerPinsV5)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireBareBootMin === 'string' &&
|
||
String(pol.requireBareBootMin).trim()
|
||
) {
|
||
const needS = String(pol.requireBareBootMin).trim()
|
||
const haveS =
|
||
ctx.env && String(ctx.env.BARE_OS_BARE_BOOT_VERSION || '').trim()
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireBareBootMin',
|
||
'[boot-policy] requireBareBootMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
Array.isArray(pol.denyBareRpcMethodPatterns) &&
|
||
ctx.env
|
||
) {
|
||
try {
|
||
ctx.env.BARE_OS_BOOT_POLICY_DENY_BARE_RPC_PATTERNS_JSON =
|
||
JSON.stringify(pol.denyBareRpcMethodPatterns)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.maxPearInspectDepth === 'number' &&
|
||
pol.maxPearInspectDepth >= 0 &&
|
||
ctx.env
|
||
) {
|
||
ctx.env.BARE_OS_BOOT_POLICY_MAX_PEAR_INSPECT_DEPTH = String(
|
||
Math.min(64, Math.floor(pol.maxPearInspectDepth))
|
||
)
|
||
}
|
||
if (
|
||
typeof pol.maxHrpcAllowlistDepth === 'number' &&
|
||
pol.maxHrpcAllowlistDepth >= 0 &&
|
||
ctx.env
|
||
) {
|
||
ctx.env.BARE_OS_BOOT_POLICY_MAX_HRPC_ALLOWLIST_DEPTH = String(
|
||
Math.min(64, Math.floor(pol.maxHrpcAllowlistDepth))
|
||
)
|
||
}
|
||
if (
|
||
typeof pol.requireBareLoggerMin === 'string' &&
|
||
String(pol.requireBareLoggerMin).trim()
|
||
) {
|
||
const needS = String(pol.requireBareLoggerMin).trim()
|
||
const haveS =
|
||
ctx.env && String(ctx.env.BARE_OS_BARE_LOGGER_VERSION || '').trim()
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireBareLoggerMin',
|
||
'[boot-policy] requireBareLoggerMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
Array.isArray(pol.denyAutobaseDiscoveryChannels) &&
|
||
ctx.env
|
||
) {
|
||
try {
|
||
ctx.env.BARE_OS_BOOT_POLICY_DENY_AUTOBASE_DISCOVERY_CHANNELS_JSON =
|
||
JSON.stringify(pol.denyAutobaseDiscoveryChannels)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireBareTlsMin === 'string' &&
|
||
String(pol.requireBareTlsMin).trim()
|
||
) {
|
||
const needS = String(pol.requireBareTlsMin).trim()
|
||
const haveS =
|
||
ctx.env && String(ctx.env.BARE_OS_BARE_TLS_VERSION || '').trim()
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireBareTlsMin',
|
||
'[boot-policy] requireBareTlsMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireBarePackMin === 'string' &&
|
||
String(pol.requireBarePackMin).trim()
|
||
) {
|
||
const needS = String(pol.requireBarePackMin).trim()
|
||
const haveS =
|
||
ctx.env && String(ctx.env.BARE_OS_BARE_PACK_VERSION || '').trim()
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireBarePackMin',
|
||
'[boot-policy] requireBarePackMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
typeof pol.requireBareAddonPolicyMin === 'string' &&
|
||
String(pol.requireBareAddonPolicyMin).trim()
|
||
) {
|
||
const needS = String(pol.requireBareAddonPolicyMin).trim()
|
||
const haveS =
|
||
ctx.env &&
|
||
String(ctx.env.BARE_OS_BARE_ADDON_POLICY_VERSION || '').trim()
|
||
const ok = haveS && semverGte(haveS, needS)
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireBareAddonPolicyMin',
|
||
'[boot-policy] requireBareAddonPolicyMin not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (pol.offlineLkgRequirePearStamp === true && ctx.env) {
|
||
ctx.env.BARE_OS_OFFLINE_LKG_REQUIRE_PEAR_STAMP = '1'
|
||
}
|
||
if (pol.offlineLkgRequireHypercorePackHrpcLifecycle === true && ctx.env) {
|
||
ctx.env.BARE_OS_OFFLINE_LKG_REQUIRE_HYPERCORE_PACK_HRPC_LIFECYCLE = '1'
|
||
}
|
||
if (
|
||
typeof pol.offlineLkgManifestMaxAgeSec === 'number' &&
|
||
pol.offlineLkgManifestMaxAgeSec > 0 &&
|
||
ctx.env
|
||
) {
|
||
ctx.env.BARE_OS_OFFLINE_LKG_MANIFEST_MAX_AGE_SEC = String(
|
||
Math.min(1e9, Math.floor(pol.offlineLkgManifestMaxAgeSec))
|
||
)
|
||
}
|
||
if (
|
||
typeof pol.maxProtomuxChannelNameLength === 'number' &&
|
||
pol.maxProtomuxChannelNameLength >= 8 &&
|
||
ctx.env
|
||
) {
|
||
const n = Math.min(512, Math.floor(pol.maxProtomuxChannelNameLength))
|
||
ctx.env.BARE_OS_BOOT_POLICY_MAX_PROTO_MUX_CHANNEL_NAME_LENGTH = String(n)
|
||
}
|
||
if (
|
||
typeof pol.maxKernelExtensionDepth === 'number' &&
|
||
pol.maxKernelExtensionDepth >= 1 &&
|
||
ctx.env
|
||
) {
|
||
const d = Math.min(32, Math.floor(pol.maxKernelExtensionDepth))
|
||
ctx.env.BARE_OS_BOOT_POLICY_MAX_KERNEL_EXT_DEPTH = String(d)
|
||
}
|
||
if (
|
||
typeof pol.gitPartialClonePolicy === 'string' &&
|
||
String(pol.gitPartialClonePolicy).trim() &&
|
||
ctx.env
|
||
) {
|
||
ctx.env.BARE_OS_BOOT_POLICY_GIT_PARTIAL_CLONE = String(
|
||
pol.gitPartialClonePolicy
|
||
).trim()
|
||
}
|
||
if (
|
||
typeof pol.maxInitdRestartsPerUnit === 'number' &&
|
||
pol.maxInitdRestartsPerUnit >= 1 &&
|
||
ctx.env
|
||
) {
|
||
const cap = Math.min(32, Math.floor(pol.maxInitdRestartsPerUnit))
|
||
ctx.env.BARE_OS_INITD_RESTART_MAX_DEFAULT = String(cap)
|
||
}
|
||
if (
|
||
typeof pol.maxExecLineDepth === 'number' &&
|
||
pol.maxExecLineDepth > 0 &&
|
||
ctx.env
|
||
) {
|
||
const cap = Math.min(256, Math.floor(pol.maxExecLineDepth))
|
||
ctx.env.BARE_OS_EXEC_MAX_DEPTH = String(cap)
|
||
}
|
||
if (Array.isArray(pol.denyEnvKeys) && ctx.env) {
|
||
for (const k of pol.denyEnvKeys) {
|
||
const key = String(k).trim()
|
||
if (key && Object.prototype.hasOwnProperty.call(ctx.env, key)) {
|
||
delete ctx.env[key]
|
||
}
|
||
}
|
||
}
|
||
const needLifecycleSchema = Math.max(
|
||
typeof pol.bootStagesRequireLifecycleMinSchema === 'number'
|
||
? pol.bootStagesRequireLifecycleMinSchema
|
||
: 0,
|
||
typeof pol.bootPhasesRequireLifecycleMinSchema === 'number'
|
||
? pol.bootPhasesRequireLifecycleMinSchema
|
||
: 0
|
||
)
|
||
if (needLifecycleSchema > 0) {
|
||
const needSch = Math.floor(needLifecycleSchema)
|
||
const raw = ctx.env && String(ctx.env.BARE_OS_LIFECYCLE_SCHEMA_VERSION || '').trim()
|
||
const haveSch = Number.parseInt(raw, 10)
|
||
const ok = Number.isFinite(haveSch) && haveSch >= needSch
|
||
if (!ok) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootStagesRequireLifecycleMinSchema',
|
||
'[boot-policy] bootStagesRequireLifecycleMinSchema not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
const needProcIndexSchema = Math.max(
|
||
typeof pol.bootStagesRequireProcIndexMinSchema === 'number'
|
||
? pol.bootStagesRequireProcIndexMinSchema
|
||
: 0,
|
||
typeof pol.bootPhasesRequireProcIndexMinSchema === 'number'
|
||
? pol.bootPhasesRequireProcIndexMinSchema
|
||
: 0
|
||
)
|
||
if (needProcIndexSchema > 0) {
|
||
const vfs = ctx.vfs
|
||
const needSch = Math.floor(needProcIndexSchema)
|
||
if (vfs && typeof vfs.readFile === 'function') {
|
||
try {
|
||
const raw = await vfs.readFile('/proc/bare_os/index.json')
|
||
const txt =
|
||
raw && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(raw)
|
||
: ''
|
||
const j = txt ? JSON.parse(txt) : null
|
||
const sch =
|
||
j && typeof j === 'object' && typeof j.schema === 'number'
|
||
? j.schema
|
||
: 0
|
||
if (sch < needSch) throw new Error('proc_index schema too old')
|
||
} catch {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootStagesRequireProcIndexMinSchema',
|
||
'[boot-policy] bootStagesRequireProcIndexMinSchema not satisfied'
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (Array.isArray(pol.requireProcNodes)) {
|
||
const vfs = ctx.vfs
|
||
if (vfs && typeof vfs.readFile === 'function') {
|
||
for (const rel of pol.requireProcNodes) {
|
||
const raw = String(rel).trim()
|
||
const p = raw.startsWith('/')
|
||
? raw
|
||
: '/proc/' + raw.replace(/^\/*/, '')
|
||
try {
|
||
const b = await vfs.readFile(p)
|
||
const empty =
|
||
!b ||
|
||
(ctx.b4a &&
|
||
typeof ctx.b4a.from === 'function' &&
|
||
ctx.b4a.from(b).length === 0)
|
||
if (empty) throw new Error('empty or missing')
|
||
} catch {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'requireProcNodes',
|
||
'[boot-policy] requireProcNodes not satisfied: ' + p
|
||
)
|
||
if (strictPol) {
|
||
if (typeof ctx.requestBooterExit === 'function') {
|
||
ctx.requestBooterExit(1)
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootPolicyParse',
|
||
'[boot-policy] boot.policy.json: ' + ((e && e.message) || String(e))
|
||
)
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
}
|
||
|
||
/**
|
||
* Hyperdrive-resident kernel (staged as /boot/init.js).
|
||
* Loaded by the booter with an injected ctx object (trusted replication source).
|
||
*
|
||
* Boot order: /etc/os-release → /etc/motd → optional profile rc → /etc/bare-os/rc →
|
||
* /etc/bare-os/rc.d/* (sorted; digit-prefixed snippet names) → /etc/bare-os/rc.local →
|
||
* /etc/bare-os/kernel.d/* (same naming rules as rc.d) → session banner →
|
||
* optional onboot lines when BARE_OS_SKIP_REPL → interactive loop.
|
||
*
|
||
* Profile: first non-empty line of /etc/bare-os/profile, overridden by BARE_OS_BOOT_PROFILE.
|
||
* When set, runs /etc/bare-os/rc.profile.<name> if present (trusted execLine, before main rc).
|
||
*
|
||
* Non-interactive onboot: when ctx.bareOsSkipRepl, runs each non-empty, non-# line from
|
||
* BARE_OS_ONBOOT (newline-separated) or, if unset, every such line from /etc/bare-os/onboot
|
||
* in file order (trusted). Then readLine yields EOF.
|
||
*
|
||
* BARE_OS_BOOT_TRACE=json logs one JSON object per boot stage on stderr: **`step`** (preferred), **`stage`**, legacy **`phase`** mirror, **`ms`**, **`bootTraceSchemaVersion`** (2). Guest **`os-release`**, **`motd`**, and **`banner`** lines also emit **`type":"bootOutput"`** previews on stderr (stdout body still goes through **`ctx.console.log`** only).
|
||
* BARE_OS_BOOT_TRACE=ndjson logs **`bootTraceSchemaVersion`**: **2** lines with **`type":"boot"`**, **`step`**, **`stage`**, **`phase`** (mirror), **`ms`**, **`sessionId`**, **`ts`**, plus **`type":"bootOutput"`** for guest-visible text previews.
|
||
*
|
||
* BARE_OS_BOOT_MINIMAL=1 or true: skip profile rc, rc, rc.d, rc.local, kernel.d (recovery shell).
|
||
* BARE_OS_BOOT_SAFE_MODE=1 or true: after boot policy merge, skip rc.d, kernel.ext.d, onboot (lighter recovery).
|
||
* BARE_OS_BOOT_TRANSACTION_JOURNAL=1, true, or ndjson: append boot stage NDJSON lines to
|
||
* /run/bare-os/boot-transaction.ndjson when ctx.vfs supports readFile/writeFile.
|
||
* BARE_OS_BOOT_CHECKPOINT=1 or true: write /run/bare-os/boot-checkpoint.json after each completed boot stage (schema 2 adds bootStage; legacy `phase` key retained).
|
||
* BARE_OS_BOOT_DRY_RUN=1 or true: parse rc/kernel.ext.d but skip trusted execLine and extension script execution (CI).
|
||
* BARE_OS_BOOT_POLICY_PATH=/etc/bare-os/… optional primary boot.policy.json path (must stay under /etc/bare-os/).
|
||
* BARE_OS_BOOT_ROLLBACK_APPLY=1: merge skip stages from /run/bare-os/boot-rollback.marker JSON when vfs supports readFile.
|
||
* BARE_OS_BOOT_BUNDLE_DIGEST_HEX: compared to boot.policy.json requireBootBundleSha256Hex when set.
|
||
* BARE_OS_REQUIRE_CTX_API_MIN & BARE_OS_BOOT_ABI_STRICT: enforce ctx.bareOsCtxApiVersion semver vs minimum (strict exits boot).
|
||
* BARE_OS_BOOT_SKIP_STAGES (preferred) and BARE_OS_BOOT_SKIP (legacy): comma-separated boot stages to skip among:
|
||
* profile, rc, rc.d, rc.local, kernel.d, kernel.ext.d, onboot (os-release and motd always run).
|
||
*
|
||
* BARE_OS_KERNEL_SELFTEST=1 or true: after boot snippets, run a short trusted self-check via execLine.
|
||
* BARE_OS_SELFTEST_FORMAT=tap: TAP-style lines on stderr for CI parsers; **junit**: single XML `<testsuite>` line on stderr.
|
||
* BARE_OS_KERNEL_STARTUP_CLASS: `critical` | `system` | `interactive` | `deferred` (default **interactive**); exposed as **`ctx.bareOsKernelStartupClass`** for extensions/initd.
|
||
* BARE_OS_BOOT_BUDGET_MS_COLD: optional cold-boot wall-time warning threshold (ms) after **`bareOsPublishBootReady`**.
|
||
* BARE_OS_BOOT_BUDGET_MS_BARE_STDLIB: optional wall-time budget (ms) for booter **`ctx.bare`** drive merge + host resolve (**`BARE_OS_BOOT_BARE_STDLIB_RESOLUTION_MS`**, set by the stock booter); guest logs and **`boot-perf.json`** when exceeded.
|
||
* BARE_OS_BOOT_BUDGET_STRICT=1 with **BARE_OS_BOOT_POLICY_STRICT**: exit via **`bareOsRequestBooterExit(1)`** when either budget is exceeded (after **`boot-transaction.ndjson`** row **`bootBudgetViolation`** when journaling is on).
|
||
* BARE_OS_BOOT_CAPABILITY_CONTRACT_DEBUG=1: log capability-contract merge diagnostics (strict builds).
|
||
* BARE_OS_BOOT_EXT_RESOLUTION_TRACE=1 with **BARE_OS_BOOT_POLICY_STRICT**: write **`/run/bare-os/kernel-ext-resolution.json`** (extension load order / cycle ids).
|
||
* BARE_OS_BOOT_RC_RESOLUTION_TRACE=1 with **BARE_OS_BOOT_POLICY_STRICT**: write **`/run/bare-os/rc-d-resolution.json`** and **`/run/bare-os/kernel-d-resolution.json`** (lexicographic execution order after filters).
|
||
* BARE_OS_BOOT_ALLOWLIST=1 and /etc/bare-os/boot.allow: only first-word commands in that file (plus shell builtins) run from trusted rc/onboot snippets.
|
||
*
|
||
* BARE_OS_BOOT_MANIFEST_SIGN=1: verify Ed25519 signature in /etc/bare-os/boot.manifest.sig over the raw
|
||
* manifest bytes; public key from BARE_OS_BOOT_MANIFEST_PUBKEY_HEX (64 hex chars). Uses ctx.bareOsVerifyBootManifestSignature.
|
||
*
|
||
* BARE_OS_KERNEL_EXT_D_HOT_RELOAD=1: after boot, exposes **`ctx.bareOsReloadKernelExtDropinsSafe()`** which re-scans
|
||
* `/etc/bare-os/kernel.ext.d` and runs only extension scripts not yet recorded in **`ctx.bareOsLoadedKernelExtScripts`**
|
||
* (append-only; does not unload). Append-only audit: **`/run/bare-os/kernel-ext-reload.ndjson`** when **`ctx.vfs.writeFile`** exists.
|
||
*
|
||
* BARE_OS_VFS_HYPERBLOBS_DEDUP=1: operator hint surfaced in **`/proc/bare_os/features`** — content-defined chunking may be enabled
|
||
* in host mirror/hyperblob pipelines; the guest VFS does not turn on hyperblobs automatically.
|
||
*
|
||
* BARE_OS_BOOT_POLICY=1: merge `skipBootStages` / `denyBootStages` from boot.policy (legacy `skipPhases` / `denyBootPhases` still honored; see applyBootPolicyFile). Optional `policyFallbackPaths` for tiered skip merge; `initdAdmission` sets initd env caps; `extensionSignerPinsV5` multi-signer pins.
|
||
* Boot policy v2 (optional): `maxExecLineDepth`, `denyEnvKeys`, `requireProcNodes` (VFS paths under /proc).
|
||
* Optional `minKernelCapabilitiesPrimary` / `requireSeedCaps` (integers) when ctx exposes
|
||
* `bareOsAdvertisedKernelCapabilityWords` / `bareOsSeedKernelCapabilityWords` (wire v2: `primary` word).
|
||
* `BARE_OS_BOOT_POLICY_STRICT=1`: exit boot on policy violation (via requestBooterExit(1)).
|
||
*
|
||
* kernel.d: optional leading comment lines `# ConditionEnvironment=KEY=VAL` or `# AssertEnvironment=KEY=VAL` skip the snippet when env does not match.
|
||
* kernel.ext.d: JSON drop-ins under /etc/bare-os/kernel.ext.d/*.json with `{ "scripts": ["/lib/bare-os/extensions/…"] }` (trusted).
|
||
* Optional per-drop-in **`minCtxApiVersion`** (semver): skip or strict-fail when **`ctx.bareOsCtxApiVersion`** is lower.
|
||
* Optional **`conflictsWith`**: string array of other extension **`id`** values; strict boot fails if both are present.
|
||
* Optional ordering: `dependsOn`, `requires` (merged into dependsOn), `after` (ids loaded before this drop-in),
|
||
* `before` (ids that must load after this drop-in). Cycles fail strict boot or fall back to filename order.
|
||
*
|
||
* BARE_OS_BOOT_STRICT=1 or true: first execLine throw in trusted boot snippets calls
|
||
* requestBooterExit(1) and stops further boot stages.
|
||
*
|
||
* BARE_OS_RC_D_SKIP: comma-separated rc.d basenames to skip; a pattern ending with * skips
|
||
* names with that prefix (e.g. 10-* skips 10-foo).
|
||
*
|
||
* BARE_OS_RC_PROPOSAL_MULTISIG_STRICT=1: before **rc.d**, require **`/etc/bare-os/pear.multisig.json`**
|
||
* ( **`bareOsPearMultisigShapeOk`** ) and validate every **`/etc/bare-os/rc.proposals/enabled/*.json`**
|
||
* (**`schema`**: **1**, **`proposalId`**, **`targetRcSnippet`** under **`/etc/bare-os/rc.d/`**, **`signaturesFrom`**: distinct
|
||
* public keys each listed in **`pear.multisig.json`** **`signers`**, count ≥ **`quorum`**). Emits **`ctx.bareOsAuditLogAppend`**
|
||
* rows **`rc.proposal.multisig_ok`** / **`rc.proposal.multisig_fail`** when the hook exists.
|
||
*
|
||
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
|
||
*/
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function wantBootTrace(ctx) {
|
||
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
|
||
return v === '1' || v === 'true' || v === 'json' || v === 'ndjson'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function isBootTraceJson(ctx) {
|
||
return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'json'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function isBootTraceNdjson(ctx) {
|
||
return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'ndjson'
|
||
}
|
||
|
||
/**
|
||
* Guest-visible boot text via **`ctx.console.log`** only (booter session / var-log mirror).
|
||
* When **`BARE_OS_BOOT_TRACE`** is **`json`** or **`ndjson`**, append one stderr JSON line per emission (**`type":"bootOutput"`**, preview-capped) for CI and hosts.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} step
|
||
* @param {string} text
|
||
*/
|
||
function bootGuestOutLine(ctx, step, text) {
|
||
const line = String(text)
|
||
const out = ctx.console
|
||
if (out && typeof out.log === 'function') out.log(line)
|
||
else if (
|
||
globalThis.console &&
|
||
typeof globalThis.console.log === 'function'
|
||
) {
|
||
globalThis.console.log(line)
|
||
}
|
||
if (!wantBootTrace(ctx)) return
|
||
const prev =
|
||
isBootTraceNdjson(ctx) && line.length > 400
|
||
? line.slice(0, 400) + '\u2026'
|
||
: isBootTraceJson(ctx) && line.length > 200
|
||
? line.slice(0, 200) + '\u2026'
|
||
: line
|
||
const errFn =
|
||
out && typeof out.error === 'function'
|
||
? out.error.bind(out)
|
||
: globalThis.console && typeof globalThis.console.error === 'function'
|
||
? globalThis.console.error.bind(globalThis.console)
|
||
: null
|
||
if (!errFn) return
|
||
if (isBootTraceNdjson(ctx)) {
|
||
errFn(
|
||
JSON.stringify({
|
||
type: 'bootOutput',
|
||
bootTraceSchemaVersion: 2,
|
||
step,
|
||
stream: 'stdout',
|
||
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''),
|
||
ts: Date.now(),
|
||
textPreview: prev
|
||
})
|
||
)
|
||
} else if (isBootTraceJson(ctx)) {
|
||
errFn(
|
||
JSON.stringify({
|
||
bootTraceSchemaVersion: 2,
|
||
type: 'bootOutput',
|
||
step,
|
||
stream: 'stdout',
|
||
textPreview: prev
|
||
})
|
||
)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Prefer **`ctx.console`** for boot-policy and boot-stage messages; when **`BARE_OS_BOOT_TRACE`**
|
||
* is **`json`** or **`ndjson`**, also emit **`type":"bootLog"`** on stderr for structured parsers.
|
||
* @param {'error'|'warn'} severity
|
||
* @param {string} code Stable machine-readable code (policy field name, **`boot.allow`**, etc.).
|
||
* @param {string} humanLine Operator-visible line (keep **`[boot-policy]`** / **`[boot]`** prefixes).
|
||
*/
|
||
function bootStructuredLog(ctx, severity, code, humanLine) {
|
||
const out = ctx.console
|
||
const line = String(humanLine)
|
||
if (severity === 'warn') {
|
||
if (out && typeof out.warn === 'function') out.warn(line)
|
||
else if (globalThis.console && typeof globalThis.console.warn === 'function') {
|
||
globalThis.console.warn(line)
|
||
}
|
||
} else {
|
||
if (out && typeof out.error === 'function') out.error(line)
|
||
else if (globalThis.console && typeof globalThis.console.error === 'function') {
|
||
globalThis.console.error(line)
|
||
}
|
||
}
|
||
if (!wantBootTrace(ctx)) return
|
||
const errFn =
|
||
out && typeof out.error === 'function'
|
||
? out.error.bind(out)
|
||
: globalThis.console && typeof globalThis.console.error === 'function'
|
||
? globalThis.console.error.bind(globalThis.console)
|
||
: null
|
||
if (!errFn) return
|
||
const msg = line.length > 600 ? line.slice(0, 600) + '\u2026' : line
|
||
const payload = {
|
||
type: 'bootLog',
|
||
bootTraceSchemaVersion: 2,
|
||
severity,
|
||
code: String(code),
|
||
message: msg,
|
||
ts: Date.now(),
|
||
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || '')
|
||
}
|
||
if (isBootTraceNdjson(ctx)) {
|
||
errFn(JSON.stringify(payload))
|
||
} else if (isBootTraceJson(ctx)) {
|
||
errFn(JSON.stringify({ ...payload, step: 'bootLog' }))
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function bootMinimal(ctx) {
|
||
const v = ctx.env && ctx.env.BARE_OS_BOOT_MINIMAL
|
||
return v === '1' || v === 'true'
|
||
}
|
||
|
||
/**
|
||
* Recovery-oriented boot: skip rc.d snippets, kernel extensions, and onboot after policy merge.
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function applyBootSafeMode(ctx) {
|
||
const v = ctx.env && ctx.env.BARE_OS_BOOT_SAFE_MODE
|
||
if (v !== '1' && v !== 'true') return
|
||
const pol =
|
||
ctx.bareOsBootPolicySkipStages instanceof Set
|
||
? ctx.bareOsBootPolicySkipStages
|
||
: ctx.bareOsBootPolicySkipPhases
|
||
if (!(pol instanceof Set)) return
|
||
pol.add('rc.d')
|
||
pol.add('kernel.ext.d')
|
||
pol.add('onboot')
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function kernelExtHotReloadEnabled(ctx) {
|
||
const v = ctx.env?.BARE_OS_KERNEL_EXT_D_HOT_RELOAD
|
||
return v === '1' || v === 'true'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {Record<string, unknown>} row
|
||
*/
|
||
async function maybeAppendKernelExtReloadJournal(ctx, row) {
|
||
const vfs = ctx.vfs
|
||
const b4 = ctx.b4a
|
||
if (!vfs || typeof vfs.writeFile !== 'function' || !b4) return
|
||
const path = '/run/bare-os/kernel-ext-reload.ndjson'
|
||
const line =
|
||
JSON.stringify({
|
||
kernelExtReloadSchemaVersion: 1,
|
||
ts: Date.now(),
|
||
...row
|
||
}) + '\n'
|
||
try {
|
||
let prev = ''
|
||
try {
|
||
const buf = await vfs.readFile(path)
|
||
if (buf) prev = b4.toString(buf)
|
||
} catch {
|
||
/* new */
|
||
}
|
||
await vfs.writeFile(path, b4.from(prev + line))
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
async function maybeAppendBootTransactionJournal(ctx, row) {
|
||
const en = ctx.env && ctx.env.BARE_OS_BOOT_TRANSACTION_JOURNAL
|
||
if (en !== '1' && en !== 'true' && en !== 'ndjson') return
|
||
const vfs = ctx.vfs
|
||
const b4 = ctx.b4a
|
||
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function' || !b4) return
|
||
const path = '/run/bare-os/boot-transaction.ndjson'
|
||
const phaseStr = String(row && row.phase !== undefined ? row.phase : '')
|
||
const txnState =
|
||
row && row.transactionState != null
|
||
? String(row.transactionState)
|
||
: BARE_OS_BOOT_TXN_STATE.STAGE_COMMITTED
|
||
const line =
|
||
JSON.stringify({
|
||
bootTransactionSchemaVersion: 2,
|
||
type: 'boot_transaction',
|
||
ts: Date.now(),
|
||
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''),
|
||
...row,
|
||
transactionState: txnState,
|
||
bootStage:
|
||
row && typeof row.bootStage === 'string'
|
||
? row.bootStage
|
||
: bootStageForPhase(phaseStr)
|
||
}) + '\n'
|
||
try {
|
||
let prev = ''
|
||
try {
|
||
const buf = await vfs.readFile(path)
|
||
prev = b4.toString(buf)
|
||
} catch {
|
||
/* new */
|
||
}
|
||
await vfs.writeFile(path, b4.from(prev + line))
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} phase
|
||
* @param {number} ms
|
||
*/
|
||
async function maybeWriteBootCheckpoint(ctx, phase, ms) {
|
||
const v = ctx.env && ctx.env.BARE_OS_BOOT_CHECKPOINT
|
||
if (v !== '1' && v !== 'true') return
|
||
const vfs = ctx.vfs
|
||
const b4 = ctx.b4a
|
||
if (!vfs || typeof vfs.writeFile !== 'function' || !b4) return
|
||
try {
|
||
const body =
|
||
JSON.stringify({
|
||
schema: 2,
|
||
phase: String(phase),
|
||
stage: String(phase),
|
||
bootStage: bootStageForPhase(String(phase)),
|
||
phaseMs: typeof ms === 'number' ? ms : 0,
|
||
stageMs: typeof ms === 'number' ? ms : 0,
|
||
atMs: Date.now(),
|
||
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || '')
|
||
}) + '\n'
|
||
await vfs.writeFile('/run/bare-os/boot-checkpoint.json', b4.from(body))
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Immutable boot snapshot under /run/bare-os/boot/snapshot.json for operators / doctor tooling.
|
||
* Schema **2** adds **`provenance`** (**`buildId`**, **`policyHashSketch`** from **`BARE_OS_BOOT_BUNDLE_DIGEST_HEX`**, **`sessionId`**).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {Set<string>} skipSet
|
||
*/
|
||
async function maybeWriteBootSnapshotExport(ctx, skipSet) {
|
||
const vfs = ctx.vfs
|
||
const b4 = ctx.b4a
|
||
if (!vfs || typeof vfs.writeFile !== 'function' || !b4) return
|
||
const policyPathRaw = String(ctx.env?.BARE_OS_BOOT_POLICY_PATH || '').trim()
|
||
const policyPath =
|
||
policyPathRaw && policyPathRaw.startsWith('/etc/bare-os/')
|
||
? policyPathRaw
|
||
: '/etc/bare-os/boot.policy.json'
|
||
try {
|
||
const preview =
|
||
skipSet instanceof Set ? [...skipSet].map((s) => String(s).toLowerCase()).sort() : []
|
||
const sid = String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || '')
|
||
const digestHex = String(ctx.env?.BARE_OS_BOOT_BUNDLE_DIGEST_HEX || '').trim()
|
||
const policyHashSketch =
|
||
digestHex.length >= 16 ? digestHex.slice(0, 16).toLowerCase() : null
|
||
const buildIdRaw = String(
|
||
ctx.env?.BARE_OS_BUILD_ID || ctx.env?.BARE_OS_IMAGE_BUILD_ID || ''
|
||
).trim()
|
||
const buildId = buildIdRaw ? buildIdRaw.slice(0, 128) : null
|
||
const body =
|
||
JSON.stringify({
|
||
schema: 2,
|
||
exportedAtMs: Date.now(),
|
||
sessionId: sid,
|
||
bootPolicyPath: policyPath,
|
||
bootPolicyEnabled:
|
||
ctx.env?.BARE_OS_BOOT_POLICY === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_POLICY === 'true',
|
||
skipBootStagesPreview: preview,
|
||
bootSafeMode:
|
||
ctx.env?.BARE_OS_BOOT_SAFE_MODE === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_SAFE_MODE === 'true',
|
||
bootMinimal: bootMinimal(ctx),
|
||
provenance: {
|
||
schema: 1,
|
||
buildId,
|
||
policyHashSketch,
|
||
sessionId: sid
|
||
}
|
||
}) + '\n'
|
||
await vfs.writeFile('/run/bare-os/boot/snapshot.json', b4.from(body))
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Set<string>}
|
||
*/
|
||
function parseBootSkip(ctx) {
|
||
const out = new Set()
|
||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||
for (const raw of [env.BARE_OS_BOOT_SKIP_STAGES, env.BARE_OS_BOOT_SKIP]) {
|
||
if (raw == null || !String(raw).trim()) continue
|
||
for (const p of String(raw)
|
||
.split(',')
|
||
.map((s) => s.trim().toLowerCase())
|
||
.filter(Boolean)) {
|
||
out.add(p)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} stage
|
||
*/
|
||
function shouldSkipBootStage(ctx, stage) {
|
||
const pol =
|
||
ctx.bareOsBootPolicySkipStages instanceof Set
|
||
? ctx.bareOsBootPolicySkipStages
|
||
: ctx.bareOsBootPolicySkipPhases
|
||
const pl = String(stage).toLowerCase()
|
||
if (pol instanceof Set && pol.has(pl)) return true
|
||
if (bootMinimal(ctx)) {
|
||
return (
|
||
stage === 'profile' ||
|
||
stage === 'rc' ||
|
||
stage === 'rc.d' ||
|
||
stage === 'rc.local' ||
|
||
stage === 'kernel.d' ||
|
||
stage === 'kernel.ext.d' ||
|
||
stage === 'onboot'
|
||
)
|
||
}
|
||
return parseBootSkip(ctx).has(pl)
|
||
}
|
||
|
||
|
||
function bootStrict(ctx) {
|
||
const v = ctx.env && ctx.env.BARE_OS_BOOT_STRICT
|
||
return v === '1' || v === 'true'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {string[]}
|
||
*/
|
||
function parseRcDSkipPatterns(ctx) {
|
||
const raw = ctx.env && ctx.env.BARE_OS_RC_D_SKIP
|
||
if (raw == null || !String(raw).trim()) return []
|
||
return String(raw)
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
}
|
||
|
||
/**
|
||
* @param {string} name
|
||
* @param {string[]} patterns
|
||
*/
|
||
function shouldSkipRcDName(name, patterns) {
|
||
for (const p of patterns) {
|
||
if (p === name) return true
|
||
if (p.endsWith('*') && p.length > 1) {
|
||
const pre = p.slice(0, -1)
|
||
if (name.startsWith(pre)) return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<Set<string> | null>} null when allowlist disabled
|
||
*/
|
||
async function loadBootAllowSet(ctx) {
|
||
const v = ctx.env && ctx.env.BARE_OS_BOOT_ALLOWLIST
|
||
if (v !== '1' && v !== 'true') return null
|
||
const { drive, b4a } = ctx
|
||
const out = new Set()
|
||
try {
|
||
const buf = await drive.get('/etc/bare-os/boot.allow')
|
||
if (!buf) return out
|
||
for (const line of b4a.toString(buf).split(/\r?\n/)) {
|
||
const t = line.trim()
|
||
if (!t || t.startsWith('#')) continue
|
||
out.add(t)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} line
|
||
*/
|
||
function firstShellCommandWord(line) {
|
||
const s = line.trim()
|
||
const m = s.match(/^(\S+)/)
|
||
return m ? m[1] : ''
|
||
}
|
||
|
||
/**
|
||
* @param {string} line
|
||
* @param {Set<string>} allow
|
||
*/
|
||
function bootLineAllowed(line, allow) {
|
||
const w = firstShellCommandWord(line)
|
||
if (!w) return true
|
||
if (
|
||
w === 'export' ||
|
||
w === 'unset' ||
|
||
w === 'readonly' ||
|
||
w === ':' ||
|
||
w === 'umask' ||
|
||
w === 'cd'
|
||
) {
|
||
return true
|
||
}
|
||
if (allow.has(w)) return true
|
||
const base = w.includes('/') ? w.split('/').pop() : w
|
||
return !!(base && allow.has(base))
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} label
|
||
* @param {() => void | Promise<void>} fn
|
||
* @param {string[]} stageLog
|
||
*/
|
||
async function bootTimed(ctx, label, fn, stageLog) {
|
||
await invokeCtxBootHooks(ctx, {
|
||
phase: label,
|
||
stage: label,
|
||
when: 'before',
|
||
label
|
||
})
|
||
await maybeAppendBootTransactionJournal(ctx, {
|
||
phase: label,
|
||
stage: label,
|
||
ms: 0,
|
||
ok: true,
|
||
transactionState: BARE_OS_BOOT_TXN_STATE.STAGE_STARTED,
|
||
note: 'enter'
|
||
})
|
||
const t0 = Date.now()
|
||
try {
|
||
await fn()
|
||
} catch (err) {
|
||
const ms = Date.now() - t0
|
||
await maybeAppendBootTransactionJournal(ctx, {
|
||
phase: label,
|
||
stage: label,
|
||
ms,
|
||
ok: false,
|
||
transactionState: BARE_OS_BOOT_TXN_STATE.STAGE_ROLLBACK,
|
||
error: String(
|
||
err && /** @type {Error} */ (err).message
|
||
? /** @type {Error} */ (err).message
|
||
: err
|
||
)
|
||
})
|
||
throw err
|
||
}
|
||
const ms = Date.now() - t0
|
||
stageLog.push(label)
|
||
if (
|
||
ctx.env?.BARE_OS_BOOT_PERF_DETAIL === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_PERF_DETAIL === 'true'
|
||
) {
|
||
if (!Array.isArray(ctx.bareOsBootStageTimings)) {
|
||
/** @type {{ label: string, wallMs: number, monotonicNs?: string }[]} */
|
||
ctx.bareOsBootStageTimings = []
|
||
}
|
||
let monotonicNs
|
||
let monotonicDeltaNs
|
||
try {
|
||
const bh = await import('bare-hrtime')
|
||
const bg = bh && (bh.bigint || bh.default?.bigint)
|
||
if (typeof bg === 'function') {
|
||
const now = bg.call(bh)
|
||
monotonicNs = String(now)
|
||
const prev = ctx.bareOsBootHrtimePrev
|
||
if (typeof prev === 'bigint')
|
||
monotonicDeltaNs = String(now - prev)
|
||
ctx.bareOsBootHrtimePrev = now
|
||
}
|
||
} catch {
|
||
/* optional native */
|
||
}
|
||
ctx.bareOsBootStageTimings.push({
|
||
label,
|
||
wallMs: ms,
|
||
...(monotonicNs ? { monotonicNs } : {}),
|
||
...(monotonicDeltaNs ? { monotonicDeltaNs } : {})
|
||
})
|
||
}
|
||
await maybeAppendBootTransactionJournal(ctx, {
|
||
phase: label,
|
||
stage: label,
|
||
ms,
|
||
ok: true,
|
||
transactionState: BARE_OS_BOOT_TXN_STATE.STAGE_COMMITTED
|
||
})
|
||
await maybeWriteBootCheckpoint(ctx, label, ms)
|
||
if (wantBootTrace(ctx)) {
|
||
if (isBootTraceNdjson(ctx)) {
|
||
const sid = (ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''
|
||
const rec = {
|
||
type: 'boot',
|
||
bootTraceSchemaVersion: 2,
|
||
step: label,
|
||
stage: label,
|
||
phase: label,
|
||
ms,
|
||
sessionId: sid,
|
||
ts: Date.now()
|
||
}
|
||
const w11 = ctx.env && String(ctx.env.BARE_OS_PROBE_ID_HYPERCORE_PACK_HRPC_LIFECYCLE || '').trim()
|
||
if (w11) rec.hypercorePackHrpcLifecycleProbeId = w11.slice(0, 128)
|
||
const bp = ctx.env && String(ctx.env.BARE_OS_BARE_PACK_VERSION || '').trim()
|
||
if (bp) rec.barePackVersionHint = bp.slice(0, 64)
|
||
const ap = ctx.env &&
|
||
String(ctx.env.BARE_OS_BARE_ADDON_POLICY_VERSION || '').trim()
|
||
if (ap) rec.bareAddonPolicyVersionHint = ap.slice(0, 64)
|
||
ctx.console.error(JSON.stringify(rec))
|
||
} else if (isBootTraceJson(ctx)) {
|
||
const base = {
|
||
bootTraceSchemaVersion: 2,
|
||
step: label,
|
||
stage: label,
|
||
phase: label,
|
||
ms
|
||
}
|
||
const w11 = ctx.env && String(ctx.env.BARE_OS_PROBE_ID_HYPERCORE_PACK_HRPC_LIFECYCLE || '').trim()
|
||
if (w11) base.hypercorePackHrpcLifecycleProbeId = w11.slice(0, 128)
|
||
ctx.console.error(JSON.stringify(base))
|
||
} else {
|
||
ctx.console.error(`[boot] ${label}: ${ms}ms`)
|
||
}
|
||
}
|
||
if (typeof ctx.bareOsEmitBootEvent === 'function') {
|
||
ctx.bareOsEmitBootEvent({
|
||
type: 'boot',
|
||
phase: label,
|
||
stage: label,
|
||
bootStage: bootStageForPhase(label),
|
||
ms,
|
||
ts: Date.now(),
|
||
sessionId: (ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''
|
||
})
|
||
}
|
||
if (typeof ctx.bareOsEmitKernelEvent === 'function') {
|
||
ctx.bareOsEmitKernelEvent({
|
||
type: 'kernel',
|
||
topic: 'boot.phase',
|
||
phase: label,
|
||
stage: label,
|
||
bootStage: bootStageForPhase(label),
|
||
ms,
|
||
ts: Date.now(),
|
||
sessionId: (ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''
|
||
})
|
||
}
|
||
await invokeCtxBootHooks(ctx, {
|
||
phase: label,
|
||
stage: label,
|
||
when: 'after',
|
||
label
|
||
})
|
||
}
|
||
|
||
/** @type {Record<string, unknown> | null | undefined} */
|
||
let bootManifestMemo
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<Record<string, unknown> | null>}
|
||
*/
|
||
async function loadBootManifest(ctx) {
|
||
const v = ctx.env && ctx.env.BARE_OS_BOOT_MANIFEST
|
||
if (v !== '1' && v !== 'true') return null
|
||
if (bootManifestMemo !== undefined) return bootManifestMemo
|
||
const { drive, b4a } = ctx
|
||
try {
|
||
const buf = await drive.get('/etc/bare-os/boot.manifest.json')
|
||
if (!buf) {
|
||
bootManifestMemo = null
|
||
return null
|
||
}
|
||
const signOn =
|
||
ctx.env &&
|
||
(ctx.env.BARE_OS_BOOT_MANIFEST_SIGN === '1' ||
|
||
ctx.env.BARE_OS_BOOT_MANIFEST_SIGN === 'true')
|
||
if (signOn) {
|
||
const sigBuf = await drive.get('/etc/bare-os/boot.manifest.sig')
|
||
const pub =
|
||
ctx.env && ctx.env.BARE_OS_BOOT_MANIFEST_PUBKEY_HEX
|
||
? String(ctx.env.BARE_OS_BOOT_MANIFEST_PUBKEY_HEX).trim()
|
||
: ''
|
||
const verifyFn = ctx.bareOsVerifyBootManifestSignature
|
||
if (typeof verifyFn !== 'function' || !pub) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootManifestSignMissingVerifier',
|
||
'[boot] signed manifest requires ctx.bareOsVerifyBootManifestSignature and BARE_OS_BOOT_MANIFEST_PUBKEY_HEX'
|
||
)
|
||
bootManifestMemo = null
|
||
return null
|
||
}
|
||
if (!verifyFn(buf, sigBuf, pub)) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootManifestSignFailed',
|
||
'[boot] boot.manifest.json Ed25519 signature verification failed'
|
||
)
|
||
bootManifestMemo = null
|
||
return null
|
||
}
|
||
}
|
||
bootManifestMemo = JSON.parse(b4a.toString(buf))
|
||
return bootManifestMemo
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootManifestParse',
|
||
'[boot] boot.manifest.json: ' + ((e && e.message) || String(e))
|
||
)
|
||
bootManifestMemo = null
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} drivePath
|
||
* @param {string | Uint8Array} content
|
||
*/
|
||
async function bootManifestDigestOk(ctx, drivePath, content) {
|
||
const m = await loadBootManifest(ctx)
|
||
const sha = m && typeof m === 'object' ? m.sha256 : null
|
||
if (!sha || typeof sha !== 'object') return true
|
||
const exp = /** @type {Record<string, string>} */ (sha)[drivePath]
|
||
if (exp == null || exp === '') return true
|
||
if (typeof ctx.bareOsBootFileSha256Hex !== 'function') {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootManifestMissingHasher',
|
||
'[boot] manifest present but bareOsBootFileSha256Hex missing'
|
||
)
|
||
return false
|
||
}
|
||
const buf =
|
||
typeof content === 'string' ? ctx.b4a.from(content, 'utf8') : content
|
||
const hex = ctx.bareOsBootFileSha256Hex(buf)
|
||
if (hex !== String(exp).trim().toLowerCase()) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootManifestSha256Mismatch',
|
||
'[boot] manifest sha256 mismatch: ' + drivePath
|
||
)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} text
|
||
* @returns {Promise<boolean>} false if BARE_OS_BOOT_STRICT and a line threw
|
||
*/
|
||
async function runRcLines(ctx, text) {
|
||
const { execLine } = ctx
|
||
const strict = bootStrict(ctx)
|
||
const allow = await loadBootAllowSet(ctx)
|
||
const dry = bootDryRun(ctx)
|
||
for (const line of text.split(/\r?\n/)) {
|
||
const t = line.trim()
|
||
if (!t || t.startsWith('#')) continue
|
||
if (allow && !bootLineAllowed(t, allow)) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootAllowDenied',
|
||
'[boot] command not in boot.allow: ' + t.slice(0, 120)
|
||
)
|
||
if (strict) {
|
||
if (typeof ctx.requestBooterExit === 'function')
|
||
ctx.requestBooterExit(1)
|
||
return false
|
||
}
|
||
continue
|
||
}
|
||
if (dry) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootDryRunExecLine',
|
||
'[boot-dry-run] skip execLine: ' + t.slice(0, 120)
|
||
)
|
||
continue
|
||
}
|
||
try {
|
||
await execLine(t)
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'execLineThrown',
|
||
(e && e.message) || String(e)
|
||
)
|
||
if (strict) {
|
||
if (typeof ctx.requestBooterExit === 'function')
|
||
ctx.requestBooterExit(1)
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
async function printOsRelease(ctx) {
|
||
const { drive, b4a } = ctx
|
||
try {
|
||
const rel = await drive.get('/etc/os-release')
|
||
if (rel) bootGuestOutLine(ctx, 'os-release', b4a.toString(rel))
|
||
} catch (e) {
|
||
const c = ctx.console
|
||
if (c && typeof c.error === 'function')
|
||
c.error((e && e.message) || String(e))
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
async function printMotd(ctx) {
|
||
const { drive, b4a } = ctx
|
||
try {
|
||
const motd = await drive.get('/etc/motd')
|
||
if (motd) bootGuestOutLine(ctx, 'motd', b4a.toString(motd).trimEnd())
|
||
} catch (e) {
|
||
const c = ctx.console
|
||
if (c && typeof c.error === 'function')
|
||
c.error((e && e.message) || String(e))
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Boot profile name: BARE_OS_BOOT_PROFILE wins over first line of /etc/bare-os/profile.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<string>}
|
||
*/
|
||
async function resolveBootProfileName(ctx) {
|
||
const fromEnv = ctx.env && ctx.env.BARE_OS_BOOT_PROFILE
|
||
if (fromEnv != null && String(fromEnv).trim()) return String(fromEnv).trim()
|
||
const { drive, b4a } = ctx
|
||
try {
|
||
const buf = await drive.get('/etc/bare-os/profile')
|
||
if (!buf) return ''
|
||
const line = b4a.toString(buf).split(/\r?\n/)[0] || ''
|
||
return line.trim()
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Optional trusted snippet /etc/bare-os/rc.profile.<name> (before /etc/bare-os/rc).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} profileName
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function runProfileRc(ctx, profileName) {
|
||
if (!profileName) return true
|
||
const safe = profileName.replace(/[^a-zA-Z0-9._-]/g, '')
|
||
if (safe !== profileName) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootProfileInvalidChars',
|
||
'[boot] profile name contains unsupported characters; skipping rc.profile'
|
||
)
|
||
return true
|
||
}
|
||
return await runRcFileAt(
|
||
ctx,
|
||
`/etc/bare-os/rc.profile.${safe}`,
|
||
`rc.profile.${safe}`
|
||
)
|
||
}
|
||
|
||
/**
|
||
* When stdin is non-interactive, run trusted boot commands (automation).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function runOnboot(ctx) {
|
||
if (!ctx.bareOsSkipRepl) return true
|
||
const { execLine, drive, b4a, env } = ctx
|
||
const strict = bootStrict(ctx)
|
||
const allow = await loadBootAllowSet(ctx)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const fromEnv = env && env.BARE_OS_ONBOOT
|
||
if (fromEnv != null && String(fromEnv).trim()) {
|
||
for (const raw of String(fromEnv).split(/\r?\n/)) {
|
||
const t = raw.trim()
|
||
if (!t || t.startsWith('#')) continue
|
||
lines.push(t)
|
||
}
|
||
} else {
|
||
try {
|
||
const buf = await drive.get('/etc/bare-os/onboot')
|
||
if (buf) {
|
||
for (const raw of b4a.toString(buf).split(/\r?\n/)) {
|
||
const t = raw.trim()
|
||
if (!t || t.startsWith('#')) continue
|
||
lines.push(t)
|
||
}
|
||
}
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'onbootRead',
|
||
'onboot: ' + ((e && e.message) || String(e))
|
||
)
|
||
}
|
||
}
|
||
const dry = bootDryRun(ctx)
|
||
for (const line of lines) {
|
||
if (allow && !bootLineAllowed(line, allow)) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'onbootBootAllowDenied',
|
||
'[boot] onboot command not in boot.allow: ' + line.slice(0, 120)
|
||
)
|
||
if (strict) {
|
||
if (typeof ctx.requestBooterExit === 'function')
|
||
ctx.requestBooterExit(1)
|
||
return false
|
||
}
|
||
continue
|
||
}
|
||
if (dry) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootDryRunOnboot',
|
||
'[boot-dry-run] skip onboot: ' + line.slice(0, 120)
|
||
)
|
||
continue
|
||
}
|
||
try {
|
||
await execLine(line)
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'onbootExecLineThrown',
|
||
(e && e.message) || String(e)
|
||
)
|
||
if (strict) {
|
||
if (typeof ctx.requestBooterExit === 'function')
|
||
ctx.requestBooterExit(1)
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} drivePath absolute path on system drive
|
||
* @param {string} label for errors
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function runRcFileAt(ctx, drivePath, label) {
|
||
const { drive, b4a } = ctx
|
||
try {
|
||
const buf = await drive.get(drivePath)
|
||
if (!buf) return true
|
||
const text = b4a.toString(buf)
|
||
if (!(await bootManifestDigestOk(ctx, drivePath, text))) {
|
||
if (bootStrict(ctx)) {
|
||
if (typeof ctx.requestBooterExit === 'function')
|
||
ctx.requestBooterExit(1)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
return await runRcLines(ctx, text)
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rcFileRead',
|
||
`${label}: ` + ((e && e.message) || String(e))
|
||
)
|
||
return true
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Only run rc.d files whose name starts with a digit (e.g. `10-local`).
|
||
* Skips README*, *.md, dotfiles, and *~ so documentation is never exec'd as shell.
|
||
* @param {string} name basename from readdir
|
||
*/
|
||
function isBareOsRcSnippetFile(name) {
|
||
if (!name || name.startsWith('.') || name.endsWith('~')) return false
|
||
if (/^README(\.|$)/i.test(name)) return false
|
||
if (/\.md$/i.test(name)) return false
|
||
return /^[0-9]/.test(name)
|
||
}
|
||
|
||
/**
|
||
* Optional snippets under /etc/bare-os/kernel.d/ — same rules as rc.d; runs after rc.local.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function runBareOsKernelDir(ctx) {
|
||
const { drive, b4a } = ctx
|
||
try {
|
||
/** @type {string[]} */
|
||
const names = []
|
||
try {
|
||
for await (const n of drive.readdir('/etc/bare-os/kernel.d'))
|
||
names.push(n)
|
||
} catch {
|
||
return true
|
||
}
|
||
names.sort()
|
||
/** @type {string[]} */
|
||
const toRun = []
|
||
for (const name of names) {
|
||
if (!isBareOsRcSnippetFile(name)) continue
|
||
toRun.push(name)
|
||
}
|
||
const strictPol =
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === 'true'
|
||
const traceKd =
|
||
strictPol &&
|
||
(ctx.env?.BARE_OS_BOOT_RC_RESOLUTION_TRACE === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_RC_RESOLUTION_TRACE === 'true')
|
||
if (traceKd && ctx.vfs && typeof ctx.vfs.writeFile === 'function' && ctx.b4a) {
|
||
try {
|
||
const body =
|
||
JSON.stringify({
|
||
schema: 1,
|
||
atMs: Date.now(),
|
||
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''),
|
||
kernelDOrdered: toRun
|
||
}) + '\n'
|
||
await ctx.vfs.writeFile(
|
||
'/run/bare-os/kernel-d-resolution.json',
|
||
ctx.b4a.from(body)
|
||
)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
for (const name of toRun) {
|
||
const p = `/etc/bare-os/kernel.d/${name}`
|
||
try {
|
||
const buf = await drive.get(p)
|
||
if (!buf) continue
|
||
const txt = b4a.toString(buf)
|
||
if (!kernelSnippetEnvGuardsOk(ctx, txt)) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'kernel.d.envGuardSkip',
|
||
`[boot] kernel.d/${name}: ConditionEnvironment / AssertEnvironment not met; skip`
|
||
)
|
||
continue
|
||
}
|
||
const cont = await runRcLines(ctx, txt)
|
||
if (!cont) return false
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'kernel.d.exec',
|
||
`kernel.d/${name}: ` + ((e && e.message) || String(e))
|
||
)
|
||
}
|
||
}
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'kernel.d.outer',
|
||
(e && e.message) || String(e)
|
||
)
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {Record<string, unknown>} entry
|
||
*/
|
||
function bareOsAppendRcProposalAudit(ctx, entry) {
|
||
try {
|
||
const fn = ctx.bareOsAuditLogAppend
|
||
if (typeof fn === 'function') fn(entry)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* When **`BARE_OS_RC_PROPOSAL_MULTISIG_STRICT`**, every enabled proposal must cite enough
|
||
* **`pear.multisig.json`** signers (distinct keys). Cryptographic signature verification is host/seeder responsibility.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function verifyRcProposalsMultisigStrict(ctx) {
|
||
const strict = ctx.env?.BARE_OS_RC_PROPOSAL_MULTISIG_STRICT
|
||
if (strict !== '1' && strict !== 'true') return true
|
||
const { drive, b4a } = ctx
|
||
let pearBuf
|
||
try {
|
||
pearBuf = await drive.get('/etc/bare-os/pear.multisig.json')
|
||
} catch {
|
||
pearBuf = null
|
||
}
|
||
if (!pearBuf) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.proposal.multisig.pear_missing',
|
||
'[rc.proposals] BARE_OS_RC_PROPOSAL_MULTISIG_STRICT requires /etc/bare-os/pear.multisig.json'
|
||
)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'pear_multisig_missing',
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
/** @type {unknown} */
|
||
let pearParsed
|
||
try {
|
||
pearParsed = JSON.parse(b4a.toString(pearBuf))
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.proposal.multisig.pear_parse',
|
||
'[rc.proposals] pear.multisig.json: ' + ((e && e.message) || String(e))
|
||
)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'pear_multisig_parse',
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
if (!bareOsPearMultisigShapeOk(pearParsed)) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.proposal.multisig.pear_shape',
|
||
'[rc.proposals] pear.multisig.json must be { signers: string[], quorum: number } with 1 ≤ quorum ≤ signers.length'
|
||
)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'pear_multisig_shape',
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
const pear = /** @type {{ signers: string[]; quorum: number }} */ (pearParsed)
|
||
const signerSet = new Set(pear.signers.map((s) => String(s).toLowerCase()))
|
||
const quorum = pear.quorum
|
||
|
||
/** @type {string[]} */
|
||
const proposalNames = []
|
||
try {
|
||
for await (const n of drive.readdir('/etc/bare-os/rc.proposals/enabled')) {
|
||
if (String(n).endsWith('.json')) proposalNames.push(String(n))
|
||
}
|
||
} catch {
|
||
return true
|
||
}
|
||
proposalNames.sort()
|
||
if (!proposalNames.length) return true
|
||
|
||
for (const fn of proposalNames) {
|
||
const p = `/etc/bare-os/rc.proposals/enabled/${fn}`
|
||
/** @type {unknown} */
|
||
let raw
|
||
try {
|
||
const buf = await drive.get(p)
|
||
if (!buf) {
|
||
bootStructuredLog(ctx, 'error', 'rc.proposal.missing', `[rc.proposals] empty: ${fn}`)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'proposal_missing',
|
||
proposalFile: fn,
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
raw = JSON.parse(b4a.toString(buf))
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.proposal.parse',
|
||
`[rc.proposals] ${fn}: ` + ((e && e.message) || String(e))
|
||
)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'proposal_parse',
|
||
proposalFile: fn,
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
||
bootStructuredLog(ctx, 'error', 'rc.proposal.shape', `[rc.proposals] ${fn}: expected object`)
|
||
return false
|
||
}
|
||
const o = /** @type {Record<string, unknown>} */ (raw)
|
||
if (o.schema !== 1) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.proposal.schema',
|
||
`[rc.proposals] ${fn}: schema must be 1`
|
||
)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'proposal_schema',
|
||
proposalFile: fn,
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
const proposalId = String(o.proposalId || '').trim()
|
||
if (!proposalId) {
|
||
bootStructuredLog(ctx, 'error', 'rc.proposal.id', `[rc.proposals] ${fn}: proposalId required`)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'proposal_id',
|
||
proposalFile: fn,
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
const targetRcSnippet = String(o.targetRcSnippet || '').trim()
|
||
if (!targetRcSnippet.startsWith('/etc/bare-os/rc.d/')) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.proposal.target',
|
||
`[rc.proposals] ${fn}: targetRcSnippet must start with /etc/bare-os/rc.d/`
|
||
)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'proposal_target',
|
||
proposalFile: fn,
|
||
proposalId,
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
const sigFrom = o.signaturesFrom
|
||
if (!Array.isArray(sigFrom)) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.proposal.signaturesFrom',
|
||
`[rc.proposals] ${fn}: signaturesFrom must be an array of signer public keys`
|
||
)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'signatures_from_shape',
|
||
proposalFile: fn,
|
||
proposalId,
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
/** @type {Set<string>} */
|
||
const approved = new Set()
|
||
for (const s of sigFrom) {
|
||
const k = String(s || '').toLowerCase().trim()
|
||
if (!k) continue
|
||
if (!signerSet.has(k)) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.proposal.signer_unknown',
|
||
`[rc.proposals] ${fn}: signer not in pear.multisig.json`
|
||
)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'signer_not_in_policy',
|
||
proposalFile: fn,
|
||
proposalId,
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
approved.add(k)
|
||
}
|
||
if (approved.size < quorum) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.proposal.quorum',
|
||
`[rc.proposals] ${fn}: need ≥ pear.multisig quorum (${quorum}) distinct approved signers, got ${approved.size}`
|
||
)
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_fail',
|
||
reason: 'quorum_not_met',
|
||
proposalFile: fn,
|
||
proposalId,
|
||
ts: Date.now()
|
||
})
|
||
return false
|
||
}
|
||
bareOsAppendRcProposalAudit(ctx, {
|
||
type: 'rc.proposal.multisig_ok',
|
||
proposalFile: fn,
|
||
proposalId,
|
||
targetRcSnippet,
|
||
signerCount: approved.size,
|
||
quorum,
|
||
ts: Date.now()
|
||
})
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* Optional snippets under /etc/bare-os/rc.d/ — executed in lexicographic order.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function runBareOsRcDir(ctx) {
|
||
const { drive, b4a } = ctx
|
||
if (!(await verifyRcProposalsMultisigStrict(ctx))) return false
|
||
const skip = parseRcDSkipPatterns(ctx)
|
||
try {
|
||
/** @type {string[]} */
|
||
const names = []
|
||
try {
|
||
for await (const n of drive.readdir('/etc/bare-os/rc.d')) names.push(n)
|
||
} catch {
|
||
return true
|
||
}
|
||
names.sort()
|
||
/** @type {string[]} */
|
||
const toRun = []
|
||
for (const name of names) {
|
||
if (!isBareOsRcSnippetFile(name)) continue
|
||
if (shouldSkipRcDName(name, skip)) continue
|
||
toRun.push(name)
|
||
}
|
||
const strictPol =
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === 'true'
|
||
const traceRc =
|
||
strictPol &&
|
||
(ctx.env?.BARE_OS_BOOT_RC_RESOLUTION_TRACE === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_RC_RESOLUTION_TRACE === 'true')
|
||
if (traceRc && ctx.vfs && typeof ctx.vfs.writeFile === 'function' && ctx.b4a) {
|
||
try {
|
||
const body =
|
||
JSON.stringify({
|
||
schema: 1,
|
||
atMs: Date.now(),
|
||
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''),
|
||
rcDOrdered: toRun
|
||
}) + '\n'
|
||
await ctx.vfs.writeFile(
|
||
'/run/bare-os/rc-d-resolution.json',
|
||
ctx.b4a.from(body)
|
||
)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
for (const name of toRun) {
|
||
const p = `/etc/bare-os/rc.d/${name}`
|
||
try {
|
||
const buf = await drive.get(p)
|
||
if (!buf) continue
|
||
const cont = await runRcLines(ctx, b4a.toString(buf))
|
||
if (!cont) return false
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.d.exec',
|
||
`rc.d/${name}: ` + ((e && e.message) || String(e))
|
||
)
|
||
}
|
||
}
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'rc.d.outer',
|
||
(e && e.message) || String(e)
|
||
)
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
async function printSessionBanner(ctx) {
|
||
const { drive, b4a } = ctx
|
||
for (const p of ['/etc/bare-os/banner', '/etc/issue']) {
|
||
try {
|
||
const buf = await drive.get(p)
|
||
if (buf) {
|
||
bootGuestOutLine(ctx, 'banner', b4a.toString(buf).trimEnd())
|
||
return
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
const defaultBanner =
|
||
'Bare operating system — guest session (run login or login --new, then passphrase at prompt)'
|
||
if (ctx.bareOsSkipRepl) {
|
||
bootGuestOutLine(
|
||
ctx,
|
||
'banner',
|
||
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
|
||
)
|
||
return
|
||
}
|
||
bootGuestOutLine(ctx, 'banner', defaultBanner)
|
||
}
|
||
|
||
/**
|
||
* Trusted post-boot checks. **`BARE_OS_SELFTEST_FORMAT=tap`** and **`junit`** emit diagnostics on
|
||
* **`ctx.console.error`** (this function destructures **`ctx.console`** as **`console`**) so output
|
||
* follows the Test Anything Protocol’s stderr convention while staying on the booter session sink—
|
||
* not **`globalThis.console`**. Plain **`selftest:`** lines use the same sink.
|
||
*
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function runKernelSelftest(ctx) {
|
||
const v = ctx.env && ctx.env.BARE_OS_KERNEL_SELFTEST
|
||
if (v !== '1' && v !== 'true' && v !== 'upgrade') return true
|
||
const { execLine, console } = ctx
|
||
const strict = bootStrict(ctx)
|
||
const fmt = (ctx.env && ctx.env.BARE_OS_SELFTEST_FORMAT) || ''
|
||
const fmtLow = String(fmt).toLowerCase()
|
||
const tap = fmtLow === 'tap'
|
||
const junit = fmtLow === 'junit'
|
||
let tapN = 0
|
||
/** @type {{ ok: boolean, name: string, detail: string }[]} */
|
||
const junitCases = []
|
||
/** @param {string} s */
|
||
function escXml(s) {
|
||
return String(s || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/"/g, '"')
|
||
}
|
||
/** @param {string} name */
|
||
function tapLine(ok, name, detail) {
|
||
if (junit) {
|
||
junitCases.push({ ok, name, detail: detail || '' })
|
||
return
|
||
}
|
||
if (!tap) return
|
||
tapN++
|
||
if (ok) {
|
||
console.error(`ok ${tapN} ${name}`)
|
||
} else {
|
||
console.error(`not ok ${tapN} ${name}${detail ? ' — ' + detail : ''}`)
|
||
}
|
||
}
|
||
if (tap) {
|
||
console.error('TAP version 13')
|
||
}
|
||
/** @type {[string, string][]} */
|
||
const specs =
|
||
v === 'upgrade'
|
||
? [
|
||
['true', 'upgrade-smoke-true'],
|
||
[
|
||
'test -f /proc/bare_os/kernel_program.json && echo selftest_kernel_program',
|
||
'proc-kernel-program-upgrade'
|
||
],
|
||
[
|
||
'test -f /run/bare-os/boot.json && echo selftest_boot_json',
|
||
'run-boot-json-upgrade'
|
||
]
|
||
]
|
||
: [
|
||
[':', 'colon-builtin'],
|
||
['true', 'true-builtin'],
|
||
['false; echo selftest_false_ok', 'false-sequencing'],
|
||
['test -f /proc/version && echo selftest_proc', 'proc-version'],
|
||
[
|
||
'test -f /proc/bare_os_resources && echo selftest_resources',
|
||
'proc-bare_os_resources'
|
||
],
|
||
[
|
||
'test -f /proc/bare_os_features && echo selftest_features',
|
||
'proc-bare_os_features'
|
||
],
|
||
[
|
||
'test -f /proc/bare_os/index.json && echo selftest_bare_os_dir',
|
||
'proc-bare_os_dir'
|
||
],
|
||
['test -d /proc/self/fd && echo selftest_proc_fd', 'proc-self-fd'],
|
||
['test -d /sys/devices && echo selftest_sys_devices', 'sys-devices'],
|
||
['echo selftest_vfs | tee /dev/null', 'pipeline-tee-devnull'],
|
||
[
|
||
'test -f /lib/bare/manifest.json && echo selftest_bare_manifest',
|
||
'lib-bare-manifest'
|
||
],
|
||
[
|
||
'test -f /proc/bare_os/provenance && echo selftest_provenance',
|
||
'proc-bare_os_provenance'
|
||
],
|
||
[
|
||
'test -f /proc/bare_os/metrics_live.json && echo selftest_metrics_live',
|
||
'proc-metrics-live'
|
||
],
|
||
[
|
||
'test -f /proc/bare_os/net_summary.json && echo selftest_net_summary',
|
||
'proc-net-summary'
|
||
],
|
||
[
|
||
'test -f /proc/bare_os/host_os.json && echo selftest_host_os',
|
||
'proc-host-os'
|
||
],
|
||
[
|
||
'test -f /proc/bare_os/sync_window.json && echo selftest_sync_window',
|
||
'proc-sync-window'
|
||
],
|
||
[
|
||
'test -f /proc/bare_os/manifest_hints && echo selftest_manifest_hints',
|
||
'proc-manifest-hints'
|
||
],
|
||
['systemctl list-units 2>/dev/null || true', 'systemctl-list']
|
||
]
|
||
for (const [line, name] of specs) {
|
||
try {
|
||
await execLine(line)
|
||
tapLine(true, name, '')
|
||
} catch (e) {
|
||
const msg = (e && e.message) || String(e)
|
||
console.error('selftest: ' + msg)
|
||
tapLine(false, name, msg)
|
||
if (strict) {
|
||
if (typeof ctx.requestBooterExit === 'function')
|
||
ctx.requestBooterExit(1)
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
try {
|
||
/** @type {string[]} */
|
||
const snames = []
|
||
for await (const n of ctx.drive.readdir('/etc/bare-os/selftest.d'))
|
||
snames.push(n)
|
||
snames.sort()
|
||
for (const name of snames) {
|
||
if (!isBareOsRcSnippetFile(name)) continue
|
||
const p = `/etc/bare-os/selftest.d/${name}`
|
||
const buf = await ctx.drive.get(p)
|
||
if (!buf) continue
|
||
const label = `selftest.d/${name}`
|
||
try {
|
||
await runRcLines(ctx, ctx.b4a.toString(buf))
|
||
tapLine(true, label, '')
|
||
} catch (e) {
|
||
const msg = (e && e.message) || String(e)
|
||
console.error('selftest: ' + msg)
|
||
tapLine(false, label, msg)
|
||
if (strict) {
|
||
if (typeof ctx.requestBooterExit === 'function')
|
||
ctx.requestBooterExit(1)
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
/* no selftest.d */
|
||
}
|
||
if (tap) {
|
||
console.error(`1..${tapN}`)
|
||
}
|
||
if (junit && junitCases.length) {
|
||
const failures = junitCases.filter((c) => !c.ok).length
|
||
const cases = junitCases
|
||
.map((c) => {
|
||
const body = c.ok
|
||
? ''
|
||
: `<failure message="selftest">${escXml(c.detail)}</failure>`
|
||
return `<testcase name="${escXml(c.name)}" classname="kernel.selftest">${body}</testcase>`
|
||
})
|
||
.join('')
|
||
console.error(
|
||
`<?xml version="1.0" encoding="UTF-8"?><testsuite name="bare-os-kernel-selftest" tests="${junitCases.length}" failures="${failures}">${cases}</testsuite>`
|
||
)
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {number} bootT0
|
||
* @param {{ kind: string, code: string, wallMs: number, limitMs: number }[]} violations
|
||
* @param {boolean} coldExceeded
|
||
* @param {boolean} stdlibExceeded
|
||
* @param {string[]} [stageLog]
|
||
*/
|
||
async function maybeWriteBootBudgetSummaryJson(
|
||
ctx,
|
||
bootT0,
|
||
violations,
|
||
coldExceeded,
|
||
stdlibExceeded,
|
||
stageLog
|
||
) {
|
||
const vfs = ctx.vfs
|
||
const b4 = ctx.b4a
|
||
if (!vfs || typeof vfs.writeFile !== 'function' || !b4) return
|
||
const wall = Date.now() - bootT0
|
||
const row =
|
||
JSON.stringify({
|
||
schema: 2,
|
||
atMs: Date.now(),
|
||
coldWallMs: wall,
|
||
coldExceeded,
|
||
bareStdlibExceeded: stdlibExceeded,
|
||
violationCodes: violations.map((v) => v.code).filter(Boolean),
|
||
violations,
|
||
bootStageCount:
|
||
Array.isArray(stageLog) && stageLog.length ? stageLog.length : null,
|
||
bootStageTail:
|
||
Array.isArray(stageLog) && stageLog.length
|
||
? stageLog.slice(-16)
|
||
: undefined,
|
||
procHint: '/proc/bare_os/boot_budget_summary.json',
|
||
note: 'Written every boot; operators mirror into proc via booter VFS provider. Schema 2 adds bootStageCount/bootStageTail from kernel stage log.'
|
||
}) + '\n'
|
||
try {
|
||
await vfs.writeFile('/run/bare-os/boot-budget-summary.json', b4.from(row))
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
async function maybeWriteBootPerfJson(ctx, bootT0, stageLog) {
|
||
const vfs = ctx.vfs
|
||
const b4 = ctx.b4a
|
||
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function' || !b4)
|
||
return
|
||
const wall = Date.now() - bootT0
|
||
const budgetRaw = Number.parseInt(
|
||
String(ctx.env?.BARE_OS_BOOT_BUDGET_MS_COLD || ''),
|
||
10
|
||
)
|
||
const budget = Number.isFinite(budgetRaw) && budgetRaw > 0 ? budgetRaw : null
|
||
const stdlibWallRaw = Number.parseInt(
|
||
String(ctx.env?.BARE_OS_BOOT_BARE_STDLIB_RESOLUTION_MS || ''),
|
||
10
|
||
)
|
||
const stdlibWall =
|
||
Number.isFinite(stdlibWallRaw) && stdlibWallRaw >= 0 ? stdlibWallRaw : null
|
||
const stdlibBudgetRaw = Number.parseInt(
|
||
String(ctx.env?.BARE_OS_BOOT_BUDGET_MS_BARE_STDLIB || ''),
|
||
10
|
||
)
|
||
const stdlibBudget =
|
||
Number.isFinite(stdlibBudgetRaw) && stdlibBudgetRaw > 0
|
||
? stdlibBudgetRaw
|
||
: null
|
||
const stages =
|
||
Array.isArray(ctx.bareOsBootStageTimings) && ctx.bareOsBootStageTimings.length
|
||
? ctx.bareOsBootStageTimings
|
||
: undefined
|
||
let schema = stages ? 2 : 1
|
||
if (stdlibWall != null || stdlibBudget != null) schema = Math.max(schema, 3)
|
||
schema = Math.max(schema, 4)
|
||
schema = Math.max(schema, 5)
|
||
const bootBudgetTelemetry = {
|
||
schema: 1,
|
||
coldWallMs: wall,
|
||
coldBudgetMs: budget,
|
||
coldWithinBudget: budget == null ? null : wall <= budget,
|
||
stdlibWallMs: stdlibWall,
|
||
stdlibBudgetMs: stdlibBudget,
|
||
stdlibWithinBudget:
|
||
stdlibBudget == null || stdlibWall == null
|
||
? null
|
||
: stdlibWall <= stdlibBudget,
|
||
envCold: 'BARE_OS_BOOT_BUDGET_MS_COLD',
|
||
envStdlibWall: 'BARE_OS_BOOT_BARE_STDLIB_RESOLUTION_MS',
|
||
envStdlibBudget: 'BARE_OS_BOOT_BUDGET_MS_BARE_STDLIB',
|
||
note: 'Unified mirror of guest cold boot vs bare-stdlib resolution budgets; same values as top-level coldWallMs / bareStdlib* (booter sets BARE_OS_BOOT_BARE_STDLIB_RESOLUTION_MS).'
|
||
}
|
||
const row =
|
||
JSON.stringify({
|
||
schema,
|
||
bootGraphJsonPath: '/proc/bare_os/boot_graph.json',
|
||
stageLogSnapshot: [...stageLog],
|
||
coldWallMs: wall,
|
||
bootBudgetMsCold: budget,
|
||
withinBudget: budget == null ? null : wall <= budget,
|
||
bootBudgetWarning:
|
||
budget != null && wall > budget
|
||
? `cold boot ${wall}ms exceeded BARE_OS_BOOT_BUDGET_MS_COLD ${budget}ms`
|
||
: null,
|
||
bareStdlibResolutionWallMs: stdlibWall,
|
||
bareStdlibBudgetMs: stdlibBudget,
|
||
bareStdlibWithinBudget:
|
||
stdlibBudget == null || stdlibWall == null
|
||
? null
|
||
: stdlibWall <= stdlibBudget,
|
||
bareStdlibBudgetWarning:
|
||
stdlibBudget != null &&
|
||
stdlibWall != null &&
|
||
stdlibWall > stdlibBudget
|
||
? `bare stdlib resolution ${stdlibWall}ms exceeded BARE_OS_BOOT_BUDGET_MS_BARE_STDLIB ${stdlibBudget}ms`
|
||
: null,
|
||
bootBudgetTelemetry,
|
||
bootBudgetStrict:
|
||
ctx.env?.BARE_OS_BOOT_BUDGET_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_BUDGET_STRICT === 'true',
|
||
bootPolicyStrict:
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === 'true',
|
||
stageCount: stageLog.length,
|
||
stages,
|
||
completedAtMs: Date.now()
|
||
}) + '\n'
|
||
try {
|
||
await vfs.writeFile('/run/bare-os/boot-perf.json', b4.from(row))
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string[]} stageLog
|
||
*/
|
||
function publishBootReady(ctx, stageLog) {
|
||
if (typeof ctx.bareOsPublishBootReady !== 'function') return
|
||
const sid = (ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''
|
||
const gpen = ctx.env && ctx.env.BARE_OS_BOOT_TRANSACTION_JOURNAL
|
||
const safe =
|
||
ctx.env?.BARE_OS_BOOT_SAFE_MODE === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_SAFE_MODE === 'true'
|
||
const bootStages = stageLog.map((label) => ({
|
||
label: String(label),
|
||
bootStage: bootStageForPhase(String(label))
|
||
}))
|
||
ctx.bareOsPublishBootReady({
|
||
ready: true,
|
||
stages: [...stageLog],
|
||
phases: [...stageLog],
|
||
steps: [...stageLog],
|
||
bootSteps: [...stageLog],
|
||
sessionId: sid,
|
||
completedAtMs: Date.now(),
|
||
minimal: bootMinimal(ctx),
|
||
subsystems: {
|
||
kernel: {
|
||
ready: true,
|
||
stageCount: stageLog.length,
|
||
phaseCount: stageLog.length,
|
||
stepCount: stageLog.length,
|
||
bootStages,
|
||
bootPhases: bootStages,
|
||
bootSteps: bootStages,
|
||
programProc: {
|
||
schema: 2,
|
||
programVersion: 2,
|
||
safeMode: safe,
|
||
bootDryRun: bootDryRun(ctx),
|
||
transactionJournal: gpen === '1' || gpen === 'true' || gpen === 'ndjson',
|
||
checkpoint:
|
||
ctx.env?.BARE_OS_BOOT_CHECKPOINT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_CHECKPOINT === 'true',
|
||
bootTransactionFsm: {
|
||
schema: 1,
|
||
state: 'committed',
|
||
note: 'Guest kernel finished boot stage sequence; see boot-transaction.ndjson for per-stage rows.'
|
||
},
|
||
kernelExtHotReload: kernelExtHotReloadEnabled(ctx)
|
||
}
|
||
},
|
||
initd: { awaited: true }
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Merge sketch for seed vs advertised capability words under strict policy (expand over time).
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function mergeBootCapabilityContract(ctx) {
|
||
const strict =
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === 'true'
|
||
const adv = ctx.bareOsAdvertisedKernelCapabilityWords
|
||
const seed = ctx.bareOsSeedKernelCapabilityWords
|
||
if (
|
||
strict &&
|
||
adv &&
|
||
seed &&
|
||
typeof adv === 'object' &&
|
||
typeof seed === 'object'
|
||
) {
|
||
const ap = /** @type {{ primary?: number }} */ (adv).primary
|
||
const sp = /** @type {{ primary?: number }} */ (seed).primary
|
||
if (typeof ap === 'number' && typeof sp === 'number' && ap !== sp) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootCapabilityPrimaryMismatch',
|
||
'[boot] strict: primary kernel capability word mismatch (advertised vs seed)'
|
||
)
|
||
if (typeof ctx.requestBooterExit === 'function') ctx.requestBooterExit(1)
|
||
return false
|
||
}
|
||
}
|
||
const dbg =
|
||
ctx.env?.BARE_OS_BOOT_CAPABILITY_CONTRACT_DEBUG === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_CAPABILITY_CONTRACT_DEBUG === 'true'
|
||
if (dbg) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootCapabilityContractDebug',
|
||
'[boot] capability contract debug: advertised=' +
|
||
(adv ? 'yes' : 'no') +
|
||
' seed=' +
|
||
(seed ? 'yes' : 'no')
|
||
)
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* Boot ABI gate: optional `BARE_OS_REQUIRE_CTX_API_MIN` vs `ctx.bareOsCtxApiVersion`.
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function enforceBootCtxApiMin(ctx) {
|
||
const need = String(ctx.env?.BARE_OS_REQUIRE_CTX_API_MIN || '').trim()
|
||
if (!need) return true
|
||
const have =
|
||
typeof ctx.bareOsCtxApiVersion === 'string'
|
||
? ctx.bareOsCtxApiVersion.trim()
|
||
: ''
|
||
if (!have || !semverGte(have, need)) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootCtxApiMin',
|
||
'[boot] BARE_OS_REQUIRE_CTX_API_MIN not satisfied (need ' +
|
||
need +
|
||
', have ' +
|
||
(have || '(none)') +
|
||
')'
|
||
)
|
||
if (
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === 'true' ||
|
||
ctx.env?.BARE_OS_BOOT_ABI_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_ABI_STRICT === 'true'
|
||
) {
|
||
if (typeof ctx.requestBooterExit === 'function') ctx.requestBooterExit(1)
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
async function start(ctx) {
|
||
const bootT0 = Date.now()
|
||
const { readLine, execLine } = ctx
|
||
/** @type {string[]} */
|
||
const stageLog = []
|
||
const bootPolicySkipSet = new Set()
|
||
ctx.bareOsBootPolicySkipStages = bootPolicySkipSet
|
||
ctx.bareOsBootPolicySkipPhases = bootPolicySkipSet
|
||
{
|
||
const raw = String(ctx.env?.BARE_OS_KERNEL_STARTUP_CLASS || 'interactive')
|
||
.trim()
|
||
.toLowerCase()
|
||
const allowed = new Set(['critical', 'system', 'interactive', 'deferred'])
|
||
ctx.bareOsKernelStartupClass = allowed.has(raw)
|
||
? raw
|
||
: 'interactive'
|
||
}
|
||
if (!(await applyBootPolicyFile(ctx))) return
|
||
if (!enforceBootCtxApiMin(ctx)) return
|
||
if (!mergeBootCapabilityContract(ctx)) return
|
||
await maybeAppendBootTransactionJournal(ctx, {
|
||
phase: 'boot.policy',
|
||
stage: 'boot.policy',
|
||
bootStage: 'policy',
|
||
ms: 0,
|
||
ok: true,
|
||
transactionState: BARE_OS_BOOT_TXN_STATE.STAGE_COMMITTED
|
||
})
|
||
await maybeWriteBootCheckpoint(ctx, 'boot.policy', 0)
|
||
applyBootSafeMode(ctx)
|
||
await maybeWriteBootSnapshotExport(ctx, bootPolicySkipSet)
|
||
await bootTimed(ctx, 'os-release', () => printOsRelease(ctx), stageLog)
|
||
await bootTimed(ctx, 'motd', () => printMotd(ctx), stageLog)
|
||
const profileName = await resolveBootProfileName(ctx)
|
||
/** @type {boolean} */
|
||
let bootOk = true
|
||
if (!shouldSkipBootStage(ctx, 'profile')) {
|
||
await bootTimed(
|
||
ctx,
|
||
'rc.profile',
|
||
async () => {
|
||
bootOk = await runProfileRc(ctx, profileName)
|
||
},
|
||
stageLog
|
||
)
|
||
} else {
|
||
stageLog.push('rc.profile(skipped)')
|
||
}
|
||
if (!bootOk) {
|
||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||
return
|
||
}
|
||
if (!shouldSkipBootStage(ctx, 'rc')) {
|
||
await bootTimed(
|
||
ctx,
|
||
'rc',
|
||
async () => {
|
||
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc')
|
||
},
|
||
stageLog
|
||
)
|
||
} else {
|
||
stageLog.push('rc(skipped)')
|
||
}
|
||
if (!bootOk) {
|
||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||
return
|
||
}
|
||
if (!shouldSkipBootStage(ctx, 'rc.d')) {
|
||
await bootTimed(
|
||
ctx,
|
||
'rc.d',
|
||
async () => {
|
||
bootOk = await runBareOsRcDir(ctx)
|
||
},
|
||
stageLog
|
||
)
|
||
} else {
|
||
stageLog.push('rc.d(skipped)')
|
||
}
|
||
if (!bootOk) {
|
||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||
return
|
||
}
|
||
if (!shouldSkipBootStage(ctx, 'rc.local')) {
|
||
await bootTimed(
|
||
ctx,
|
||
'rc.local',
|
||
async () => {
|
||
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc.local', 'rc.local')
|
||
},
|
||
stageLog
|
||
)
|
||
} else {
|
||
stageLog.push('rc.local(skipped)')
|
||
}
|
||
if (!bootOk) {
|
||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||
return
|
||
}
|
||
if (!shouldSkipBootStage(ctx, 'kernel.d')) {
|
||
await bootTimed(
|
||
ctx,
|
||
'kernel.d',
|
||
async () => {
|
||
bootOk = await runBareOsKernelDir(ctx)
|
||
},
|
||
stageLog
|
||
)
|
||
} else {
|
||
stageLog.push('kernel.d(skipped)')
|
||
}
|
||
if (!bootOk) {
|
||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||
return
|
||
}
|
||
if (!shouldSkipBootStage(ctx, 'kernel.ext.d')) {
|
||
await bootTimed(
|
||
ctx,
|
||
'kernel.ext.d',
|
||
async () => {
|
||
bootOk = await runKernelExtDropins(ctx)
|
||
},
|
||
stageLog
|
||
)
|
||
} else {
|
||
stageLog.push('kernel.ext.d(skipped)')
|
||
}
|
||
if (!bootOk) {
|
||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||
return
|
||
}
|
||
await bootTimed(
|
||
ctx,
|
||
'banner',
|
||
async () => {
|
||
await printSessionBanner(ctx)
|
||
},
|
||
stageLog
|
||
)
|
||
if (!shouldSkipBootStage(ctx, 'onboot')) {
|
||
await bootTimed(
|
||
ctx,
|
||
'onboot',
|
||
async () => {
|
||
bootOk = await runOnboot(ctx)
|
||
},
|
||
stageLog
|
||
)
|
||
} else {
|
||
stageLog.push('onboot(skipped)')
|
||
}
|
||
if (!bootOk) {
|
||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||
return
|
||
}
|
||
await bootTimed(
|
||
ctx,
|
||
'selftest',
|
||
async () => {
|
||
bootOk = await runKernelSelftest(ctx)
|
||
},
|
||
stageLog
|
||
)
|
||
if (!bootOk) {
|
||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||
return
|
||
}
|
||
publishBootReady(ctx, stageLog)
|
||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||
if (kernelExtHotReloadEnabled(ctx)) {
|
||
ctx.bareOsReloadKernelExtDropinsSafe = async () => {
|
||
if (!kernelExtHotReloadEnabled(ctx)) {
|
||
return { ok: false, reason: 'env_disabled', atMs: Date.now() }
|
||
}
|
||
/** @type {string[]} */
|
||
const ran = []
|
||
const ok = await runKernelExtDropins(ctx, {
|
||
incremental: true,
|
||
ranScripts: ran
|
||
})
|
||
await maybeAppendKernelExtReloadJournal(ctx, {
|
||
ok,
|
||
incremental: true,
|
||
ranScripts: ran,
|
||
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || '')
|
||
})
|
||
return { ok, ranScripts: ran, atMs: Date.now() }
|
||
}
|
||
}
|
||
{
|
||
/** @type {{ kind: string, code: string, wallMs: number, limitMs: number }[]} */
|
||
let bootBudgetViolationsForSummary = []
|
||
const budgetStrict =
|
||
ctx.env?.BARE_OS_BOOT_BUDGET_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_BUDGET_STRICT === 'true'
|
||
const polStrict =
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === '1' ||
|
||
ctx.env?.BARE_OS_BOOT_POLICY_STRICT === 'true'
|
||
let coldExceeded = false
|
||
let stdlibExceeded = false
|
||
const budget = Number.parseInt(
|
||
String(ctx.env?.BARE_OS_BOOT_BUDGET_MS_COLD || ''),
|
||
10
|
||
)
|
||
if (Number.isFinite(budget) && budget > 0) {
|
||
const wall = Date.now() - bootT0
|
||
if (wall > budget) {
|
||
coldExceeded = true
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootBudgetColdExceeded',
|
||
`[boot] cold wall ${wall}ms exceeds BARE_OS_BOOT_BUDGET_MS_COLD=${budget}ms`
|
||
)
|
||
if (ctx.env) {
|
||
ctx.env.BARE_OS_BOOT_BUDGET_COLD_EXCEEDED = '1'
|
||
ctx.env.BARE_OS_BOOT_BUDGET_COLD_WALL_MS = String(wall)
|
||
ctx.env.BARE_OS_BOOT_BUDGET_COLD_LIMIT_MS = String(budget)
|
||
}
|
||
}
|
||
}
|
||
const sb = Number.parseInt(
|
||
String(ctx.env?.BARE_OS_BOOT_BUDGET_MS_BARE_STDLIB || ''),
|
||
10
|
||
)
|
||
if (Number.isFinite(sb) && sb > 0) {
|
||
const sw = Number.parseInt(
|
||
String(ctx.env?.BARE_OS_BOOT_BARE_STDLIB_RESOLUTION_MS || ''),
|
||
10
|
||
)
|
||
if (Number.isFinite(sw) && sw > sb) {
|
||
stdlibExceeded = true
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootBudgetBareStdlibExceeded',
|
||
`[boot] bare stdlib resolution ${sw}ms exceeds BARE_OS_BOOT_BUDGET_MS_BARE_STDLIB=${sb}ms`
|
||
)
|
||
if (ctx.env) {
|
||
ctx.env.BARE_OS_BOOT_BUDGET_STDLIB_EXCEEDED = '1'
|
||
ctx.env.BARE_OS_BOOT_BUDGET_STDLIB_WALL_MS = String(sw)
|
||
ctx.env.BARE_OS_BOOT_BUDGET_STDLIB_LIMIT_MS = String(sb)
|
||
}
|
||
}
|
||
}
|
||
if (coldExceeded || stdlibExceeded) {
|
||
/** @type {{ kind: string, code: string, wallMs: number, limitMs: number }[]} */
|
||
const violations = []
|
||
if (coldExceeded && Number.isFinite(budget) && budget > 0) {
|
||
violations.push({
|
||
kind: 'cold',
|
||
code: 'BARE_OS_BOOT_BUDGET_COLD_EXCEEDED',
|
||
wallMs: Date.now() - bootT0,
|
||
limitMs: budget
|
||
})
|
||
}
|
||
if (stdlibExceeded && Number.isFinite(sb) && sb > 0) {
|
||
const sw = Number.parseInt(
|
||
String(ctx.env?.BARE_OS_BOOT_BARE_STDLIB_RESOLUTION_MS || ''),
|
||
10
|
||
)
|
||
violations.push({
|
||
kind: 'bare_stdlib',
|
||
code: 'BARE_OS_BOOT_BUDGET_BARE_STDLIB_EXCEEDED',
|
||
wallMs: Number.isFinite(sw) ? sw : 0,
|
||
limitMs: sb
|
||
})
|
||
}
|
||
await maybeAppendBootTransactionJournal(ctx, {
|
||
phase: 'boot.budget',
|
||
stage: 'boot.budget',
|
||
bootStage: 'budget',
|
||
ms: 0,
|
||
ok: false,
|
||
bootBudgetViolation: true,
|
||
bootBudgetSchemaVersion: 2,
|
||
coldBudgetExceeded: coldExceeded,
|
||
bareStdlibBudgetExceeded: stdlibExceeded,
|
||
bootBudgetViolations: violations,
|
||
transactionState: BARE_OS_BOOT_TXN_STATE.STAGE_COMMITTED
|
||
})
|
||
bootBudgetViolationsForSummary = violations
|
||
}
|
||
await maybeWriteBootBudgetSummaryJson(
|
||
ctx,
|
||
bootT0,
|
||
bootBudgetViolationsForSummary,
|
||
coldExceeded,
|
||
stdlibExceeded,
|
||
stageLog
|
||
)
|
||
if (budgetStrict && polStrict && (coldExceeded || stdlibExceeded)) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'bootBudgetStrictAbort',
|
||
'[boot] BARE_OS_BOOT_BUDGET_STRICT with BARE_OS_BOOT_POLICY_STRICT: exiting after budget violation'
|
||
)
|
||
if (typeof ctx.bareOsRequestBooterExit === 'function')
|
||
ctx.bareOsRequestBooterExit(1)
|
||
return
|
||
}
|
||
}
|
||
while (true) {
|
||
const line = await readLine('')
|
||
if (line == null) break
|
||
const t = line.trim()
|
||
if (t === '') continue
|
||
let status = 'ok'
|
||
try {
|
||
status = await execLine(t)
|
||
} catch (e) {
|
||
bootStructuredLog(
|
||
ctx,
|
||
'error',
|
||
'replExecLineThrown',
|
||
(e && e.message) || String(e)
|
||
)
|
||
}
|
||
if (status === 'exit') break
|
||
}
|
||
}
|