メモログ

💡 Personal notes about anything I'm interested in

npm script warns permission errors in the cache directory

(そんなことするかどうかは置いておいて)たとえばdocker-node-npm-scripti-permission-issue-demoをGithub codespaces上で起動して、npm ciして、npm run buildを実行すると、permission denied, scandir '/root/.npm/_logs' というエラーのWARNが発生する。

原因はnpm run buildの中身が npm run tsc になっているため。rootユーザーで別のユーザーが所有権を持つディレクトリで実行すると問題になる。

解決方法はDocker containerを実行するユーザーをnodeに変更するとか、npm configで、npm config set cache /tmpと設定するとか(export npm_config_cache=/tmpでもいける雰囲気)、所有権を調整すると解決する。解決方法は他にもいろいろあると思う。

再現手順はdocker-node-npm-scripti-permission-issue-demoのcodespacesのVS codeの中でターミナルを開いて、以下のような感じでDocker containerを起動して、npm ciとnpm run buildを実行するとエラーを再現することができる。動作環境はNode.js 16.18.0でnpmは8.19.2。たぶんNode.js 18.xでも発生する(npmのバージョンは変わらないので)。

docker build -f Dockerfile -t demo .
docker run -it --rm -v `pwd`:/current -w /current demo bash
root@71524f7f2bea:/current# npm ci
... (snip) ...
added 1 package, and audited 2 packages in 2s
found 0 vulnerabilities
... (snip) ...
root@71524f7f2bea:/current# npm run build

> docker-node-npm-scripti-permission-issue-demo@1.0.0 build
> npm run tsc

npm WARN logfile Error: EACCES: permission denied, scandir '/root/.npm/_logs'
npm WARN logfile  error cleaning log files [Error: EACCES: permission denied, scandir '/root/.npm/_logs'] {
npm WARN logfile   errno: -13,
npm WARN logfile   code: 'EACCES',
npm WARN logfile   syscall: 'scandir',
npm WARN logfile   path: '/root/.npm/_logs'
npm WARN logfile }

> docker-node-npm-scripti-permission-issue-demo@1.0.0 tsc
> tsc

npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /root/.npm/_cacache/tmp
npm ERR! errno -13
npm ERR!
npm ERR! Your cache folder contains root-owned files, due to a bug in
npm ERR! previous versions of npm which has since been addressed.
npm ERR!
npm ERR! To permanently fix this problem, please run:
npm ERR!   sudo chown -R 1000:0 "/root/.npm"

npm ERR! Log files were not written due to an error writing to the directory: /root/.npm/_logs
npm ERR! You can rerun the command with `--loglevel=verbose` to see the logs in your terminal

以下は詳細。

npmのscriptは@npmcli/run-scriptが実行スクリプトで/usr/local/lib/node_modules/npm/node_modules/@npmcliにある。これのバージョンは4.2.1なので、run-script.jsが実際の処理になる。これをrun-script-pkg.js、@npmcli/promise-spawnと辿っていくと、promise-spawn/lib/index.jsに到達する。

ここで実行しているinfer-ownerは、スクリプトを実行するディレクトリの所有権を推測して、ディレクトリのuid、gidを返してくれる。run-script.jsではこれを利用してchild_process.spawnをuid, gid付きで実行する。

codespacesのディレクトリはcodespace(uid:1000)がオーナーで、Docker containerのユーザーはデフォルトではroot(uid:0)なので、codespaceが所有するディレクトリでrootユーザーが処理を実行する形になる。

具体的にはrootユーザーがnpm scriptが実行したとき、uid:1000, gid:0でchild_process.spawnを実行する。ここまでは特に問題ない。

npm scriptの中でnpm runを記述していると、run-scriptの中でrun-scriptを実行する状態になる(npm scriptもnpm runもrun-scriptで実行するので)。

つまり、root(uid:0)ユーザーが、uid:1000, gid:0で実行した処理の中で、uid:1000, gid:0で処理を実行する。するとnpm cacheのディレクトリ、/root/.npm に対してログを出力しようとしてパーミッションエラーとなる雰囲気。なぜrootユーザーのキャッシュディレクトリを参照するとエラーになるかは理解できていない。run-scriptを実行してるユーザーのuidでファイルアクセスしようとするためなのだろうか。

なので、Docker containerの実行ユーザーをrootではないユーザーに変更するか、rootユーザーを維持したければ、スクリプト実行前にnpm config set cache /tmpみたいな感じで、キャッシュディレクトリを適当な場所に変更しておけば、問題は発生しない。


ところで、このinfer-ownerによる処理、feat: remove infer-ownerのプルリクエストで削除されることになった。@npmcli/promise-spawnの5.0以降は同じ問題は発生しなくなる。npm cliでは9.0.1で5.0がインストールされるようになる。Node.jsではdeps: upgrade npm to 9.1.0にて進行中のよう。

しかしながら、run-scriptの処理は(npm install / 詳細は未確認)でも利用されている。つまり今までは、npm installを実行するユーザーが誰であれ、インストール先のディレクトリのオーナーでインストールしてくれるということであり、インストールする実行ユーザーを気にしないで済んでいた。npm v9.0.1以降はそうはいかなくなる。

たとえば、Docker container中で、rootユーザーでnpm ciしたりして、node_modulesの中だけ所有権がrootになってしまったり、npm scriptで生成したファイルの所有権がrootユーザーになったり(生成したファイルを他ユーザーで削除しようとしてエラーになったり)などという問題が発生する可能性がある。

なのでnpm install、npm scriptの実行するユーザーは今のうちに見直しておくのが良いかもしれない。

という長いメモであった。