Automatically allocate one uid per session

This commit is contained in:
Radon Rosborough 2020-06-22 12:35:38 -06:00
parent e9ff1d92d3
commit afad563d56
10 changed files with 291 additions and 27 deletions

View File

@ -38,8 +38,7 @@ EXPOSE 6120
ENTRYPOINT ["/usr/local/bin/pid1.bash"] ENTRYPOINT ["/usr/local/bin/pid1.bash"]
COPY scripts/pid1.bash /usr/local/bin/ COPY scripts/pid1.bash /usr/local/bin/
CMD ["yarn", "run", "server"]
RUN sudo deluser docker sudo
RUN mkdir /tmp/riju RUN mkdir /tmp/riju
COPY --chown=docker:docker package.json yarn.lock /tmp/riju/ COPY --chown=docker:docker package.json yarn.lock /tmp/riju/
@ -49,8 +48,10 @@ COPY --chown=docker:docker frontend /tmp/riju/frontend
RUN cd /tmp/riju && yarn run frontend RUN cd /tmp/riju && yarn run frontend
COPY --chown=docker:docker backend /tmp/riju/backend COPY --chown=docker:docker backend /tmp/riju/backend
RUN cd /tmp/riju && yarn run backend RUN cd /tmp/riju && yarn run backend
COPY --chown=docker:docker system /tmp/riju/system
RUN cd /tmp/riju && RIJU_PRIVILEGED=1 yarn run system
COPY --chown=docker:docker . /home/docker/src COPY --chown=docker:docker . /home/docker/src
RUN cp -R /tmp/riju/* /home/docker/src/ && rm -rf /tmp/riju RUN cp -R /tmp/riju/* /home/docker/src/ && rm -rf /tmp/riju
WORKDIR /home/docker/src WORKDIR /home/docker/src
CMD ["yarn", "run", "server"] RUN sudo deluser docker sudo

View File

@ -20,6 +20,7 @@ usual to install dependencies. For production, it's:
$ yarn backend $ yarn backend
$ yarn frontend $ yarn frontend
$ yarn system
$ yarn server $ yarn server
For development with file watching and automatic server rebooting and For development with file watching and automatic server rebooting and
@ -27,15 +28,18 @@ all that, it's:
$ yarn backend-dev $ yarn backend-dev
$ yarn frontend-dev $ yarn frontend-dev
$ yarn system-dev
$ yarn server-dev $ yarn server-dev
The webserver listens on `localhost:6119`. Now, although the server The webserver listens on `localhost:6119`. Now, although the server
itself will work, the only languages that will work are the ones that itself will work, the only languages that will work are the ones that
happen to be installed on your machine. (I'm sure you can find a few happen to be installed on your machine. (I'm sure you can find a few
that are already.) If you want to test with *all* the languages (or that are already.) Also, sandboxing using UNIX filesystem permissions
you're working on adding a new language), then you need to use Docker. will be disabled, because that requires root privileges. If you want
Running the app is exactly the same as before, you just have to jump to test with *all* the languages plus sandboxing (or you're working on
into the container first: adding a new language), then you need to use Docker. Running the app
is exactly the same as before, you just have to jump into the
container first:
$ make docker $ make docker

View File

@ -1,64 +1,88 @@
import * as fs from "fs"; import * as fs from "fs";
import * as mkdirp from "mkdirp";
import * as pty from "node-pty";
import { IPty } from "node-pty";
import * as path from "path"; import * as path from "path";
import * as tmp from "tmp";
import * as WebSocket from "ws"; import * as WebSocket from "ws";
import * as mkdirp from "mkdirp";
import * as nodeCleanup from "node-cleanup";
import * as pty from "node-pty";
import { IPty } from "node-pty";
import * as tmp from "tmp";
import { v4 as getUUID } from "uuid";
import { LangConfig, langs } from "./langs"; import { LangConfig, langs } from "./langs";
import { borrowUser } from "./users";
export class Session { export class Session {
id: string;
code: string; code: string;
config: LangConfig; config: LangConfig;
term: { pty: IPty | null; live: boolean }; term: { pty: IPty | null; live: boolean };
ws: WebSocket; ws: WebSocket;
tmpdir: string | null; tmpdir: string | null;
tmpdirCleanup: (() => void) | null; tmpdirCleanup: (() => void) | null;
uid: number | null;
uidCleanup: (() => Promise<void>) | null;
log = (msg: string) => console.log(`[${this.id}] ${msg}`);
constructor(ws: WebSocket, lang: string) { constructor(ws: WebSocket, lang: string) {
this.id = getUUID();
this.log(`Creating session, language ${lang}`);
this.ws = ws; this.ws = ws;
this.config = langs[lang]; this.config = langs[lang];
this.term = { pty: null, live: false }; this.term = { pty: null, live: false };
this.code = ""; this.code = "";
this.tmpdir = null; this.tmpdir = null;
this.tmpdirCleanup = null; this.tmpdirCleanup = null;
this.uid = null;
this.uidCleanup = null;
ws.on("message", this.handleClientMessage); ws.on("message", this.handleClientMessage);
ws.on("close", this.cleanup); ws.on("close", () =>
this.run().catch(console.error); this.cleanup().catch((err) =>
this.log(`Error during session cleanup: ${err}`)
)
);
nodeCleanup();
this.run().catch((err) => this.log(`Error while running: ${err}`));
} }
handleClientMessage = (event: string) => { handleClientMessage = (event: string) => {
let msg: any; let msg: any;
try { try {
msg = JSON.parse(event); msg = JSON.parse(event);
} catch (err) { } catch (err) {
console.error(`failed to parse client message: ${msg}`); this.log(`Failed to parse client message: ${msg}`);
return; return;
} }
switch (msg?.event) { switch (msg?.event) {
case "terminalInput": case "terminalInput":
if (!this.term) { if (!this.term) {
console.error(`terminalInput: no terminal`); this.log(`Got terminal input before pty was started`);
} else if (typeof msg.input !== "string") { } else if (typeof msg.input !== "string") {
console.error(`terminalInput: missing or malformed input field`); this.log(`Got malformed terminal input message`);
} else { } else {
this.term.pty!.write(msg.input); this.term.pty!.write(msg.input);
} }
break; break;
case "runCode": case "runCode":
if (typeof msg.code !== "string") { if (typeof msg.code !== "string") {
console.error(`runCode: missing or malformed code field`); this.log(`Got malformed run message`);
} else { } else {
this.code = msg.code; this.code = msg.code;
this.run(); this.run();
} }
break; break;
default: default:
console.error(`unknown client message type: ${msg.event}`); this.log(`Got unknown message type: ${msg.event}`);
break; break;
} }
}; };
run = async () => { run = async () => {
if (this.uid === null) {
({ uid: this.uid, cleanup: this.uidCleanup } = await borrowUser(
this.log
));
}
this.log(`Borrowed uid ${this.uid}`);
const { name, repl, main, suffix, compile, run, hacks } = this.config; const { name, repl, main, suffix, compile, run, hacks } = this.config;
if (this.term.pty) { if (this.term.pty) {
this.term.pty.kill(); this.term.pty.kill();
@ -72,13 +96,16 @@ export class Session {
if (this.tmpdir == null) { if (this.tmpdir == null) {
({ path: this.tmpdir, cleanup: this.tmpdirCleanup } = await new Promise( ({ path: this.tmpdir, cleanup: this.tmpdirCleanup } = await new Promise(
(resolve, reject) => (resolve, reject) =>
tmp.dir({ unsafeCleanup: true }, (err, path, cleanup) => { tmp.dir(
if (err) { { unsafeCleanup: true, dir: "riju" },
reject(err); (err, path, cleanup) => {
} else { if (err) {
resolve({ path, cleanup }); reject(err);
} else {
resolve({ path, cleanup });
}
} }
}) )
)); ));
} }
let cmdline: string; let cmdline: string;
@ -157,9 +184,13 @@ export class Session {
} }
}); });
}; };
cleanup = () => { cleanup = async () => {
this.log(`Cleaning up session`);
if (this.tmpdirCleanup) { if (this.tmpdirCleanup) {
this.tmpdirCleanup(); this.tmpdirCleanup();
} }
if (this.uidCleanup) {
await this.uidCleanup();
}
}; };
} }

100
backend/src/users.ts Normal file
View File

@ -0,0 +1,100 @@
import { spawn } from "child_process";
import * as fs from "fs";
import * as process from "process";
import * as AsyncLock from "async-lock";
import * as _ from "lodash";
import * as parsePasswd from "parse-passwd";
// Keep in sync with system/src/riju-system-privileged.c
const MIN_UID = 2000;
const MAX_UID = 65000;
const PRIVILEGED = process.env.RIJU_PRIVILEGED ? true : false;
const CUR_UID = parseInt(process.env.UID || "") || null;
let availIds: number[] | null = null;
let nextId: number | null = null;
let lock = new AsyncLock();
async function readExistingUsers(log: (msg: string) => void) {
availIds = parsePasswd(
await new Promise((resolve, reject) =>
fs.readFile("/etc/passwd", "utf-8", (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
})
)
)
.filter(({ username }) => username.startsWith("riju_user"))
.map(({ uid }) => parseInt(uid))
.filter((uid) => !isNaN(uid) && uid >= MIN_UID && uid < MAX_UID);
nextId = (_.max(availIds) || MIN_UID - 1) + 1;
log(`Found ${availIds.length} existing users, next ID is ${nextId}`);
}
async function createUser(log: (msg: string) => void): Promise<number> {
if (nextId! >= MAX_UID) {
throw new Error("too many users");
}
return await new Promise((resolve, reject) => {
const uid = nextId!;
const useradd = spawn("system/out/riju-system-privileged", [
"useradd",
`${uid}`,
]);
let output = "";
useradd.stdout.on("data", (data) => {
output += `${data}`;
});
useradd.stderr.on("data", (data) => {
output += `${data}`;
});
useradd.on("close", (code) => {
output = output.trim();
if (output) {
log("Output from useradd:\n" + output);
}
if (code === 0) {
log(`Created new user with ID ${uid}`);
nextId! += 1;
resolve(uid);
} else {
reject(`useradd failed with error code ${code}`);
}
});
});
}
export async function borrowUser(log: (msg: string) => void) {
if (!PRIVILEGED) {
if (CUR_UID === null) {
throw new Error("unable to determine current UID");
} else {
return { uid: CUR_UID, cleanup: async () => {} };
}
} else {
return await lock.acquire("key", async () => {
if (availIds === null || nextId === null) {
await readExistingUsers(log);
}
let uid: number;
if (availIds!.length > 0) {
uid = availIds!.pop()!;
} else {
uid = await createUser(log);
}
return {
uid,
cleanup: async () => {
await lock.acquire("key", () => {
availIds!.push(uid);
});
},
};
});
}
}

View File

@ -5,12 +5,16 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@types/app-root-path": "^1.2.4", "@types/app-root-path": "^1.2.4",
"@types/async-lock": "^1.1.2",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/express-ws": "^3.0.0", "@types/express-ws": "^3.0.0",
"@types/lodash": "^4.14.155", "@types/lodash": "^4.14.155",
"@types/mkdirp": "^1.0.1", "@types/mkdirp": "^1.0.1",
"@types/parse-passwd": "^1.0.0",
"@types/tmp": "^0.2.0", "@types/tmp": "^0.2.0",
"@types/uuid": "^8.0.0",
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"async-lock": "^1.2.4",
"css-loader": "^3.5.3", "css-loader": "^3.5.3",
"ejs": "^3.1.3", "ejs": "^3.1.3",
"express": "^4.17.1", "express": "^4.17.1",
@ -19,11 +23,14 @@
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"monaco-editor": "^0.20.0", "monaco-editor": "^0.20.0",
"node-cleanup": "^2.1.2",
"node-pty": "^0.9.0", "node-pty": "^0.9.0",
"parse-passwd": "^1.0.0",
"style-loader": "^1.2.1", "style-loader": "^1.2.1",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"ts-loader": "^7.0.5", "ts-loader": "^7.0.5",
"typescript": "^3.9.5", "typescript": "^3.9.5",
"uuid": "^8.1.0",
"webpack": "^4.43.0", "webpack": "^4.43.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
"xterm": "^4.6.0", "xterm": "^4.6.0",
@ -34,7 +41,9 @@
"backend-dev": "tsc --watch", "backend-dev": "tsc --watch",
"frontend": "webpack --production", "frontend": "webpack --production",
"frontend-dev": "webpack --development --watch", "frontend-dev": "webpack --development --watch",
"server": "node backend/out/server.js", "server": "scripts/setup.bash && node backend/out/server.js",
"server-dev": "watchexec -w backend/out -r -n node backend/out/server.js" "server-dev": "watchexec -w backend/out -r 'scripts/setup.bash && node backend/out/server.js'",
"system": "scripts/compile-system.bash",
"system-dev": "watchexec -w system/src -n scripts/compile-system.bash"
} }
} }

26
scripts/compile-system.bash Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -e
set -o pipefail
if [[ ! -d system/src ]]; then
echo "compile-system.bash: no system/src directory" >&2
exit 1
fi
function verbosely {
echo "$@"
"$@"
}
mkdir -p system/out
rm -f system/out/*
for src in system/src/*.c; do
out="${src/src/out}"
out="${out/.c}"
verbosely clang -Wall -Wextra -Werror -std=c11 "${src}" -o "${out}"
if [[ "${out}" == *-privileged && -n "${RIJU_PRIVILEGED}" ]]; then
sudo chown root:docker "${out}"
sudo chmod a=,g=rx,u=rwxs "${out}"
fi
done

View File

@ -8,6 +8,7 @@ export LC_ALL=C.UTF-8
export SHELL="$(which bash)" export SHELL="$(which bash)"
export HOST=0.0.0.0 export HOST=0.0.0.0
export RIJU_PRIVILEGED=yes
if [[ -d /home/docker/src ]]; then if [[ -d /home/docker/src ]]; then
cd /home/docker/src cd /home/docker/src

7
scripts/setup.bash Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
set -o pipefail
mkdir -p /tmp/riju
rm -rf /tmp/riju/*

View File

@ -0,0 +1,55 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// Keep in sync with backend/src/users.ts
const int MIN_UID = 2000;
const int MAX_UID = 65000;
void die(const char *msg)
{
fprintf(stderr, "%s\n", msg);
exit(1);
}
void die_with_usage()
{
die("usage:\n"
" riju-system-privileged useradd UID");
}
void useradd(int uid)
{
char *cmdline;
if (asprintf(&cmdline, "useradd -M -N -l -r -u %1$d riju_user%1$d", uid) < 0) {
die("asprintf failed");
}
int status = system(cmdline);
if (status) {
die("useradd failed");
}
}
int main(int argc, char **argv)
{
setuid(0);
if (argc < 2)
die_with_usage();
if (!strcmp(argv[1], "useradd")) {
if (argc != 3)
die_with_usage();
char *endptr;
long uid = strtol(argv[2], &endptr, 10);
if (!argv[2] || *endptr) {
die("uid must be an integer");
}
if (uid < MIN_UID || uid >= MAX_UID) {
die("uid is out of range");
}
useradd(uid);
return 0;
}
die_with_usage();
}

View File

@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/@types/app-root-path/-/app-root-path-1.2.4.tgz#a78b703282b32ac54de768f5512ecc3569919dc7" resolved "https://registry.yarnpkg.com/@types/app-root-path/-/app-root-path-1.2.4.tgz#a78b703282b32ac54de768f5512ecc3569919dc7"
integrity sha1-p4twMoKzKsVN52j1US7MNWmRncc= integrity sha1-p4twMoKzKsVN52j1US7MNWmRncc=
"@types/async-lock@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.1.2.tgz#cbc26a34b11b83b28f7783a843c393b443ef8bef"
integrity sha512-j9n4bb6RhgFIydBe0+kpjnBPYumDaDyU8zvbWykyVMkku+c2CSu31MZkLeaBfqIwU+XCxlDpYDfyMQRkM0AkeQ==
"@types/body-parser@*": "@types/body-parser@*":
version "1.19.0" version "1.19.0"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
@ -77,6 +82,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3"
integrity sha512-lCvvI24L21ZVeIiyIUHZ5Oflv1hhHQ5E1S25IRlKIXaRkVgmXpJMI3wUJkmym2bTbCe+WoIibQnMVAU3FguaOg== integrity sha512-lCvvI24L21ZVeIiyIUHZ5Oflv1hhHQ5E1S25IRlKIXaRkVgmXpJMI3wUJkmym2bTbCe+WoIibQnMVAU3FguaOg==
"@types/parse-passwd@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-passwd/-/parse-passwd-1.0.0.tgz#72fc50feb14d484547f40ba7da54dfcfc711dcf1"
integrity sha512-+kETlH3XJMQKUpJ4dYfU4MlfYqyjKwsOC9PLJcGiUhPcHrNEgvcEbllSJ5HlsvflDLswTtyDoKJEUcCQg2l9PA==
"@types/qs@*": "@types/qs@*":
version "6.9.3" version "6.9.3"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.3.tgz#b755a0934564a200d3efdf88546ec93c369abd03" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.3.tgz#b755a0934564a200d3efdf88546ec93c369abd03"
@ -100,6 +110,11 @@
resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.0.tgz#e3f52b4d7397eaa9193592ef3fdd44dc0af4298c" resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.0.tgz#e3f52b4d7397eaa9193592ef3fdd44dc0af4298c"
integrity sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ== integrity sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ==
"@types/uuid@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0"
integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw==
"@types/ws@*": "@types/ws@*":
version "7.2.5" version "7.2.5"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.5.tgz#513f28b04a1ea1aa9dc2cad3f26e8e37c88aae49" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.5.tgz#513f28b04a1ea1aa9dc2cad3f26e8e37c88aae49"
@ -390,6 +405,11 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
async-lock@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.4.tgz#80d0d612383045dd0c30eb5aad08510c1397cb91"
integrity sha512-UBQJC2pbeyGutIfYmErGc9RaJYnpZ1FHaxuKwb0ahvGiiCkPUf3p67Io+YLPmmv3RHY+mF6JEtNW8FlHsraAaA==
async@0.9.x: async@0.9.x:
version "0.9.2" version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
@ -2190,6 +2210,11 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-cleanup@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
integrity sha1-esGavSl+Caf3KnFUXZUbUX5N3iw=
node-libs-browser@^2.2.1: node-libs-browser@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
@ -3299,6 +3324,11 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
v8-compile-cache@2.0.3: v8-compile-cache@2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"