diff --git a/Dockerfile b/Dockerfile index 80fa167..d0a2a1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,13 +60,14 @@ COPY --from=build /src/container/progress.mjs ./container/progress.mjs RUN mkdir -p /app/release \ && chmod 0555 /app/runtime/wasm-oj-compiler /app/runtime/wasm-oj-runner \ && chmod 0444 /app/container/server.mjs /app/container/identity.mjs /app/container/generate-identity.mjs /app/container/tree-digest.mjs /app/container/submission-result.mjs /app/container/progress.mjs \ + && chmod -R a+rX /app \ && chmod -R a-w /app \ && chmod u+w /app/release \ && WASM_OJ_RELEASE_ID="$WASM_OJ_RELEASE_ID" \ WASM_OJ_GIT_COMMIT="$WASM_OJ_GIT_COMMIT" \ node /app/container/generate-identity.mjs \ && chmod a-w /app/release /app/release/container-identity.json \ - && node -e "import('/app/container/identity.mjs').then(async m => { await m.loadEmbeddedContainerIdentity(); })" \ + && runuser -u wasmoj -- node --input-type=module -e "const dependencies = ['@wasm-oj/core', '@wasm-oj/server', '@wasm-oj/toolchain-clang', '@wasm-oj/toolchain-go', '@wasm-oj/toolchain-javascript', '@wasm-oj/toolchain-python', '@wasm-oj/toolchain-rust']; await Promise.all(dependencies.map((dependency) => import(dependency))); const { loadEmbeddedContainerIdentity } = await import('/app/container/identity.mjs'); await loadEmbeddedContainerIdentity();" \ && runuser -u wasmoj -- node -e "const fs=require('node:fs/promises');const targets=['/app/release/container-identity.json','/app/runtime/wasm-oj-compiler','/app/runtime/wasm-oj-runner'];Promise.all(targets.flatMap((p)=>[fs.access(p,2).then(()=>{throw new Error('wasmoj can write '+p)},()=>{}),fs.unlink(p).then(()=>{throw new Error('wasmoj can delete '+p)},(error)=>{if(!['EACCES','EPERM'].includes(error.code))throw error})])).catch((error)=>{console.error(error);process.exit(1)})" USER wasmoj ENV NODE_ENV=production diff --git a/scripts/container-build-context.test.mjs b/scripts/container-build-context.test.mjs index c56a60c..419e388 100644 --- a/scripts/container-build-context.test.mjs +++ b/scripts/container-build-context.test.mjs @@ -41,3 +41,31 @@ test("judge Dockerfile fails closed if generated state still enters the context" assert.match(audit, /-name '\.wasm-oj-build-\*'/u); assert.match(audit, /-lname '\*\/tmp\/\*'/u); }); + +test("judge image exposes its immutable runtime graph to wasmoj before committing the image", async () => { + const dockerfile = await readFile(new URL("../Dockerfile", import.meta.url), "utf8"); + const grantReadIndex = dockerfile.indexOf("chmod -R a+rX /app"); + const removeWriteIndex = dockerfile.indexOf("chmod -R a-w /app"); + const runtimeSmokeIndex = dockerfile.indexOf("runuser -u wasmoj -- node --input-type=module"); + const finalUserIndex = dockerfile.indexOf("USER wasmoj"); + + assert.ok(grantReadIndex >= 0, "the copied runtime tree must be readable and traversable by wasmoj"); + assert.ok(removeWriteIndex > grantReadIndex, "read/traverse repair must happen before the immutable write fence"); + assert.ok(runtimeSmokeIndex > removeWriteIndex, "runtime imports must execute after permission hardening"); + assert.ok(finalUserIndex > runtimeSmokeIndex, "runtime imports must execute during the image build"); + + const runtimeSmoke = dockerfile.slice(runtimeSmokeIndex, finalUserIndex); + for (const packageName of [ + "@wasm-oj/core", + "@wasm-oj/server", + "@wasm-oj/toolchain-clang", + "@wasm-oj/toolchain-go", + "@wasm-oj/toolchain-javascript", + "@wasm-oj/toolchain-python", + "@wasm-oj/toolchain-rust", + ]) { + assert.ok(runtimeSmoke.includes(`'${packageName}'`), `${packageName} must be imported as wasmoj`); + } + assert.match(runtimeSmoke, /loadEmbeddedContainerIdentity\(\)/u); + assert.doesNotMatch(runtimeSmoke, /container\/server\.mjs/u); +});