Files
bare-operating-system/packages/bare-os-coreutils/test/hardening.test.mjs
T
Raven Scott 21325e18d3
Release rolling / release (push) Successful in 9m45s
Harden Coreutils Bump to 0.1.1
2026-08-12 21:47:16 -04:00

377 lines
9.6 KiB
JavaScript

/**
* Hardening regressions for high-traffic /bin utilities.
*/
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import test from 'brittle'
import b4a from 'b4a'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
async function loadBin(name) {
const runtime = await readFile(path.join(__dirname, '../lib/runtime.js'), 'utf8')
const bodyRaw = await readFile(path.join(__dirname, `../src/${name}.js`), 'utf8')
const body = bodyRaw.replace(/^export\s+\{[^}]*\}\s*;?\s*$/gm, '')
return new AsyncFunction(
'ctx',
'argv',
`${runtime}\n${body}\nif (typeof run === 'function') return await run(ctx, argv)\n`
)
}
function mkCtx(vfs = {}) {
const logs = []
const errs = []
const raw = []
return {
b4a,
exitCode: 0,
shellStdin: '',
console: {
log: (m) => logs.push(String(m)),
error: (m) => errs.push(String(m))
},
vfs: { env: {}, ...vfs },
bareOsBinWrite: (u8) => {
raw.push(u8 instanceof Uint8Array ? u8 : b4a.from(u8))
},
_logs: logs,
_errs: errs,
_raw: raw
}
}
function memVfs(files = {}) {
const store = { ...files }
return {
env: {},
files: store,
resolveLogical: (p) => String(p || ''),
async lstat(p) {
if (!Object.prototype.hasOwnProperty.call(store, p)) return null
const e = store[p]
if (e && e.stat) return e.stat
return { type: 'file', mode: 0o644, mtimeMs: 1, size: e ? e.length : 0 }
},
async stat(p) {
return this.lstat(p)
},
async readFile(p) {
if (!Object.prototype.hasOwnProperty.call(store, p)) return null
const e = store[p]
if (e && e.body) return e.body
return e
},
async writeFile(p, buf, opts) {
store[p] = buf
return opts
},
async unlink(p) {
if (!Object.prototype.hasOwnProperty.call(store, p)) {
const err = new Error('ENOENT')
err.code = 'ENOENT'
throw err
}
delete store[p]
},
async mkdir(p) {
store[p] = { stat: { type: 'directory', mode: 0o755, mtimeMs: 1 } }
},
async readdir(p) {
const prefix = String(p).replace(/\/+$/, '') + '/'
const names = new Set()
for (const k of Object.keys(store)) {
if (k.startsWith(prefix)) {
const rest = k.slice(prefix.length)
if (rest && !rest.includes('/')) names.add(rest)
}
}
return [...names]
},
async rm(p) {
if (!Object.prototype.hasOwnProperty.call(store, p)) {
const err = new Error('ENOENT')
err.code = 'ENOENT'
throw err
}
delete store[p]
},
async chmod() {
return true
},
async chown() {
return true
}
}
}
test('mv refuses self-move', async (t) => {
const run = await loadBin('mv')
const vfs = memVfs({ '/a': b4a.from('keep') })
const ctx = mkCtx(vfs)
await run(ctx, ['mv', '/a', '/a'])
t.is(ctx.exitCode, 1)
t.ok(vfs.files['/a'])
t.ok(ctx._errs.join('\n').includes('cannot move'))
})
test('cp refuses copy into self', async (t) => {
const run = await loadBin('cp')
const vfs = memVfs({
'/dir': { stat: { type: 'directory', mode: 0o755 } },
'/dir/f': b4a.from('x')
})
const ctx = mkCtx(vfs)
await run(ctx, ['cp', '-R', '/dir', '/dir'])
t.is(ctx.exitCode, 1)
t.ok(ctx._errs.join('\n').includes('itself'))
})
test('touch does not replace a directory with an empty file', async (t) => {
const run = await loadBin('touch')
const vfs = memVfs({
'/d': { stat: { type: 'directory', mode: 0o755, mtimeMs: 1 } }
})
const ctx = mkCtx(vfs)
await run(ctx, ['touch', '/d'])
t.is(ctx.exitCode, 0)
t.is(vfs.files['/d'].stat.type, 'directory')
})
test('truncate does not clobber unread existing file', async (t) => {
const run = await loadBin('truncate')
const vfs = memVfs({
'/secret': { stat: { type: 'file', mode: 0o600, mtimeMs: 1 } }
})
vfs.readFile = async () => null
const ctx = mkCtx(vfs)
await run(ctx, ['truncate', '-s', '0', '/secret'])
t.is(ctx.exitCode, 1)
t.ok(ctx._errs.join('\n').includes('cannot read'))
})
test('rm -i is rejected rather than deleting', async (t) => {
const run = await loadBin('rm')
const vfs = memVfs({ '/f': b4a.from('x') })
const ctx = mkCtx(vfs)
await run(ctx, ['rm', '-i', '/f'])
t.is(ctx.exitCode, 1)
t.ok(vfs.files['/f'])
t.ok(ctx._errs.join('\n').includes('invalid option'))
})
test('rm -f with no operands exits 0', async (t) => {
const run = await loadBin('rm')
const ctx = mkCtx(memVfs())
await run(ctx, ['rm', '-f'])
t.is(ctx.exitCode, 0)
})
test('head -n 0 prints nothing; missing file exits 1', async (t) => {
const run = await loadBin('head')
const ctx = mkCtx({
env: {},
async readFile() {
return b4a.from('a\nb\n')
}
})
ctx.shellStdin = 'a\nb\n'
await run(ctx, ['head', '-n', '0'])
t.is(ctx.exitCode, 0)
t.is(ctx._logs.join(''), '')
const ctx2 = mkCtx({
env: {},
async readFile() {
return null
}
})
await run(ctx2, ['head', '/missing'])
t.is(ctx2.exitCode, 1)
})
test('wc missing file exits 1', async (t) => {
const run = await loadBin('wc')
const ctx = mkCtx({
env: {},
async readFile() {
return null
}
})
await run(ctx, ['wc', '/missing'])
t.is(ctx.exitCode, 1)
})
test('du missing path exits 1', async (t) => {
const run = await loadBin('du')
const ctx = mkCtx({
env: {},
resolveLogical: (p) => p,
async lstat() {
return null
},
async stat() {
return null
}
})
await run(ctx, ['du', '/nope'])
t.is(ctx.exitCode, 1)
t.ok(ctx._errs.join('\n').includes('No such file'))
})
test('mkdir without -p fails if directory exists', async (t) => {
const run = await loadBin('mkdir')
const ctx = mkCtx({
env: {},
async lstat() {
return { type: 'directory' }
},
async mkdir() {
throw new Error('should not mkdir')
}
})
await run(ctx, ['mkdir', '/already'])
t.is(ctx.exitCode, 1)
t.ok(ctx._errs.join('\n').includes('File exists'))
})
test('base64 decode uses portable alphabet (not Node Buffer)', async (t) => {
const run = await loadBin('base64')
const ctx = mkCtx()
ctx.b4a = {
from: (s) => new TextEncoder().encode(String(s)),
toString: (u8) => new TextDecoder().decode(u8)
}
ctx.shellStdin = 'YWJj'
await run(ctx, ['base64', '-d'])
t.is(ctx.exitCode, 0)
t.is(new TextDecoder().decode(ctx._raw[0]), 'abc')
})
test('basenc hex decode rejects junk', async (t) => {
const run = await loadBin('basenc')
const ctx = mkCtx()
ctx.shellStdin = 'zzzz'
await run(ctx, ['basenc', '--base16', '-d'])
t.is(ctx.exitCode, 1)
t.ok(ctx._errs.join('\n').includes('invalid hex'))
})
test('seq -5 5 works and huge spans are capped', async (t) => {
const run = await loadBin('seq')
const ctx = mkCtx()
await run(ctx, ['seq', '-5', '5'])
t.is(ctx.exitCode, 0)
t.ok(ctx._logs.includes('-5'))
t.ok(ctx._logs.includes('5'))
const ctx2 = mkCtx({ env: { BARE_OS_SEQ_MAX_LINES: '10' } })
await run(ctx2, ['seq', '1', '100'])
t.is(ctx2.exitCode, 1)
t.ok(ctx2._errs.join('\n').includes('exceeds'))
})
test('grep -m 0 matches nothing', async (t) => {
const run = await loadBin('grep')
const ctx = mkCtx()
ctx.shellStdin = 'foo\nbar\n'
await run(ctx, ['grep', '-m', '0', 'foo'])
t.is(ctx.exitCode, 1)
t.is(ctx._logs.join(''), '')
})
test('expr zero result exits 1', async (t) => {
const run = await loadBin('expr')
const ctx = mkCtx()
await run(ctx, ['expr', '1', '-', '1'])
t.is(ctx.exitCode, 1)
t.is(ctx._logs.join(''), '0')
})
test('cmp missing file exits 2', async (t) => {
const run = await loadBin('cmp')
const ctx = mkCtx({
async readFile() {
return null
}
})
await run(ctx, ['cmp', '/a', '/b'])
t.is(ctx.exitCode, 2)
})
test('pathchk empty path exits 1', async (t) => {
const run = await loadBin('pathchk')
const ctx = mkCtx()
await run(ctx, ['pathchk', ''])
t.is(ctx.exitCode, 1)
})
test('chmod rejects garbage symbolic mode', async (t) => {
const run = await loadBin('chmod')
const ctx = mkCtx({
async stat() {
return { type: 'file', mode: 0o644 }
},
async chmod() {
return true
}
})
await run(ctx, ['chmod', 'garbage', '/f'])
t.is(ctx.exitCode, 1)
t.ok(ctx._errs.join('\n').includes('invalid mode'))
})
test('sum empty file reports 0 blocks', async (t) => {
const run = await loadBin('sum')
const ctx = mkCtx()
ctx.shellStdin = ''
await run(ctx, ['sum'])
t.is(ctx.exitCode, 0)
t.ok(ctx._logs[0].startsWith('0\t0\t'))
})
test('hostname set does not throw', async (t) => {
const run = await loadBin('hostname')
const ctx = mkCtx()
await run(ctx, ['hostname', '-S', 'box'])
t.is(ctx.exitCode, 1)
t.ok(ctx._errs.join('\n').includes('setting disabled'))
})
test('sleep missing operand exits 1', async (t) => {
const run = await loadBin('sleep')
const ctx = mkCtx()
await run(ctx, ['sleep'])
t.is(ctx.exitCode, 1)
})
test('factor rejects non-integers', async (t) => {
const run = await loadBin('factor')
const ctx = mkCtx()
await run(ctx, ['factor', 'foo'])
t.is(ctx.exitCode, 1)
})
test('tsort odd token count exits 1', async (t) => {
const run = await loadBin('tsort')
const ctx = mkCtx()
ctx.shellStdin = 'a b c'
await run(ctx, ['tsort'])
t.is(ctx.exitCode, 1)
})
test('unlink -- -foo unlinks dashed name', async (t) => {
const run = await loadBin('unlink')
const seen = []
const ctx = mkCtx({
async unlink(p) {
seen.push(p)
}
})
await run(ctx, ['unlink', '--', '-foo'])
t.is(ctx.exitCode, 0)
t.alike(seen, ['-foo'])
})