Reputation: 22923
tldr; script using fs.readFileSync throws EACCESS when called using npm
, but not using node
On an ancient (2016) Docker image, I need to run a postinstall
NPM script involving Bower (bower install --allow-root
), but whenever I do, I get EACCES: permission denied, open '/root/.config/configstore/bower-github.json'
. I found out that doing npx bower
results in the same. Running npx bower
outside of Docker works fine.
Usually, I would easily have dealt with these issues, as they normally arise whenever someone has been executing a command using sudo
when they should not have. The fix for those issues is usually to either change the owner back to the current user or just run the bower command with sudo and --allow-root
(example 1, example 2).
This, however, is not one of these issues. I am already root!
The full error is like any of the similar issues:
root@eaa32456c249:/var/www/myproj# npx bower --allow-root
/var/www/myproj/node_modules/bower/lib/node_modules/configstore/index.js:54
throw err;
^
Error: EACCES: permission denied, open '/root/.config/configstore/bower-github.json'
You don't have access to this file.
at Object.openSync (node:fs:585:3)
at Object.readFileSync (node:fs:453:35)
at Configstore.get (/var/www/myproj/node_modules/bower/lib/node_modules/configstore/index.js:35:38)
at new Configstore (/var/www/myproj/node_modules/bower/lib/node_modules/configstore/index.js:28:48)
at readCachedConfig (/var/www/myproj/node_modules/bower/lib/config.js:19:23)
at defaultConfig (/var/www/myproj/node_modules/bower/lib/config.js:11:12)
at Object.<anonymous> (/var/www/myproj/node_modules/bower/lib/index.js:16:32)
at Module._compile (node:internal/modules/cjs/loader:1101:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
at Module.load (node:internal/modules/cjs/loader:981:32) {
errno: -13,
syscall: 'open',
code: 'EACCES',
path: '/root/.config/configstore/bower-github.json'
I cannot elevate my rights any further and adding --allow-root
does not do anything. I even inspected the module in question, seeing that the call that always failed was simply this:
readFileSync(this.path, 'utf8');
where this.path
was of course '/root/.config/configstore/bower-github.json'
.
I then wrote this small test module that does the same, and it ran without issues:
root@eaa32456c249:/var/www/myproj# cat test.js
const execSync = require('child_process').execSync;
const fs = require('fs');
const path = '/root/.config/configstore/bower-github.json';
console.log('exec whoami: ', execSync('whoami').toString());
try {
const result = execSync('ls -l ' + path, { encoding: 'utf8' });
console.log('exec ls -l: ', result);
} catch (err) {}
try {
const parsed = JSON.parse(fs.readFileSync(path, 'utf8', { encoding: 'utf8' }));
console.log('parsed: ', parsed);
} catch (err) {
console.error(err.message);
}
root@eaa32456c249:/var/www/myproj# node test.js
exec whoami: root
exec ls -l: -rw-r--r-- 1 root root 3 Dec 8 22:55 /root/.config/configstore/bower-github.json
parsed: {}
A mystery!
Upvotes: 6
Views: 6815
Reputation: 22923
tldr; NPM versions 7 and 8 will run as the owner of the root package directory. In other words, if you want to run as root, do chown root.root -R .
on the root dir of your project. Earlier and later versions lack this behaviour, so upgrading NPM will also avoid the issue.
Update NPM 9 reverts the change, meaning the above just applies to NPM 7 and 8. See PR.
After letting this simmer for a good while, I just figured I might check who the code was running as, so I opened the module in question (node_modules/configstore/index.js
) and added this to the lines preceding the call that failed:
const execSync = require('child_process').execSync;
console.log('exec `id`: ', execSync('id', { encoding: 'utf8' }));
console.log('exec `ls`: ', execSync('ls -l /root', { encoding: 'utf8' }));
Something fishy is indeed going on, as those lines printed:
exec `id`: uid=1000 gid=1000 groups=1000
ls: cannot open directory '/root': Permission denied
So somehow, running npx bower
as root
makes bower
run as a user with uid=1000? Running npm run postinstall
results in the same issue.
OK ... let's have a closer look at this. What if I run the bower
CLI module manually using node
?
$ node node_modules/.bin/bower --allow-root
root@eaa32456c249:/var/www/myproj# node node_modules/.bin/bower --allow-root
exec `id`: uid=0(root) gid=0(root) groups=0(root)
exec `ls`: total 0
It works - I am still root! So obviously both npx
and npm
is somehow doing something funky under the covers with regards to who the commands are running as!
root
and the owner of the CWDDigging deep to find the solution
After my discovery of the above fact, I did some googling and came across this NPM issue, while not actually having a direct explanation set me onto the the trail of Node trying to execute as the owner of the files. And for the first time I actually checked who the owner of the files were:
root@eaa32456c249:/var/www/myproj# ls -lh node_modules/bower/
total 72K
-rw-r--r-- 1 1000 1000 40K Oct 20 08:50 CHANGELOG.md
-rw-r--r-- 1 1000 1000 1.1K Oct 20 08:50 LICENSE
-rw-r--r-- 1 1000 1000 14K Oct 20 08:50 README.md
drwxr-xr-x 2 1000 1000 4.0K Oct 20 08:50 bin
drwxr-xr-x 9 1000 1000 4.0K Oct 20 08:50 lib
-rw-r--r-- 1 1000 1000 460 Oct 20 08:50 package.json
Simply doing chown root.root -R node_modules
did nothing, so I continued the search. I then read this article, which had this snippet:
If npm was invoked with root privileges, then it will change the uid to the user account or uid specified by the user config, which defaults to nobody. Set the unsafe-perm flag to run scripts with root privileges.
OK, let's try setting unsafe-perm
to true. No go, I was still running as uid=1000. I then ventured into the actual NPM library on the search for something relevant to uid
and found the answer in:
/usr/local/lib/node_modules/npm/node_modules/@npmcli/promise-spawn/index.js
For NPM 8, it uses this bit of code to determine who to run as:
const { uid, gid } = isRoot ? inferOwner.sync(cwd) : {}
and as the docs of infer-owner
module says:
Infer the owner of a path based on the owner of its nearest existing parent
Note the cwd
part. It does not look at node_modules
, but the current working directory! So I then did chown root.root -R .
on the root dir of the project and lo and behold, it worked!
This behavior is only for NPM versions 7 and 8. NPM versions < 7 (shipped with Node 12-14) work perfectly fine when running as root. So the quickest fix might simply be to use Node 14. Starting with NPM 9 the old behavior is back (probably shipped with Node 20).
Upvotes: 16
Reputation: 570
This is a very specific solution/scenario - you're using Docusaurus. This may happen if you set path: '/'
in docusaurus.config.js
. Set it to something else, maybe you are trying to set routeBasePath
and not path
.
Upvotes: 0