Guest isolation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user