You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
近期,在线上运行服务时遇到了一个诡异的 Linux 权限问题:root 用户在操作本该有权限的资源时,却报了权限错误。
报错如下:
Error: EACCES: permission denied, mkdir '/root/.pm2/logs'
at Object.mkdirSync (fs.js:921:3)
at mkdirpNativeSync (/home/web_server/project/node_modules/pm2/node_modules/mkdirp/lib/mkdirp-native.js:29:10)
at Function.mkdirpSync [as sync] (/home/web_server/project/node_modules/pm2/node_modules/mkdirp/index.js:21:7)
at module.exports.Client.initFileStructure (/home/web_server/project/node_modules/pm2/lib/Client.js:133:25)
at new module.exports (/home/web_server/project/node_modules/pm2/lib/Client.js:38:8)
at new API (/home/web_server/project/node_modules/pm2/lib/API.js:108:19)
at Object.<anonymous> (/home/web_server/、project/node_modules/pm2/lib/binaries/CLI.js:22:11)
at Module._compile (internal/modules/cjs/loader.js:1137:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1157:10)
at Module.load (internal/modules/cjs/loader.js:985:32)
functionrun(pkg,wd,cmd,args,cb){// ...chain(cmds.map(function(c){// pass cli arguments after -- to script.if(pkg.scripts[c]&&c===cmd){pkg.scripts[c]=pkg.scripts[c]+joinArgs(args)}// when running scripts explicitly, assume that they're trusted.return[lifecycle,pkg,c,wd,{unsafePerm: true}]}),cb)}
The user, group, uid, gid, and unsafe-perms configurations are no longer relevant. When npm is run as root, scripts are always run with the effective uid and gid of the working directory owner.
1、问题背景
近期,在线上运行服务时遇到了一个诡异的 Linux 权限问题:root 用户在操作本该有权限的资源时,却报了权限错误。
报错如下:
这个错误非常直观,就是用户想要创建
/root/.pm2/logs
文件夹,但是没有权限。该服务使用 pm2 做多进程管理。pm2 默认会将其日志信息、进程信息等写入到$HOME/.pm2
下。因为是 root 后用户所以写到了/root/.pm2
里。但这个问题的奇怪之处在于,服务是通过 root 用户启动的,对
/root
目录是具有写入权限的。但这里却报了名优权限的错我。那么是什么导致 root 用户操作
/root
目录的权限“丢失”了呢?2、初步排查
项目是容器化部署,使用 npm script 启动,代码文件位于
/home/web_server/project
下。执行npm start
即可启动。这是我们使用的一套标准的构建与部署「模版」,已经在上百个服务上应用,且一直都正常。知道近期的一次上线出现了上面这个问题。
这次突然出现的这个问题让我充满了疑惑 —— 基于对 Linux 系统用户、用户组权限控制的理解,不可能出现这个错误。难道是我理解有误?
在疑惑的同时,我尝试不使用 npm script,直接通过 pm2 命令行
pm2 start ecosystem.config.js
启动,发现服务正常启动了!莫非是 npm 导致的?而在这次上线的时候,确实更新了基础镜像,升级了 npm cli。之前是 v6.x,这次更新到了 v7.x。而当我将 npm 版本回退到 v6.x 后,问题小时。看来是 v7.x 的改动导致了这个问题。
3、问题定位
先说结论:npm v6.x 使用 npm script 执行命令时默认会使用 unsafe 模式,将子执行命令的子进程设置为 root 用户/用户组,该行为可以通过
unafe-pem
配置来控制。而在 v7.x 中,如果通过 root 用户执行 npm script,则会基于当前目录(cwd)所属用户来设置。下面通过代码来一起看下。
3.1、v7.x 中 npm script 的实现
npm script 的执行逻辑可以从
lib/exec.js
中查看:省略无关代码,可以看到执行 npm script 时会调用
libexec
方法,libexec
方法内部会调用runScript
方法来执行命令。通过一系列曲折的调用,代码最后会调用到
promiseSpawn
方法。这个方法最终会使用 child_process 内置模块里提的spawn
方法来启动子进程执行命令,其相关代码如下:上面的实现中,有一行非常重要:
可以看到,如果当前进程的用户是 root,则会使用
inferOwner
方法来设置启动的子进程的 uid 和 gid(也就是用户 id 和用户组 id)。那么
inferOwner
是做什么的呢?它其实就是用来获取某个文件所属的用户与用户组的:其中最重要的代码是这几行:
fs.lstatSync
方法 会使用fstat
这个系统调用来获取文件的 uid 和 gid。promiseSpawn
中会将 cwd 传入来获取 uid 和 gid。而在我们线上服务的容器里,我们是在/home/web_server/project
下执行npm start
,该目录所属用户是web_server
,用户组是web_server
。所以 npm 在启动子进程时“切换”了用户。所以实际情况是,
pm2 start ecosystemt.config.js
相当于是被 web_server 用户启动的,但是环境变量$HOME
仍然是/root
。所以在/root
中创建文件夹,自然就没有权限。3.2、v6.x 中 npm script 实现方式的区别
v7.x 为了权限安全,做了上述操作,那么 v6.x 如何呢?
v6.x 的 npm script 入口是
lib/run-script.js
文件:而其实际执行则需要从
lifecycle
方法中来找。上面这段代码的最后一行还有一个非常重要的参数{ unsafePerm: true }
,之后会用到。lifecycle 本身代码并不复杂,主要就是参数调整,然后调用实际函数。和 uid、gid 实际的设置代码是在
npm-lifecycle/index.js
中的runCmd
里:runCmd
里会通过传入的opt.unsafePem
参数(就是上面设置的那个{ unsafePerm: true }
)来判断是否是unsafe
的。如果是unsafe
,则会在调用runCmd_
时将 uid、gid 设置为 0。0 就代表 root 用户和 root 用户组。而最终在
runCmd_
中的spawn
就是child_process
中的spawn
方法:到这里我们就定位到了该问题:
unsafe-pem
这个 npm config,npm script 就会在启动子进程时默认设置为 root。目前从代码实现来看,似乎没有特别好的处理方式,比较简答的两种就是:
/home/web_server/project
所属用户/用户组改为 root。但权限的改动可能会引发其他问题。3.3、npm cli 的变更日志
其实,这个变更在 npm v7.0.0-beta.0 发布时的 CHANGELOG 里是有提到的。不过只有寥寥一行:
大致说的就是咱们上面从代码分析的结论:如果是 root 运行 npm,则在脚本执行时切换到当前工作目录的 owner。
然后如果你跟着代码看下来,也会发现 v6.x 中的
unsafe-pem
配置,在 v7.0.0 开始就被废弃了。不过 npm cli 文档更新的较慢,直到 v7.0.0 正式版发布后的一个月后,才在 v7.0.15 的 Release 里把unsafe-pem
从文档中移除。3.4、其他可能出现的问题
这个功能实现的变更,除了会导致一些文件操作时的权限问题,还会有一些其他场景的权限错误。例如在如果你用 npm script 启动一个 nodejs server,要绑定 443 端口,这个时候可能就会报错。因为会需要 root 权限来执行这个端口绑定。在 issue 里就有人提到了这个情况。
4、加餐:child_process#spawn 是如何设置 user 和 group 的?
通过上面的分析,问题已经被解决了。沿着这个问题,可以具体看了下 Nodejs 中,child_process 模块的
spawn
方法是如何设置 user 和 group 的。Nodejs 中,我们在上层引入的模块,是直接放在
lib
下面的,而其一般会在调用lib/internal
下的对应模块,这部分会直接使用 internalBinding 来调用 C++ 对象和方法。child_process 也不例外,你会在lib/internal/child_process.js
中看到如下代码:因为比较简答,所以这里省去了
lib/child_process.js
中的方法。只要知道,我们在 JavaScript 层使用spawn
方法时,最后会调用到 ChildProcess 实例的spawn
方法即可。可以看到最后是调用了this._handle.spawn
。那么this._handle
是什么呢?它其实就是通过 binding 创建的 Process 对象:
这个 binding 的设置在
src/process_wrap.cc
中,可以看到,它把从 JavaScript 层设置的 uid 和 gid 设置到 options 上,然后调用了
uv_spawn
函数创建子进程。在uv_spawn
中对于创建的子进程会通过uv__process_child_init
来做初始化设置:最后则是在
uv__process_child_init
里通过setuid
和setgid
这两个系统调用来实现的:在 Nodejs 官方文档中也有介绍。
我们通过阅读代码也印证了这一点。
完。
The text was updated successfully, but these errors were encountered: