Node snap: issues with exec of npm, node, yarn

I’m using the node snap sudo snap install node --classic and I’m encountering a bad issue when trying to exec (or spawn) npm, node or yarn, which are provided in the snap.

The issue is that the output of the exec (or spawn) is empty for npm and node and it even fails with yarn. I think it might be a permission issue due to snap, but I couldn’t find anything in the logs.

About npm and yarn, I installed them as global packages under /home/fabio/.npm-global (See this to learn how to do it)

If you want to test it, please try running the following script on your machine:

const { exec } = require('child_process');

// Test
exec('echo "exec test is running"', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec test error: ${error}`);
    return;
  }
  console.log(`exec test stdout: ${stdout}`);
  console.error(`exec test stderr: ${stderr}`);
});

// Node
exec('node -v', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec node error: ${error}`);
    return;
  }
  console.log(`exec node stdout: ${stdout}`);
  console.error(`exec node stderr: ${stderr}`);
});

// NPM
exec('npm --version', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec npm error: ${error}`);
    return;
  }
  console.log(`exec npm stdout: ${stdout}`);
  console.error(`exec npm stderr: ${stderr}`);
});

// Yarn
exec('yarn --version', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec yarn error: ${error}`);
    return;
  }
  console.log(`exec yarn stdout: ${stdout}`);
  console.error(`exec yarn stderr: ${stderr}`);
});

// -------------------------------------------------------

const { spawn } = require('child_process');

// Test
const testSpawn = spawn('echo', ['"test spawn is running"']);
testSpawn.stdout.on('data', (data) => {
  console.log(`spawn test stdout: ${data}`);
});
testSpawn.stderr.on('data', (data) => {
  console.error(`spawn test stderr: ${data}`);
});
testSpawn.on('close', (code) => {
  console.log(`spawn test child process exited with code ${code}`);
});

// Node
const nodeSpawn = spawn('node', ['-v']);
nodeSpawn.stdout.on('data', (data) => {
  console.log(`spawn node stdout: ${data}`);
});
nodeSpawn.stderr.on('data', (data) => {
  console.error(`spawn node stderr: ${data}`);
});
nodeSpawn.on('close', (code) => {
  console.log(`spawn node child process exited with code ${code}`);
});

// NPM
const npmSpawn = spawn('npm', ['--version']);
npmSpawn.stdout.on('data', (data) => {
  console.log(`spawn npm stdout: ${data}`);
});
npmSpawn.stderr.on('data', (data) => {
  console.error(`spawn npm stderr: ${data}`);
});
npmSpawn.on('close', (code) => {
  console.log(`spawn npm child process exited with code ${code}`);
});

// Yarn
const yarnSpawn = spawn('yarn', ['--version']);
yarnSpawn.stdout.on('data', (data) => {
  console.log(`spawn yarn stdout: ${data}`);
});
yarnSpawn.stderr.on('data', (data) => {
  console.error(`spawn yarn stderr: ${data}`);
});
yarnSpawn.on('close', (code) => {
  console.log(`spawn yarn child process exited with code ${code}`);
});

And save it as index.js.

If it works correctly you should get something like this:

$ node index.js 
spawn test stdout: "test spawn is running"

spawn test child process exited with code 0
exec test stdout: exec test is running

exec test stderr: 
exec node stdout: v14.16.0
exec node stderr: 
spawn node stdout: v14.16.0
spawn node child process exited with code 0
spawn yarn stdout: 1.22.5
spawn yarn child process exited with code 0
exec yarn stdout: 1.22.5
exec yarn stderr: 

exec npm stdout: 7.7.5
exec npm stderr: 
spawn npm stdout: 7.7.5
spawn npm child process exited with code 0

If it doesn’t work (like for me), you would see something like this instead:

$ node index.js
spawn test stdout: "test spawn is running"

spawn test child process exited with code 0
exec test stdout: exec test is running

exec test stderr: 
exec node stdout: 
exec node stderr: 
spawn node child process exited with code 0
spawn yarn child process exited with code 1
exec yarn error: Error: Command failed: yarn --version

exec npm stdout: 
exec npm stderr: 
spawn npm child process exited with code 0

I opened a bug report on node, but I’m writing here as well as I think the issue might lie in some snap mechanic.

May it be https://bugs.launchpad.net/ubuntu/+source/snapd/+bug/1849753 ? Do you see any AppArmor denials when executing the script?

$ node index.js 
spawn test stdout: "test spawn is running"

spawn test child process exited with code 0
exec test stdout: exec test is running

exec test stderr: 
exec node stdout: 
exec node stderr: 
spawn node child process exited with code 0
spawn yarn child process exited with code 1
exec yarn error: Error: Command failed: yarn --version

exec npm stdout: 
exec npm stderr: 
spawn npm child process exited with code 0
$ dmesg
[322233.359233] audit: type=1400 audit(1617099391.781:6570): apparmor="DENIED" operation="file_inherit" profile="/snap/core/10908/usr/lib/snapd/snap-confine" name="/apparmor/.null" pid=1100258 comm="snap-confine" requested_mask="wr" denied_mask="wr" fsuid=1000 ouid=0

However, the name appears as /apparmor/.null.

The proposed workaround doesn’t seem to work either:

$ /snap/node/current/bin/node index.js 
spawn test stdout: "test spawn is running"

spawn test child process exited with code 0
exec test stdout: exec test is running

exec test stderr: 
exec node stdout: 
exec node stderr: 
spawn node child process exited with code 0
spawn yarn child process exited with code 1
exec yarn error: Error: Command failed: yarn --version

exec npm stdout: 
exec npm stderr: 
spawn npm child process exited with code 0

This is the special path that apparmor uses to replace a file descriptor which isn’t allowed, this looks exactly like the bug Maciej linked to above

1 Like