@threadedstack/wmd-web 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -2
- package/RELEASE.json +41 -56
- package/guest/arm64/Image +2 -2
- package/guest/arm64/Image-4k +1 -1
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +4 -4
- package/relay/hosted.ts +10 -8
- package/relay/main.ts +2 -2
- package/relay/relay.ts +1 -0
- package/relay/token.ts +34 -5
- package/relay/wisp.ts +37 -5
- package/src/boot.js +53 -9
- package/src/build/builder.js +791 -0
- package/src/build/context.js +349 -0
- package/src/build/dockerfile.js +1055 -0
- package/src/build/dockerignore.js +270 -0
- package/src/build/fast.js +1187 -0
- package/src/build/inspect.js +78 -0
- package/src/build/pin.js +6 -0
- package/src/build/tar.js +304 -0
- package/src/host/ckpt-point.js +55 -10
- package/src/host/ckpt.js +35 -2
- package/src/host/ctl.js +17 -0
- package/src/host/disk-map.js +330 -0
- package/src/host/disk-worker.js +125 -0
- package/src/host/disk.js +219 -0
- package/src/host/wmd-imports.js +3 -3
- package/src/image-files.js +2 -1
- package/src/registry/hub.js +113 -44
- package/src/registry/load-tar.js +473 -0
- package/src/registry/pull.js +5 -3
- package/src/worker.js +5 -1
- package/wmdpack.wasm +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Reading built layers back, for the fast-path parity gate (docs/plans/web-buildkit-impl.md 5.5): the entries of a
|
|
2
|
+
// layer tar (path, type, mode, owner, size, sha256, link target, xattrs, mtime) and the layers of a Docker tar
|
|
3
|
+
// (`docker save` layout or an OCI layout, layers plain or gzip).
|
|
4
|
+
import { scanTarBytes } from './tar.js'
|
|
5
|
+
|
|
6
|
+
const dec = new TextDecoder()
|
|
7
|
+
const TYPES = { 0: 'file', 7: 'file', 1: 'hardlink', 2: 'symlink', 3: 'char', 4: 'block', 5: 'dir', 6: 'fifo' }
|
|
8
|
+
|
|
9
|
+
const hex = (buf) => [...new Uint8Array(buf)].map((x) => x.toString(16).padStart(2, '0')).join('')
|
|
10
|
+
const sha256Hex = async (u8) => hex(await crypto.subtle.digest('SHA-256', u8))
|
|
11
|
+
|
|
12
|
+
/** gunzip when the bytes start with the gzip magic; else the bytes themselves. */
|
|
13
|
+
export async function maybeGunzip(u8) {
|
|
14
|
+
if (!(u8.length > 2 && u8[0] === 0x1f && u8[1] === 0x8b)) return u8
|
|
15
|
+
const s = new Blob([u8]).stream().pipeThrough(new DecompressionStream('gzip'))
|
|
16
|
+
return new Uint8Array(await new Response(s).arrayBuffer())
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** './a/', '/a' and 'a' are the same path: no leading './' or '/', no trailing '/'. */
|
|
20
|
+
export function cleanEntryPath(p) {
|
|
21
|
+
return p.replace(/^(\.\/|\/)+/, '').replace(/\/+$/, '') || '.'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every entry of a layer tar (plain or gzip) as {path, type, mode, uid, gid, size, sha256, linkname, xattrs, mtime}
|
|
26
|
+
* in archive order. `sha256` is the file's content for regular files, else null; `mode` the permission bits (with
|
|
27
|
+
* setuid/setgid/sticky).
|
|
28
|
+
*/
|
|
29
|
+
export async function layerEntries(bytes) {
|
|
30
|
+
const u8 = await maybeGunzip(bytes)
|
|
31
|
+
const out = []
|
|
32
|
+
for await (const e of scanTarBytes(u8)) {
|
|
33
|
+
const type = TYPES[e.type] || `type-${e.type}`
|
|
34
|
+
out.push({
|
|
35
|
+
path: cleanEntryPath(e.name),
|
|
36
|
+
type,
|
|
37
|
+
mode: e.mode & 0o7777,
|
|
38
|
+
uid: e.uid,
|
|
39
|
+
gid: e.gid,
|
|
40
|
+
size: type === 'file' ? e.size : 0,
|
|
41
|
+
sha256: type === 'file' ? await sha256Hex(u8.subarray(e.offset, e.offset + e.size)) : null,
|
|
42
|
+
linkname: e.linkname,
|
|
43
|
+
xattrs: e.xattrs || {},
|
|
44
|
+
mtime: e.mtime,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A Docker tar (what BuildKit's type=docker output writes: a `docker save` layout with manifest.json, or an OCI
|
|
52
|
+
* layout with index.json) as {config, layers}: `config` the image config JSON, `layers` the layer tars' bytes
|
|
53
|
+
* bottom first, as stored (plain or gzip).
|
|
54
|
+
*/
|
|
55
|
+
export async function dockerTarLayers(bytes) {
|
|
56
|
+
const files = new Map()
|
|
57
|
+
for await (const e of scanTarBytes(bytes)) {
|
|
58
|
+
if (e.type === '0' || e.type === '7') files.set(cleanEntryPath(e.name), bytes.subarray(e.offset, e.offset + e.size))
|
|
59
|
+
}
|
|
60
|
+
const get = (p) => {
|
|
61
|
+
const f = files.get(cleanEntryPath(p))
|
|
62
|
+
if (!f) throw new Error(`the Docker tar has no ${p}`)
|
|
63
|
+
return f
|
|
64
|
+
}
|
|
65
|
+
const json = (p) => JSON.parse(dec.decode(get(p)))
|
|
66
|
+
const blob = (digest) => get(`blobs/${digest.replace(':', '/')}`)
|
|
67
|
+
if (files.has('manifest.json')) {
|
|
68
|
+
const m = json('manifest.json')[0]
|
|
69
|
+
return { config: json(m.Config), layers: m.Layers.map((l) => get(l)) }
|
|
70
|
+
}
|
|
71
|
+
if (files.has('index.json')) {
|
|
72
|
+
let m = json('index.json').manifests[0]
|
|
73
|
+
let man = JSON.parse(dec.decode(blob(m.digest)))
|
|
74
|
+
if (man.manifests) man = JSON.parse(dec.decode(blob(man.manifests[0].digest)))
|
|
75
|
+
return { config: JSON.parse(dec.decode(blob(man.config.digest))), layers: man.layers.map((l) => blob(l.digest)) }
|
|
76
|
+
}
|
|
77
|
+
throw new Error('the tar has neither manifest.json nor index.json')
|
|
78
|
+
}
|
package/src/build/pin.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The wmd-web builder images (scripts/web-builder.sh writes this file): the sha256 and size of each
|
|
2
|
+
// `docker save` tar in web/dist/builder/. createBuilder refuses a tar that does not match.
|
|
3
|
+
export const BUILDER_PINS = {
|
|
4
|
+
arm64: { file: 'builder-arm64.tar', sha256: '905d0735a2fb7aa12f00bb21f50e82651d1f65db0aa030aa38ab4033047072ee', bytes: 107119104 },
|
|
5
|
+
amd64: { file: 'builder-amd64.tar', sha256: 'be2199d5eb05d1bcc2814999749de17d4e4ce206726f31fe8a495cf602fd3ccd', bytes: 113018368 },
|
|
6
|
+
}
|
package/src/build/tar.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// A small deterministic tar writer and header scanner (docs/plans/web-buildkit-impl.md 5.6).
|
|
2
|
+
// Output is ustar; a pax extended header (typeflag 'x') carries what ustar cannot hold: names over the
|
|
3
|
+
// name+prefix limits, non-ASCII names, link targets over 100 bytes, sizes of 8 GiB or more, and uid/gid/mtime
|
|
4
|
+
// that overflow their octal fields. Nothing reads the host clock: mtime is what the caller gives, else 0.
|
|
5
|
+
|
|
6
|
+
const enc = new TextEncoder()
|
|
7
|
+
const dec = new TextDecoder()
|
|
8
|
+
const BLOCK = 512
|
|
9
|
+
|
|
10
|
+
/** TextDecoder.decode that also takes a view on a SharedArrayBuffer (wasm memory), which browsers' decoders refuse. */
|
|
11
|
+
export function decodeText(u8, d = dec) {
|
|
12
|
+
return typeof SharedArrayBuffer !== 'undefined' && u8.buffer instanceof SharedArrayBuffer ? d.decode(u8.slice()) : d.decode(u8)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Throws on a path that must not enter an archive: absolute, '..', NUL, empty components. */
|
|
16
|
+
export function checkTarPath(path) {
|
|
17
|
+
if (path === './') return // the archive's root folder entry
|
|
18
|
+
if (typeof path !== 'string' || path === '') throw new Error('tar: empty path')
|
|
19
|
+
if (path.includes('\0')) throw new Error(`tar: NUL in path ${JSON.stringify(path)}`)
|
|
20
|
+
if (path.startsWith('/')) throw new Error(`tar: absolute path ${path}`)
|
|
21
|
+
const parts = path.split('/')
|
|
22
|
+
if (parts[parts.length - 1] === '') parts.pop() // one trailing '/' is a directory spelling
|
|
23
|
+
for (const p of parts) {
|
|
24
|
+
if (p === '' || p === '.' || p === '..') throw new Error(`tar: unsafe path ${path}`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isAscii(s) {
|
|
29
|
+
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) return false
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toSeconds(t) {
|
|
34
|
+
if (t === undefined || t === null) return 0
|
|
35
|
+
if (t instanceof Date) return Math.floor(t.getTime() / 1000)
|
|
36
|
+
return Math.max(0, Math.floor(Number(t) || 0))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function octal(buf, off, len, value) {
|
|
40
|
+
const s = value.toString(8).padStart(len - 1, '0')
|
|
41
|
+
for (let i = 0; i < len - 1; i++) buf[off + i] = s.charCodeAt(i)
|
|
42
|
+
buf[off + len - 1] = 0
|
|
43
|
+
}
|
|
44
|
+
const octMax = (len) => 8 ** (len - 1) - 1
|
|
45
|
+
|
|
46
|
+
function putStr(buf, off, str) {
|
|
47
|
+
for (let i = 0; i < str.length; i++) buf[off + i] = str.charCodeAt(i)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One pax record: "<len> <key>=<value>\n" where len counts itself. */
|
|
51
|
+
function paxRecord(key, value) {
|
|
52
|
+
const body = enc.encode(` ${key}=${value}\n`)
|
|
53
|
+
let n = body.length + 1
|
|
54
|
+
while (String(n).length + body.length !== n) n = String(n).length + body.length
|
|
55
|
+
const head = enc.encode(String(n))
|
|
56
|
+
const out = new Uint8Array(n)
|
|
57
|
+
out.set(head, 0)
|
|
58
|
+
out.set(body, head.length)
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function concat(parts, total) {
|
|
63
|
+
const out = new Uint8Array(total ?? parts.reduce((a, p) => a + p.length, 0))
|
|
64
|
+
let o = 0
|
|
65
|
+
for (const p of parts) {
|
|
66
|
+
out.set(p, o)
|
|
67
|
+
o += p.length
|
|
68
|
+
}
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function rawHeader(name, prefix, typeflag, size, mode, mtime, uid, gid, linkname) {
|
|
73
|
+
const h = new Uint8Array(BLOCK)
|
|
74
|
+
putStr(h, 0, name)
|
|
75
|
+
octal(h, 100, 8, mode)
|
|
76
|
+
octal(h, 108, 8, uid)
|
|
77
|
+
octal(h, 116, 8, gid)
|
|
78
|
+
octal(h, 124, 12, size)
|
|
79
|
+
octal(h, 136, 12, mtime)
|
|
80
|
+
h.fill(0x20, 148, 156)
|
|
81
|
+
h[156] = typeflag.charCodeAt(0)
|
|
82
|
+
putStr(h, 157, linkname)
|
|
83
|
+
putStr(h, 257, 'ustar')
|
|
84
|
+
h[262] = 0
|
|
85
|
+
h[263] = 0x30
|
|
86
|
+
h[264] = 0x30
|
|
87
|
+
putStr(h, 345, prefix)
|
|
88
|
+
let sum = 0
|
|
89
|
+
for (let i = 0; i < BLOCK; i++) sum += h[i]
|
|
90
|
+
const cs = sum.toString(8).padStart(6, '0')
|
|
91
|
+
putStr(h, 148, cs)
|
|
92
|
+
h[154] = 0
|
|
93
|
+
h[155] = 0x20
|
|
94
|
+
return h
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The header block(s) of one entry: a pax header first when needed, then the ustar header.
|
|
99
|
+
* Exported so the over-8-GiB size path can be tested without 8 GiB of data.
|
|
100
|
+
* @returns {Uint8Array}
|
|
101
|
+
*/
|
|
102
|
+
export function tarEntryHeader(path, typeflag, size, { mode, mtime, uid = 0, gid = 0, linkname = '' } = {}) {
|
|
103
|
+
checkTarPath(path)
|
|
104
|
+
const isDir = typeflag === '5'
|
|
105
|
+
const full = isDir && !path.endsWith('/') ? `${path}/` : path
|
|
106
|
+
const pax = []
|
|
107
|
+
let name = full
|
|
108
|
+
let prefix = ''
|
|
109
|
+
const nameBytes = enc.encode(full)
|
|
110
|
+
if (isAscii(full) && nameBytes.length <= 100) {
|
|
111
|
+
// plain
|
|
112
|
+
} else if (isAscii(full) && nameBytes.length <= 255 + 1 + 100) {
|
|
113
|
+
// ustar prefix/name split at a '/'
|
|
114
|
+
let at = -1
|
|
115
|
+
for (let i = full.length - 2; i > 0; i--) {
|
|
116
|
+
if (full[i] !== '/') continue
|
|
117
|
+
if (full.length - i - 1 <= 100 && i <= 155) {
|
|
118
|
+
at = i
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (at > 0) {
|
|
123
|
+
prefix = full.slice(0, at)
|
|
124
|
+
name = full.slice(at + 1)
|
|
125
|
+
} else pax.push(['path', full])
|
|
126
|
+
} else pax.push(['path', full])
|
|
127
|
+
if (pax.length) {
|
|
128
|
+
// ustar fallback name: ASCII-only tail, never empty
|
|
129
|
+
name = full.replace(/[^\x20-\x7e]/g, '_').slice(-100)
|
|
130
|
+
prefix = ''
|
|
131
|
+
}
|
|
132
|
+
let link = linkname
|
|
133
|
+
if (link && (!isAscii(link) || enc.encode(link).length > 100)) {
|
|
134
|
+
pax.push(['linkpath', link])
|
|
135
|
+
link = link.replace(/[^\x20-\x7e]/g, '_').slice(0, 100)
|
|
136
|
+
}
|
|
137
|
+
const m = (mode ?? (isDir ? 0o755 : 0o644)) & 0o7777
|
|
138
|
+
let t = toSeconds(mtime)
|
|
139
|
+
if (t > octMax(12)) {
|
|
140
|
+
pax.push(['mtime', String(t)])
|
|
141
|
+
t = octMax(12)
|
|
142
|
+
}
|
|
143
|
+
let u = uid
|
|
144
|
+
if (u > octMax(8)) {
|
|
145
|
+
pax.push(['uid', String(u)])
|
|
146
|
+
u = octMax(8)
|
|
147
|
+
}
|
|
148
|
+
let g = gid
|
|
149
|
+
if (g > octMax(8)) {
|
|
150
|
+
pax.push(['gid', String(g)])
|
|
151
|
+
g = octMax(8)
|
|
152
|
+
}
|
|
153
|
+
let sz = size
|
|
154
|
+
if (sz > octMax(12)) {
|
|
155
|
+
pax.push(['size', String(sz)])
|
|
156
|
+
sz = octMax(12)
|
|
157
|
+
}
|
|
158
|
+
const main = rawHeader(name, prefix, typeflag, sz, m, t, u, g, link)
|
|
159
|
+
if (!pax.length) return main
|
|
160
|
+
const recs = pax.map(([k, v]) => paxRecord(k, v))
|
|
161
|
+
const dataLen = recs.reduce((a, r) => a + r.length, 0)
|
|
162
|
+
const padded = Math.ceil(dataLen / BLOCK) * BLOCK
|
|
163
|
+
const tail = (full.replace(/\/$/, '').split('/').pop() || 'x').replace(/[^\x20-\x7e]/g, '_').slice(-60)
|
|
164
|
+
const ph = rawHeader(`PaxHeaders.0/${tail}`, '', 'x', dataLen, 0o644, 0, 0, 0, '')
|
|
165
|
+
const body = new Uint8Array(padded)
|
|
166
|
+
body.set(concat(recs, dataLen), 0)
|
|
167
|
+
return concat([ph, body, main])
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A tar writer. Paths are relative, '/'-separated, without '..' or a leading '/'. */
|
|
171
|
+
export function tarWriter() {
|
|
172
|
+
const chunks = []
|
|
173
|
+
let total = 0
|
|
174
|
+
let done = false
|
|
175
|
+
const push = (c) => {
|
|
176
|
+
if (done) throw new Error('tar: writer already finished')
|
|
177
|
+
chunks.push(c)
|
|
178
|
+
total += c.length
|
|
179
|
+
}
|
|
180
|
+
const pad = (n) => (BLOCK - (n % BLOCK)) % BLOCK
|
|
181
|
+
return {
|
|
182
|
+
file(path, bytes, { mode, mtime, uid, gid } = {}) {
|
|
183
|
+
const data = typeof bytes === 'string' ? enc.encode(bytes) : bytes
|
|
184
|
+
push(tarEntryHeader(path, '0', data.length, { mode, mtime, uid, gid }))
|
|
185
|
+
if (data.length) {
|
|
186
|
+
push(data)
|
|
187
|
+
const p = pad(data.length)
|
|
188
|
+
if (p) push(new Uint8Array(p))
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
dir(path, { mode, mtime, uid, gid } = {}) {
|
|
192
|
+
push(tarEntryHeader(path, '5', 0, { mode, mtime, uid, gid }))
|
|
193
|
+
},
|
|
194
|
+
symlink(path, target, { mtime, uid, gid } = {}) {
|
|
195
|
+
if (!target) throw new Error('tar: empty symlink target')
|
|
196
|
+
push(tarEntryHeader(path, '2', 0, { mode: 0o777, mtime, uid, gid, linkname: target }))
|
|
197
|
+
},
|
|
198
|
+
/** The archive so far, ended with two zero blocks. */
|
|
199
|
+
finish() {
|
|
200
|
+
if (done) throw new Error('tar: writer already finished')
|
|
201
|
+
push(new Uint8Array(BLOCK * 2))
|
|
202
|
+
done = true
|
|
203
|
+
return concat(chunks, total)
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseNum(buf, off, len) {
|
|
209
|
+
if (buf[off] & 0x80) {
|
|
210
|
+
// GNU base-256
|
|
211
|
+
let v = buf[off] & 0x7f
|
|
212
|
+
for (let i = 1; i < len; i++) v = v * 256 + buf[off + i]
|
|
213
|
+
return v
|
|
214
|
+
}
|
|
215
|
+
let s = ''
|
|
216
|
+
for (let i = 0; i < len && buf[off + i] !== 0 && buf[off + i] !== 0x20; i++) s += String.fromCharCode(buf[off + i])
|
|
217
|
+
return s ? parseInt(s, 8) : 0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function cstr(buf, off, len) {
|
|
221
|
+
let e = off
|
|
222
|
+
while (e < off + len && buf[e] !== 0) e++
|
|
223
|
+
return decodeText(buf.subarray(off, e))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parsePax(data) {
|
|
227
|
+
const out = {}
|
|
228
|
+
let p = 0
|
|
229
|
+
while (p < data.length) {
|
|
230
|
+
let sp = p
|
|
231
|
+
while (sp < data.length && data[sp] !== 0x20) sp++
|
|
232
|
+
const n = parseInt(decodeText(data.subarray(p, sp)), 10)
|
|
233
|
+
if (!(n > 0) || p + n > data.length) break
|
|
234
|
+
const rec = decodeText(data.subarray(sp + 1, p + n - 1))
|
|
235
|
+
const eq = rec.indexOf('=')
|
|
236
|
+
if (eq > 0) out[rec.slice(0, eq)] = rec.slice(eq + 1)
|
|
237
|
+
p += n
|
|
238
|
+
}
|
|
239
|
+
return out
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Walks a tar's headers without reading file data. `read(offset, length)` returns a Uint8Array (sync or
|
|
244
|
+
* promise) so a Blob can be walked by slices. Yields real entries only (pax 'x'/'g' and GNU 'L'/'K' are folded
|
|
245
|
+
* into the entry they describe): {name, type, size, mode, mtime, uid, gid, linkname, xattrs, offset} where `offset` is
|
|
246
|
+
* the start of the entry's data. `type` is the typeflag character ('0' for the old NUL flag).
|
|
247
|
+
*/
|
|
248
|
+
export async function* scanTar(read, totalLength) {
|
|
249
|
+
let off = 0
|
|
250
|
+
let pax = null
|
|
251
|
+
let gnuName = null
|
|
252
|
+
let gnuLink = null
|
|
253
|
+
const need = async (o, n) => {
|
|
254
|
+
const b = await read(o, n)
|
|
255
|
+
if (b.length < n) throw new Error('tar: truncated archive')
|
|
256
|
+
return b
|
|
257
|
+
}
|
|
258
|
+
while (off + BLOCK <= totalLength) {
|
|
259
|
+
const h = await need(off, BLOCK)
|
|
260
|
+
if (h.every((b) => b === 0)) return
|
|
261
|
+
let sum = 0
|
|
262
|
+
for (let i = 0; i < BLOCK; i++) sum += i >= 148 && i < 156 ? 0x20 : h[i]
|
|
263
|
+
if (sum !== parseNum(h, 148, 8)) throw new Error(`tar: bad header checksum at ${off}`)
|
|
264
|
+
const type = h[156] === 0 ? '0' : String.fromCharCode(h[156])
|
|
265
|
+
let size = parseNum(h, 124, 12)
|
|
266
|
+
off += BLOCK
|
|
267
|
+
if (type === 'x' || type === 'g' || type === 'L' || type === 'K') {
|
|
268
|
+
const data = await need(off, size)
|
|
269
|
+
off += Math.ceil(size / BLOCK) * BLOCK
|
|
270
|
+
if (type === 'x') pax = { ...(pax || {}), ...parsePax(data) }
|
|
271
|
+
else if (type === 'L') gnuName = cstr(data, 0, data.length)
|
|
272
|
+
else if (type === 'K') gnuLink = cstr(data, 0, data.length)
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
const prefix = cstr(h, 345, 155)
|
|
276
|
+
let name = cstr(h, 0, 100)
|
|
277
|
+
if (prefix) name = `${prefix}/${name}`
|
|
278
|
+
let linkname = cstr(h, 157, 100)
|
|
279
|
+
let mtime = parseNum(h, 136, 12)
|
|
280
|
+
let uid = parseNum(h, 108, 8)
|
|
281
|
+
let gid = parseNum(h, 116, 8)
|
|
282
|
+
if (gnuName !== null) name = gnuName
|
|
283
|
+
if (gnuLink !== null) linkname = gnuLink
|
|
284
|
+
if (pax) {
|
|
285
|
+
if (pax.path !== undefined) name = pax.path
|
|
286
|
+
if (pax.linkpath !== undefined) linkname = pax.linkpath
|
|
287
|
+
if (pax.size !== undefined) size = Number(pax.size)
|
|
288
|
+
if (pax.mtime !== undefined) mtime = Math.floor(Number(pax.mtime))
|
|
289
|
+
if (pax.uid !== undefined) uid = Number(pax.uid)
|
|
290
|
+
if (pax.gid !== undefined) gid = Number(pax.gid)
|
|
291
|
+
}
|
|
292
|
+
const xattrs = {}
|
|
293
|
+
if (pax) for (const [k, v] of Object.entries(pax)) if (k.startsWith('SCHILY.xattr.')) xattrs[k.slice(13)] = v
|
|
294
|
+
pax = gnuName = gnuLink = null
|
|
295
|
+
const hasData = type === '0' || type === '7' || (type !== '1' && type !== '2' && type !== '5' && size > 0)
|
|
296
|
+
yield { name, type, size, mode: parseNum(h, 100, 8), mtime, uid, gid, linkname, xattrs, offset: off }
|
|
297
|
+
if (hasData) off += Math.ceil(size / BLOCK) * BLOCK
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** scanTar over a Uint8Array. */
|
|
302
|
+
export function scanTarBytes(u8) {
|
|
303
|
+
return scanTar((o, n) => u8.subarray(o, o + n), u8.length)
|
|
304
|
+
}
|
package/src/host/ckpt-point.js
CHANGED
|
@@ -30,27 +30,44 @@ export const REQ_CKPT = VM_POINT_CKPT
|
|
|
30
30
|
|
|
31
31
|
/** Pages per chunk of the memory section (32 MiB). */
|
|
32
32
|
export const CHUNK_PAGES = 512
|
|
33
|
+
/** Pages per view while a memory is scanned (1 GiB). */
|
|
34
|
+
export const WINDOW_PAGES = 16384
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Non-zero pages of memory: {index: Uint32Array of page numbers, chunks: ArrayBuffer[] (pages
|
|
36
38
|
* back to back, CHUNK_PAGES each), pages (total), nonzero, scanMs, copyMs}. Reads the memory
|
|
37
39
|
* without atomics: the machine is frozen.
|
|
38
40
|
*/
|
|
39
|
-
export function scanMemory(memory, { now = () => performance.now(), chunkPages = CHUNK_PAGES, limitBytes = Infinity, verifyAbove = false } = {}) {
|
|
41
|
+
export function scanMemory(memory, { now = () => performance.now(), chunkPages = CHUNK_PAGES, limitBytes = Infinity, verifyAbove = false, skip = null } = {}) {
|
|
40
42
|
const buf = memory.buffer
|
|
41
43
|
// limitBytes: pages from here up are taken to be zero and are not read. Reading an untouched page of a shared
|
|
42
44
|
// memory commits it in WebKit (W34: a full walk of a 3968 MiB memory added about 2.2 GB of footprint), so a caller
|
|
43
45
|
// that knows where the guest's heap ends should say so; the page count of the checkpoint stays the memory's.
|
|
44
46
|
const total = buf.byteLength / PAGE
|
|
45
47
|
const pages = Math.min(total, Math.ceil(limitBytes / PAGE))
|
|
46
|
-
const w = new Int32Array(buf)
|
|
47
|
-
const u8 = new Uint8Array(buf)
|
|
48
48
|
const per = PAGE / 4
|
|
49
|
+
// views of at most WINDOW_PAGES pages: a guest RAM memory64 can be 16 GiB, past the length of one typed array view
|
|
50
|
+
let wLo = -1
|
|
51
|
+
let w = null
|
|
52
|
+
const words = p => {
|
|
53
|
+
const at = p - (p % WINDOW_PAGES)
|
|
54
|
+
if (at !== wLo) {
|
|
55
|
+
wLo = at
|
|
56
|
+
w = new Int32Array(buf, at * PAGE, Math.min(WINDOW_PAGES, total - at) * per)
|
|
57
|
+
}
|
|
58
|
+
return (p - at) * per
|
|
59
|
+
}
|
|
49
60
|
const t0 = now()
|
|
50
61
|
let idx = new Uint32Array(Math.min(pages, 1 << 14))
|
|
51
62
|
let n = 0
|
|
63
|
+
let skipped = 0
|
|
52
64
|
for (let p = 0; p < pages; p++) {
|
|
53
|
-
|
|
65
|
+
// a page the guest never wrote is zero: it is not read (reading it would make the host back it with memory)
|
|
66
|
+
if (skip && skip(p)) {
|
|
67
|
+
skipped++
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
const lo = words(p)
|
|
54
71
|
const hi = lo + per
|
|
55
72
|
let nz = false
|
|
56
73
|
for (let i = lo; i < hi; i += 8) {
|
|
@@ -72,7 +89,7 @@ export function scanMemory(memory, { now = () => performance.now(), chunkPages =
|
|
|
72
89
|
let aboveNonzero = 0
|
|
73
90
|
if (verifyAbove) {
|
|
74
91
|
for (let p = pages; p < total; p++) {
|
|
75
|
-
const lo = p
|
|
92
|
+
const lo = words(p)
|
|
76
93
|
for (let i = lo; i < lo + per; i += 8) {
|
|
77
94
|
if ((w[i] | w[i + 1] | w[i + 2] | w[i + 3] | w[i + 4] | w[i + 5] | w[i + 6] | w[i + 7]) !== 0) {
|
|
78
95
|
aboveNonzero++
|
|
@@ -86,10 +103,32 @@ export function scanMemory(memory, { now = () => performance.now(), chunkPages =
|
|
|
86
103
|
for (let k = 0; k < n; k += chunkPages) {
|
|
87
104
|
const m = Math.min(chunkPages, n - k)
|
|
88
105
|
const c = new Uint8Array(m * PAGE)
|
|
89
|
-
for (let j = 0; j < m; j++) c.set(
|
|
106
|
+
for (let j = 0; j < m; j++) c.set(new Uint8Array(buf, idx[k + j] * PAGE, PAGE), j * PAGE)
|
|
90
107
|
chunks.push(c.buffer)
|
|
91
108
|
}
|
|
92
|
-
return { index: idx.slice(0, n), chunks, pages: total, scannedPages: pages, aboveNonzero, nonzero: n, scanMs: t1 - t0, copyMs: now() - t1 }
|
|
109
|
+
return { index: idx.slice(0, n), chunks, pages: total, scannedPages: pages - skipped, skippedPages: skipped, aboveNonzero, nonzero: n, scanMs: t1 - t0, copyMs: now() - t1 }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The pages a checkpoint need not read, from vm.wasm's vm_touched_map (guest/vm/include/vm/ram.h): the RAM pages the guest never
|
|
114
|
+
* wrote. `desc` is the address of its six u64s: [bitmap address, bitmap pages, memory-0 offset of RAM (or of the flat layout's boot
|
|
115
|
+
* stand-in), its length, RAM bytes, flat]. Returns {mem0(p), ram(p)}: whether page p of memory 0, or of the RAM memory, is skipped.
|
|
116
|
+
*/
|
|
117
|
+
export function touchedSkips(memory, desc) {
|
|
118
|
+
const d = new BigUint64Array(memory.buffer, desc, 6)
|
|
119
|
+
const bits = new Uint8Array(memory.buffer, Number(d[0]), Number(d[1]) / 8)
|
|
120
|
+
const nbits = Number(d[1])
|
|
121
|
+
const lowOff = Number(d[2])
|
|
122
|
+
const lowLen = Number(d[3])
|
|
123
|
+
const flat = d[5] !== 0n
|
|
124
|
+
const unset = i => i >= nbits || !(bits[i >> 3] & (1 << (i & 7)))
|
|
125
|
+
const first = Math.ceil(lowOff / PAGE)
|
|
126
|
+
const end = Math.floor((lowOff + lowLen) / PAGE) // pages wholly inside the range
|
|
127
|
+
return {
|
|
128
|
+
// memory 0: a page inside guest RAM that was never written; with the flat layout the boot stand-in is never read again
|
|
129
|
+
mem0: p => p >= first && p < end && (flat || unset(p - lowOff / PAGE)),
|
|
130
|
+
ram: p => unset(p),
|
|
131
|
+
}
|
|
93
132
|
}
|
|
94
133
|
|
|
95
134
|
/**
|
|
@@ -97,7 +136,7 @@ export function scanMemory(memory, { now = () => performance.now(), chunkPages =
|
|
|
97
136
|
* message to the page; goTimeoutMs: how long vCPU 0 waits for the page (a page that is gone
|
|
98
137
|
* makes the point fail: the guest goes on, as it does for any failed point).
|
|
99
138
|
*/
|
|
100
|
-
export function createCkptImports({ memory, ctl, post, scanLimit = null, verifyBound = false, useHeapTop = false, heapTop = () => null, goTimeoutMs = 60_000, now = () => performance.timeOrigin + performance.now() }) {
|
|
139
|
+
export function createCkptImports({ memory, ram = null, touched = () => null, ctl, post, scanLimit = null, verifyBound = false, useHeapTop = false, heapTop = () => null, goTimeoutMs = 60_000, now = () => performance.timeOrigin + performance.now() }) {
|
|
101
140
|
return {
|
|
102
141
|
ctl_attach(wordAddr) {
|
|
103
142
|
Atomics.store(ctl, CTL_CK_HOSTREQ, wordAddr >>> 0)
|
|
@@ -124,7 +163,11 @@ export function createCkptImports({ memory, ctl, post, scanLimit = null, verifyB
|
|
|
124
163
|
// verifyBound found no non-zero page above it
|
|
125
164
|
const top = scanLimit ?? (useHeapTop ? heapTop() : null)
|
|
126
165
|
const limit = typeof top === 'number' && top > 0 ? Math.min(top, memory.buffer.byteLength) : Infinity
|
|
127
|
-
|
|
166
|
+
const desc = touched()
|
|
167
|
+
const skips = desc ? touchedSkips(memory, desc) : null
|
|
168
|
+
// RAM in memory 0 starts at a 64 KiB boundary (memory.grow), so a skipped page is all RAM
|
|
169
|
+
if (skips && (Number(new BigUint64Array(memory.buffer, desc, 6)[2]) % PAGE) !== 0) throw new Error('guest RAM is not page-aligned in memory 0')
|
|
170
|
+
cap = scanMemory(memory, { now, limitBytes: limit, verifyAbove: verifyBound, skip: skips ? skips.mem0 : null })
|
|
128
171
|
cap.limitBytes = Number.isFinite(limit) ? limit : null
|
|
129
172
|
// the live g_rsv {brk, end} of vm.wasm 50c6495e / 3aef93fc (linear address 4251160), next to what vm_heap_top() says
|
|
130
173
|
try {
|
|
@@ -133,13 +176,15 @@ export function createCkptImports({ memory, ctl, post, scanLimit = null, verifyB
|
|
|
133
176
|
cap.endLive = Number(g[1])
|
|
134
177
|
} catch {}
|
|
135
178
|
cap.heapTopRet = heapTop()
|
|
179
|
+
// the flat layout: guest RAM is a memory of its own (host/ram-mem.js), every page of it the guest's
|
|
180
|
+
if (ram) cap.ram = scanMemory(ram, { now, skip: skips ? skips.ram : null })
|
|
136
181
|
} catch (err) {
|
|
137
182
|
post({ t: 'ckpt-mem-error', message: String(err && err.message ? err.message : err) })
|
|
138
183
|
Atomics.wait(ctl, CTL_CK_GO, 0, goTimeoutMs)
|
|
139
184
|
Atomics.store(ctl, CTL_CK_GO, 0)
|
|
140
185
|
return -12
|
|
141
186
|
}
|
|
142
|
-
post({ t: 'ckpt-mem', index: cap.index, chunks: cap.chunks, pages: cap.pages, scannedPages: cap.scannedPages, aboveNonzero: cap.aboveNonzero, brkLive: cap.brkLive, endLive: cap.endLive, heapTopRet: cap.heapTopRet, limitBytes: cap.limitBytes, nonzero: cap.nonzero, scanMs: cap.scanMs, copyMs: cap.copyMs, doneAt: now() }, [cap.index.buffer, ...cap.chunks])
|
|
187
|
+
post({ t: 'ckpt-mem', index: cap.index, chunks: cap.chunks, pages: cap.pages, scannedPages: cap.scannedPages, skippedPages: cap.skippedPages, ramScannedPages: cap.ram?.scannedPages, ramSkippedPages: cap.ram?.skippedPages, aboveNonzero: cap.aboveNonzero, brkLive: cap.brkLive, endLive: cap.endLive, heapTopRet: cap.heapTopRet, limitBytes: cap.limitBytes, nonzero: cap.nonzero, scanMs: cap.scanMs, copyMs: cap.copyMs, ram: cap.ram ? { index: cap.ram.index, chunks: cap.ram.chunks, pages: cap.ram.pages, nonzero: cap.ram.nonzero, scanMs: cap.ram.scanMs, copyMs: cap.ram.copyMs } : null, doneAt: now() }, [cap.index.buffer, ...cap.chunks, ...(cap.ram ? [cap.ram.index.buffer, ...cap.ram.chunks] : [])])
|
|
143
188
|
const r = Atomics.wait(ctl, CTL_CK_GO, 0, goTimeoutMs)
|
|
144
189
|
const go = Atomics.load(ctl, CTL_CK_GO)
|
|
145
190
|
Atomics.store(ctl, CTL_CK_GO, 0)
|
package/src/host/ckpt.js
CHANGED
|
@@ -154,6 +154,11 @@ export function assembleCheckpoint(p) {
|
|
|
154
154
|
const sections = []
|
|
155
155
|
sections.push({ name: 'mem-index', chunks: [new Uint8Array(p.mem.index.buffer, p.mem.index.byteOffset, p.mem.index.byteLength)] })
|
|
156
156
|
sections.push({ name: 'mem', chunks: p.mem.chunks.map(c => new Uint8Array(c)) })
|
|
157
|
+
// the flat layout: the guest's RAM is a memory of its own, saved as its non-zero pages the same way
|
|
158
|
+
if (p.mem.ram) {
|
|
159
|
+
sections.push({ name: 'ram-index', chunks: [new Uint8Array(p.mem.ram.index.buffer, p.mem.ram.index.byteOffset, p.mem.ram.index.byteLength)] })
|
|
160
|
+
sections.push({ name: 'ram', chunks: p.mem.ram.chunks.map(c => new Uint8Array(c)) })
|
|
161
|
+
}
|
|
157
162
|
const header = {
|
|
158
163
|
...p.meta,
|
|
159
164
|
format: FORMAT_VERSION,
|
|
@@ -164,6 +169,10 @@ export function assembleCheckpoint(p) {
|
|
|
164
169
|
guestHdr: b64(p.hdr),
|
|
165
170
|
svcPorts: Object.keys(p.svc).map(Number),
|
|
166
171
|
}
|
|
172
|
+
if (p.mem.ram) {
|
|
173
|
+
header.ramPages = p.mem.ram.pages
|
|
174
|
+
header.ramNonzero = p.mem.ram.nonzero
|
|
175
|
+
}
|
|
167
176
|
if (p.jit) {
|
|
168
177
|
sections.push({ name: 'jit-recs', chunks: [new Uint8Array(p.jit.snap.recs.buffer, p.jit.snap.recs.byteOffset, p.jit.snap.recs.byteLength)] })
|
|
169
178
|
sections.push({ name: 'jit-log', chunks: concatChunks([p.jit.snap.log], 32 << 20) })
|
|
@@ -198,6 +207,7 @@ export function parseCheckpoint(ck) {
|
|
|
198
207
|
if (index[i] >= h.memPages) throw new CheckpointError('malformed', `page ${index[i]} is past the ${h.memPages} pages of memory`)
|
|
199
208
|
if (i && index[i] <= index[i - 1]) throw new CheckpointError('malformed', `the page list is not strictly increasing at entry ${i} (page ${index[i]} after ${index[i - 1]}): a duplicate or unordered page`)
|
|
200
209
|
}
|
|
210
|
+
const ram = h.ramPages ? pagesOf(get, 'ram', h.ramPages) : null
|
|
201
211
|
let jit = null
|
|
202
212
|
if (h.jit) {
|
|
203
213
|
const recs = concat(get('jit-recs'))
|
|
@@ -213,7 +223,26 @@ export function parseCheckpoint(ck) {
|
|
|
213
223
|
const svc = {}
|
|
214
224
|
for (const port of h.svcPorts || []) svc[port] = concat(get(`svc-${port}`))
|
|
215
225
|
const ctlBytes = concat(get('ctl'))
|
|
216
|
-
return { header: h, index, memChunks, jit, svc, ctl: new Uint8Array(ctlBytes), names: parseNames(ck) }
|
|
226
|
+
return { header: h, index, memChunks, ram, jit, svc, ctl: new Uint8Array(ctlBytes), names: parseNames(ck) }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The flat layout's RAM sections (`<name>-index` and `<name>`): {index, chunks}, checked as the memory's are. */
|
|
230
|
+
function pagesOf(get, name, total) {
|
|
231
|
+
if (!Number.isInteger(total) || total <= 0) throw new CheckpointError('malformed', `${name}Pages is ${total}`)
|
|
232
|
+
const idxBytes = concat(get(`${name}-index`))
|
|
233
|
+
if (idxBytes.length % 4) throw new CheckpointError('malformed', `${name}-index is not a list of 32-bit page numbers`)
|
|
234
|
+
const index = new Uint32Array(idxBytes.buffer.slice(idxBytes.byteOffset, idxBytes.byteOffset + idxBytes.length))
|
|
235
|
+
const chunks = get(name)
|
|
236
|
+
const have = chunks.reduce((n, c) => n + c.length, 0)
|
|
237
|
+
if (have !== index.length * PAGE) throw new CheckpointError('malformed', `the ${name} section holds ${have} bytes for ${index.length} pages`)
|
|
238
|
+
chunks.forEach((c, i) => {
|
|
239
|
+
if (c.length % PAGE) throw new CheckpointError('malformed', `${name} chunk ${i} is ${c.length} bytes, not a whole number of ${PAGE}-byte pages`)
|
|
240
|
+
})
|
|
241
|
+
for (let i = 0; i < index.length; i++) {
|
|
242
|
+
if (index[i] >= total) throw new CheckpointError('malformed', `${name} page ${index[i]} is past its ${total} pages`)
|
|
243
|
+
if (i && index[i] <= index[i - 1]) throw new CheckpointError('malformed', `the ${name} page list is not strictly increasing at entry ${i}`)
|
|
244
|
+
}
|
|
245
|
+
return { pages: total, index, chunks }
|
|
217
246
|
}
|
|
218
247
|
|
|
219
248
|
// no leading zeros: "198.18.07.1" and "198.18.7.1" are one address, and the map keeps each address once
|
|
@@ -266,7 +295,7 @@ export function checkVmKeyFits(h, vmKey) {
|
|
|
266
295
|
}
|
|
267
296
|
}
|
|
268
297
|
|
|
269
|
-
export function checkCheckpointFits(h, { vmKey, bootKey, bootDesc, maxPages, hasJit, fmaFused = true }) {
|
|
298
|
+
export function checkCheckpointFits(h, { vmKey, bootKey, bootDesc, maxPages, ramPages = 0, hasJit, fmaFused = true }) {
|
|
270
299
|
if (h.format !== FORMAT_VERSION) throw new CheckpointError('version', `checkpoint format ${h.format}; this build restores format ${FORMAT_VERSION}`)
|
|
271
300
|
if (h.vmKey !== vmKey) {
|
|
272
301
|
throw new CheckpointError('stale', `this checkpoint was taken with a different vm.wasm (${String(h.vmKey).slice(0, 12)}, this boot has ${String(vmKey).slice(0, 12)}): its memory image is the old emulator's and cannot be restored; take a new one`)
|
|
@@ -276,6 +305,10 @@ export function checkCheckpointFits(h, { vmKey, bootKey, bootDesc, maxPages, has
|
|
|
276
305
|
throw new CheckpointError('stale', `this checkpoint was taken by a boot with a different kernel, initramfs, command line, environment, image layers or bundle files than this one; restore it only into the same configuration${diff ? ` (differs: ${diff})` : ''}`)
|
|
277
306
|
}
|
|
278
307
|
if (h.memPages > maxPages) throw new CheckpointError('stale', `this checkpoint's memory is ${h.memPages} pages and this boot's maximum is ${maxPages}`)
|
|
308
|
+
// the flat layout: the guest's RAM is restored into a memory of exactly its size
|
|
309
|
+
if ((h.ramPages || 0) !== ramPages) {
|
|
310
|
+
throw new CheckpointError('stale', h.ramPages ? `this checkpoint holds ${h.ramPages / 16} MiB of guest RAM in a memory of its own and this boot ${ramPages ? `has ${ramPages / 16} MiB` : 'keeps its RAM in memory 0'}` : 'this checkpoint keeps the guest\'s RAM in memory 0 and this boot keeps it in a memory of its own')
|
|
311
|
+
}
|
|
279
312
|
if (h.jit && !hasJit) throw new CheckpointError('stale', 'this checkpoint carries JIT state and this boot has the JIT door off (jit: false)')
|
|
280
313
|
if (!h.jit && hasJit) throw new CheckpointError('stale', 'this checkpoint was taken without the JIT door and this boot has it on')
|
|
281
314
|
// D-539: its JIT blocks may use relaxed_madd, which this engine does not fuse or parse
|
package/src/host/ctl.js
CHANGED
|
@@ -719,6 +719,14 @@ export const DEFAULT_CAPS = [
|
|
|
719
719
|
'CAP_CHOWN', 'CAP_DAC_OVERRIDE', 'CAP_FSETID', 'CAP_FOWNER', 'CAP_MKNOD', 'CAP_NET_RAW', 'CAP_SETGID',
|
|
720
720
|
'CAP_SETUID', 'CAP_SETFCAP', 'CAP_SETPCAP', 'CAP_NET_BIND_SERVICE', 'CAP_SYS_CHROOT', 'CAP_KILL', 'CAP_AUDIT_WRITE',
|
|
721
721
|
]
|
|
722
|
+
/** Every capability, as internal/engine/ocispec.go allCaps (Linux 6.1: 0..40); what --privileged grants. */
|
|
723
|
+
export const ALL_CAPS = [
|
|
724
|
+
'CHOWN', 'DAC_OVERRIDE', 'DAC_READ_SEARCH', 'FOWNER', 'FSETID', 'KILL', 'SETGID', 'SETUID', 'SETPCAP', 'LINUX_IMMUTABLE',
|
|
725
|
+
'NET_BIND_SERVICE', 'NET_BROADCAST', 'NET_ADMIN', 'NET_RAW', 'IPC_LOCK', 'IPC_OWNER', 'SYS_MODULE', 'SYS_RAWIO', 'SYS_CHROOT',
|
|
726
|
+
'SYS_PTRACE', 'SYS_PACCT', 'SYS_ADMIN', 'SYS_BOOT', 'SYS_NICE', 'SYS_RESOURCE', 'SYS_TIME', 'SYS_TTY_CONFIG', 'MKNOD', 'LEASE',
|
|
727
|
+
'AUDIT_WRITE', 'AUDIT_CONTROL', 'SETFCAP', 'MAC_OVERRIDE', 'MAC_ADMIN', 'SYSLOG', 'WAKE_ALARM', 'BLOCK_SUSPEND', 'AUDIT_READ',
|
|
728
|
+
'PERFMON', 'BPF', 'CHECKPOINT_RESTORE',
|
|
729
|
+
].map(c => `CAP_${c}`)
|
|
722
730
|
const MASKED = ['/proc/acpi', '/proc/asound', '/proc/kcore', '/proc/keys', '/proc/latency_stats', '/proc/timer_list', '/proc/timer_stats', '/proc/sched_debug', '/proc/scsi', '/sys/firmware']
|
|
723
731
|
const READONLY = ['/proc/bus', '/proc/fs', '/proc/irq', '/proc/sys', '/proc/sysrq-trigger']
|
|
724
732
|
const ETC_FILES = ['hosts', 'resolv.conf', 'hostname']
|
|
@@ -751,6 +759,8 @@ function defaultMounts() {
|
|
|
751
759
|
* @param {{destination:string, files:Record<string,{data:Uint8Array|string, mode?:number}|string>, file?:string, readOnly?:boolean}[]} [o.mounts]
|
|
752
760
|
* each mount is a share vol<i> with these files; `file` binds only that file of the share
|
|
753
761
|
* @param {string} [o.arch] for imageconfig.json (default arm64)
|
|
762
|
+
* @param {boolean} [o.privileged] docker run --privileged as dockerd writes it: all capabilities, no masked or
|
|
763
|
+
* read-only paths, /sys read-write, every device allowed (default false; only builder machines use it)
|
|
754
764
|
* @param {boolean} [o.init] docker run --init: tini at /sbin/docker-init runs the command
|
|
755
765
|
* @param {boolean} [o.keepRunning] wmd's keep-running init at /sbin/docker-init: the container stays up
|
|
756
766
|
* after its command ends; args may be [] (nothing runs until ControlClient.startCommand). Send the
|
|
@@ -785,6 +795,13 @@ export function containerBundle(o) {
|
|
|
785
795
|
resources: { devices: [{ allow: false, access: 'rwm' }] },
|
|
786
796
|
},
|
|
787
797
|
}
|
|
798
|
+
if (o.privileged) {
|
|
799
|
+
spec.process.capabilities = { bounding: ALL_CAPS, effective: ALL_CAPS, inheritable: ALL_CAPS, permitted: ALL_CAPS }
|
|
800
|
+
spec.linux.maskedPaths = []
|
|
801
|
+
spec.linux.readonlyPaths = []
|
|
802
|
+
spec.linux.resources = { devices: [{ allow: true, access: 'rwm' }] }
|
|
803
|
+
for (const m of spec.mounts) if (m.destination === '/sys') m.options = m.options.filter(x => x !== 'ro')
|
|
804
|
+
}
|
|
788
805
|
if (o.init || o.keepRunning) {
|
|
789
806
|
// as dockerd binds its docker-init (internal/engine/ocispec.go): the guest's tini, or its own init for keep-running
|
|
790
807
|
spec.mounts.push({ destination: DOCKER_INIT, type: 'bind', source: o.keepRunning ? '/sbin/init' : '/sbin/tini', options: ['bind', 'ro'] })
|