Guest isolation

This commit is contained in:
Raven Scott
2026-04-03 18:31:22 -04:00
parent b727177842
commit c02ffc6021
9 changed files with 232 additions and 34 deletions
@@ -12,7 +12,7 @@ export const INITD_LOG = `${BARE_OS_VAR_LOG_DIR}/initd.log`
const README_REL = `${BARE_OS_VAR_LOG_DIR}/README`
const README_TEXT = `Bare OS session logs (mirrored on your personal drive under /.bare-os/var/log).
const README_TEXT = `Bare OS session logs (mirrored on your personal drive under /.bare-os/var/log/<HOME-basename>/).
kernel-console.log — console.log / console.error from the kernel session
cron.log — bare-cron job errors
initd.log — bare-initd service start failures
+105 -1
View File
@@ -80,6 +80,7 @@ export async function applyUnlockedEnv(ctx, publicKey, secretKey) {
env.LOGNAME = name
env.HOME = home
env.PWD = home
await migrateLegacyPersonalHomeIfNeeded(ctx)
const uid = uidFromPublicKey(publicKey)
env.UID = uid
env.GID = uid
@@ -145,9 +146,10 @@ export async function ensureBareDir(ctx) {
*/
export async function ensureGuestHome(ctx) {
await ensureBareDir(ctx)
await migrateLegacyPersonalHomeIfNeeded(ctx)
const drive = ctx.personalDrive
if (!drive || typeof drive.put !== 'function') return
const keep = '/.keep_guest'
const keep = '/.bare-os/home/guest/.keep_guest'
try {
const existing = await drive.get(keep)
if (existing) return
@@ -217,6 +219,8 @@ const VAULT_EXCLUDE_PREFIXES = [
'bare',
'.bare/',
'.bare',
'.bare-os/var/',
'.bare-os/var',
'.vault/',
'.vault',
'bin/',
@@ -238,6 +242,106 @@ function shouldVaultSkip(short) {
return false
}
/**
* Top-level personal-drive names we do not lift from `/` into `/.bare-os/home/<seg>/`
* during legacy migration (machine metadata, new layout root, old guest marker).
* @param {string} name single path segment (no slashes)
*/
function legacyRootMigrateSkip(name) {
if (shouldVaultSkip(name)) return true
if (name === '.bare-os' || name === '.keep_guest') return true
return false
}
/**
* @param {import('hyperdrive').default} drive
* @param {string} prefix absolute e.g. /.bare-os/home/guest
*/
async function personalHomePrefixIsEmpty(drive, prefix) {
try {
const names = await driveReaddirNames(drive, prefix)
return names.length === 0
} catch {
return true
}
}
/**
* @param {import('hyperdrive').default} drive
* @param {string} fromAbs
* @param {string} toAbs
*/
async function moveDriveSubtree(drive, fromAbs, toAbs) {
const ent = await drive.entry(fromAbs, { follow: false })
if (!ent?.value) return
const v = ent.value
if (v.linkname != null) {
await drive.putEntry(toAbs, {
linkname: v.linkname,
executable: !!v.executable,
metadata: v.metadata ?? null
})
await drive.del(fromAbs)
return
}
if (v.blob) {
const data = await drive.get(fromAbs, { follow: true })
if (data) {
await drive.put(toAbs, data, {
executable: !!v.executable,
metadata: v.metadata ?? null
})
}
await drive.del(fromAbs)
return
}
const names = await driveReaddirNames(drive, fromAbs)
for (const n of names) {
const f = fromAbs === '/' ? `/${n}` : `${fromAbs}/${n}`
const t = toAbs === '/' ? `/${n}` : `${toAbs}/${n}`
await moveDriveSubtree(drive, f, t)
}
try {
await drive.del(fromAbs)
} catch {
/* directory may have no tombstone */
}
}
/**
* One-time best-effort: flat personal-root files from older booters → `/.bare-os/home/<seg>/`.
* @param {Record<string, unknown>} ctx
*/
export async function migrateLegacyPersonalHomeIfNeeded(ctx) {
const drive = ctx.personalDrive
const vfs = ctx.vfs
if (!drive || typeof drive.put !== 'function' || !vfs?.env) return
const home = vfs.env.HOME
if (typeof home !== 'string' || !home.startsWith('/home/')) return
const seg = home.slice('/home/'.length).split('/')[0]
if (!seg) return
const prefix = `/.bare-os/home/${seg}`
if (!(await personalHomePrefixIsEmpty(drive, prefix))) return
const rootNames = await driveReaddirNames(drive, '/')
for (const name of rootNames) {
if (legacyRootMigrateSkip(name)) continue
const from = `/${name}`
const to = `${prefix}/${name}`
try {
await moveDriveSubtree(drive, from, to)
} catch {
/* best-effort */
}
}
try {
const oldGuest = await drive.get('/.keep_guest')
if (oldGuest) await drive.del('/.keep_guest')
} catch {
/* ignore */
}
}
/**
* @param {import('hyperdrive').default} drive
* @param {string} dir
+34 -14
View File
@@ -18,13 +18,10 @@ import {
/** Empty-directory marker (must match {@link ./git-fs-adapter.js}). */
const DIR_MARKER = '.bareos_empty'
/** Personal-drive backing for logical `/var/log/…` (system drive is read-only). */
const VAR_LOG_STORAGE = '/.bare-os/var/log'
/**
* Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME,
* optional HDMS mounts under /mnt/<label>/…, virtual /var with writable /var/log/…
* on the personal drive at /.bare-os/var/log/
* on the personal drive under /.bare-os/var/log/<home-seg>/… (session-isolated).
* @param {import('hyperdrive').default} systemDrive
* @param {import('hyperdrive').default} personalDrive
* @param {Record<string, string>} env
@@ -47,6 +44,18 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
return seg || null
}
/** Session home tree on the personal drive (isolates guest vs unlocked users). */
function personalHomeStorageRoot() {
const seg = activeHomeBasename()
return seg ? `/.bare-os/home/${seg}` : '/.bare-os/home/_nosession'
}
/** Personal-drive backing for logical `/var/log/…` (per session segment). */
function varLogStorageRoot() {
const seg = activeHomeBasename()
return seg ? `/.bare-os/var/log/${seg}` : '/.bare-os/var/log/_nosession'
}
/**
* `cd ~`, `touch ~/x`, etc. must map to $HOME — otherwise `unix-path-resolve(cwd, '~')`
* becomes `/~` on the system drive (read-only).
@@ -107,12 +116,13 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
if (varNorm === '/var') {
return { virtualVarRoot: true }
}
const varLogRoot = varLogStorageRoot()
if (varNorm === '/var/log' || absPath === '/var/log/') {
return { drive: personalDrive, path: VAR_LOG_STORAGE }
return { drive: personalDrive, path: varLogRoot }
}
if (absPath.startsWith('/var/log/')) {
const rel = absPath.slice('/var/log/'.length).replace(/^\/+/, '')
const p = rel ? unixPathResolve(VAR_LOG_STORAGE, rel) : VAR_LOG_STORAGE
const p = rel ? unixPathResolve(varLogRoot, rel) : varLogRoot
return { drive: personalDrive, path: p }
}
if (absPath.startsWith('/var/')) {
@@ -121,6 +131,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
const h = normalizeHome()
const activeSeg = activeHomeBasename()
const homeRoot = personalHomeStorageRoot()
if (activeSeg && absPath === '/home') {
return { virtualHomeDir: true }
@@ -132,19 +143,22 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
const seg = slash === -1 ? after : after.slice(0, slash)
const rest = slash === -1 ? '' : after.slice(slash + 1)
if (seg === activeSeg) {
const sub = rest ? '/' + rest.replace(/^\//, '') : '/'
const p = unixPathResolve('/', sub)
if (!rest) {
return { drive: personalDrive, path: homeRoot }
}
const rel = rest.replace(/^\/+/, '')
const p = unixPathResolve(homeRoot, rel)
return { drive: personalDrive, path: p }
}
return { drive: systemDrive, path: absPath }
}
if (absPath === h || absPath.startsWith(h + '/')) {
const sub =
absPath === h
? '/'
: '/' + absPath.slice(h.length + 1).replace(/^\//, '')
const p = unixPathResolve('/', sub)
if (absPath === h) {
return { drive: personalDrive, path: homeRoot }
}
const rel = absPath.slice(h.length + 1).replace(/^\/+/, '')
const p = unixPathResolve(homeRoot, rel)
return { drive: personalDrive, path: p }
}
return { drive: systemDrive, path: absPath }
@@ -277,10 +291,16 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
if (
normLog === '/var/log' &&
r.drive === personalDrive &&
p === VAR_LOG_STORAGE
p === varLogStorageRoot()
) {
return { ...synthesizeStat(abs, true, env, 'directory'), path: abs }
}
const h = normalizeHome()
const homeNorm = h.replace(/\/+$/, '') || h
const absNorm = abs.replace(/\/+$/, '') || abs
if (activeSeg && absNorm === homeNorm) {
return { ...synthesizeStat(absNorm, true, env, 'directory'), path: abs }
}
return null
}
+86 -14
View File
@@ -92,6 +92,13 @@ function testCtx(drive, personal, env) {
}
}
/** Personal-drive path backing logical `/home/<seg>/…` (matches `vfs.js`). */
function personalHomeBacking(homePath, rel = '') {
const seg = homePath.replace(/^\/home\//, '').split('/')[0] || 'user'
const r = String(rel).replace(/^\//, '')
return r ? `/.bare-os/home/${seg}/${r}` : `/.bare-os/home/${seg}`
}
test('runKernelFromSource invokes start(ctx)', async (t) => {
const calls = []
const source = `
@@ -135,7 +142,7 @@ test('runBinCommand runs bare hello.js from cwd on personal drive', async (t) =>
await drive.ready()
await personal.ready()
await personal.put(
'/hello.js',
personalHomeBacking('/home/user', 'hello.js'),
b4a.from(`
async function run(ctx, argv) {
ctx.out.push(argv.join(' '))
@@ -159,7 +166,7 @@ test('runBinCommand strips shebang from user script', async (t) => {
await drive.ready()
await personal.ready()
await personal.put(
'/x.js',
personalHomeBacking('/home/user', 'x.js'),
b4a.from(`#!/usr/bin/env bare
async function run(ctx) { ctx.out.push('ok') }
`)
@@ -180,7 +187,10 @@ test('runBinCommand user script error is caught and logged', async (t) => {
const personal = new Hyperdrive(store.namespace('ptj'))
await drive.ready()
await personal.ready()
await personal.put('/bad.js', b4a.from(`async function run() { test() }`))
await personal.put(
personalHomeBacking('/home/user', 'bad.js'),
b4a.from(`async function run() { test() }`)
)
const errs = []
const ctx = testCtx(drive, personal)
ctx.console = {
@@ -206,8 +216,8 @@ test('ls hides dotfiles unless -a', async (t) => {
await drive.ready()
await personal.ready()
await drive.put('/bin/ls', b4a.from(lsSrc))
await personal.put('/shown.txt', b4a.from(''))
await personal.put('/.hidden', b4a.from(''))
await personal.put(personalHomeBacking('/home/user', 'shown.txt'), b4a.from(''))
await personal.put(personalHomeBacking('/home/user', '.hidden'), b4a.from(''))
const lines = []
const ctx = testCtx(drive, personal)
ctx.console = {
@@ -346,7 +356,7 @@ test('vfs routes HOME to personal drive', async (t) => {
const env = { HOME: '/home/user', PWD: '/home/user', PATH: '/bin' }
const vfs = createVfs(sys, personal, env)
await vfs.writeFile('f.txt', b4a.from('hi'))
const buf = await personal.get('/f.txt')
const buf = await personal.get(personalHomeBacking('/home/user', 'f.txt'))
t.ok(buf)
t.is(b4a.toString(buf), 'hi')
const sysTry = await sys.get('/home/user/f.txt')
@@ -387,7 +397,7 @@ test('vfs expands ~ and ~/ to $HOME (not read-only /~)', async (t) => {
await vfs.chdir('~')
t.is(vfs.getcwd(), '/home/zuser')
await vfs.writeFile('~/tilde.txt', b4a.from('ok'))
const buf = await personal.get('/tilde.txt')
const buf = await personal.get(personalHomeBacking('/home/zuser', 'tilde.txt'))
t.ok(buf)
t.is(b4a.toString(buf), 'ok')
await store.close()
@@ -416,12 +426,66 @@ test('vfs lists virtual /home at root; /home shows only active session dir', asy
t.alike(await vfsU.readdir('/home'), ['eeb18de988e9'])
t.is(await vfsU.stat('/home/guest'), null)
await vfsU.writeFile('/home/eeb18de988e9/x', b4a.from('1'))
const buf = await personal.get('/x')
const buf = await personal.get(
personalHomeBacking('/home/eeb18de988e9', 'x')
)
t.ok(buf)
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('vfs isolates home and /var/log per HOME basename on personal drive', async (t) => {
const dir = testCorestoreDir('vfsisol')
const store = new Corestore(dir)
const sys = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pvis'))
await sys.ready()
await personal.ready()
const vfsGuest = createVfs(sys, personal, {
HOME: '/home/guest',
PWD: '/home/guest',
PATH: '/bin'
})
await vfsGuest.writeFile('shared-name.txt', b4a.from('guest-data'))
await vfsGuest.writeFile('/var/log/bare-os/g.log', b4a.from('glog'))
const vfsUser = createVfs(sys, personal, {
HOME: '/home/alice12hex',
PWD: '/home/alice12hex',
PATH: '/bin'
})
t.is(await vfsUser.readFile('/home/alice12hex/shared-name.txt'), null)
await vfsUser.writeFile('shared-name.txt', b4a.from('user-data'))
t.is(
b4a.toString(
await personal.get(personalHomeBacking('/home/guest', 'shared-name.txt'))
),
'guest-data'
)
t.is(
b4a.toString(
await personal.get(
personalHomeBacking('/home/alice12hex', 'shared-name.txt')
)
),
'user-data'
)
t.is(
b4a.toString(
await personal.get('/.bare-os/var/log/guest/bare-os/g.log')
),
'glog'
)
await vfsUser.writeFile('/var/log/bare-os/u.log', b4a.from('ulog'))
t.is(
b4a.toString(
await personal.get('/.bare-os/var/log/alice12hex/bare-os/u.log')
),
'ulog'
)
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('vfs /mnt lists HDMS mounts and allows writable put', async (t) => {
const dir = testCorestoreDir('vfsmnt')
const store = new Corestore(dir)
@@ -464,10 +528,12 @@ test('vfs /var in root readdir; /var/log empty lstat; writes map to personal', a
t.is(stLog.type, 'directory')
await vfs.mkdir('/var/log/bare-os', { recursive: true })
await vfs.writeFile('/var/log/bare-os/test-vfs.log', b4a.from('hello'))
const buf = await personal.get('/.bare-os/var/log/bare-os/test-vfs.log')
const buf = await personal.get(
'/.bare-os/var/log/u/bare-os/test-vfs.log'
)
t.ok(buf)
t.is(b4a.toString(buf), 'hello')
const sysTry = await sys.get('/.bare-os/var/log/bare-os/test-vfs.log')
const sysTry = await sys.get('/.bare-os/var/log/u/bare-os/test-vfs.log')
t.is(sysTry, null)
await store.close()
rmSync(dir, { recursive: true, force: true })
@@ -577,7 +643,7 @@ test('loadBarerc applies export and alias from personal ~/.barerc', async (t) =>
await drive.ready()
await personal.ready()
await personal.put(
'/.barerc',
personalHomeBacking('/home/user', '.barerc'),
b4a.from('export MYRC=1\nalias dog=echo woof\n')
)
const ctx = testCtx(drive, personal)
@@ -1418,8 +1484,14 @@ test('tier-1 rm -rf removes directory tree on personal drive', async (t) => {
const ctx = testCtx(drive, personal)
ctx.exitCode = 0
ctx.console = { log() {}, error() {} }
await personal.put('/nest/leaf/x.txt', b4a.from('x'))
await personal.put('/nest/other/y.txt', b4a.from('y'))
await personal.put(
personalHomeBacking('/home/user', 'nest/leaf/x.txt'),
b4a.from('x')
)
await personal.put(
personalHomeBacking('/home/user', 'nest/other/y.txt'),
b4a.from('y')
)
t.ok((await ctx.vfs.readdir('nest')).includes('leaf'))
await runBinCommand(ctx, ['rm', '-rf', 'nest'])
t.is(ctx.exitCode, 0)
@@ -1482,7 +1554,7 @@ test('runBinCommand runs ./git from cwd instead of booter delegate', async (t) =
await drive.ready()
await personal.ready()
await personal.put(
'/git',
personalHomeBacking('/home/user', 'git'),
b4a.from(`
async function run(ctx) {
ctx.out.push('local-git-script')
File diff suppressed because one or more lines are too long