/** * 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} ctx * @param {Record} 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} ctx * @returns {Promise} 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} */ (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} merge * @param {Record} 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} 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} 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} */ (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} */ (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} */ const adj = new Map() /** @type {Map} */ 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} 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 | null | undefined} env * @returns {Record} */ 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} */ 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} ctx * @param {{ extId: string, file: string, signaturePointer?: string }} ent * @param {string} imgPath * @param {Record} pins * @param {boolean} strictPol * @returns {Promise} 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} ctx * @param {Record} 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} ctx * @param {{ incremental?: boolean, ranScripts?: string[] }} [opts] * @returns {Promise} */ 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} */ const loadedSet = /** @type {Set} */ (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} */ 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} */ 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. 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 `` 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} 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} ctx */ function isBootTraceJson(ctx) { return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'json' } /** * @param {Record} 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} 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} 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} 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} ctx */ function kernelExtHotReloadEnabled(ctx) { const v = ctx.env?.BARE_OS_KERNEL_EXT_D_HOT_RELOAD return v === '1' || v === 'true' } /** * @param {Record} ctx * @param {Record} 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} 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} ctx * @param {Set} 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} ctx * @returns {Set} */ 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} 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} 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} ctx * @returns {Promise | 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} 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} ctx * @param {string} label * @param {() => void | Promise} 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 | null | undefined} */ let bootManifestMemo /** * @param {Record} ctx * @returns {Promise | 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} 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} */ (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} ctx * @param {string} text * @returns {Promise} 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} 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} 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} ctx * @returns {Promise} */ 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. (before /etc/bare-os/rc). * @param {Record} ctx * @param {string} profileName * @returns {Promise} */ 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} ctx * @returns {Promise} */ 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} ctx * @param {string} drivePath absolute path on system drive * @param {string} label for errors * @returns {Promise} */ 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} ctx * @returns {Promise} */ 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} ctx * @param {Record} 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} ctx * @returns {Promise} */ 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} */ (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} */ 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} ctx * @returns {Promise} */ 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} 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} ctx * @returns {Promise} */ 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(//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 ? '' : `${escXml(c.detail)}` return `${body}` }) .join('') console.error( `${cases}` ) } return true } /** * @param {Record} 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} 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} 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} 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 } }