一个由于环境变量产生的bug
起因
事情的起因主要是一个 Java 的同学通过 sudo -u admin
来执行基于 Egg 实现的服务挂了,然后在各种调试之后,终于找到了问题的所在。
Egg 会在用户路径下记录 Log 文件,而这个用户路径是由 process.env.HOME
获取到的。很神奇的一点是,通过它获取到的用户路径依然是原用户的路径,而不是期望中的 /home/admin
,于是就出现了权限问题,导致写入失败。
也就是类似下面的步骤:
1 | $ sudo -u admin node |
process.env
那么 process.env
它的值是怎么得到的呢?(・ω´・ )是不是取值的时候有什么奇怪的设定呢?
接着我找到了 node.cc 里初始化 node 里 process 的函数–SetupProcessObject函数实现,以及 process.env
的定义:
1 | // create process.env |
顺带一提,SetNamedPropertyHandler的定义:
void SetNamedPropertyHandler ( NamedPropertyGetterCallback getter,
NamedPropertySetterCallback setter = 0,
NamedPropertyQueryCallback query = 0,
NamedPropertyDeleterCallback deleter = 0,
NamedPropertyEnumeratorCallback enumerator = 0,
Handle< Value > data = Handle< Value >()
)
所以上面的process_env_template->SetNamedPropertyHandler
相当于创建了一个拥有 getter, setter, query…方法的对象
(% ゚ー゚)那么再来看 EnvGetter
的实现戳这里:
1 | static void EnvGetter(Local<String> property, |
最后定位到 char* getenv (const char* name)
这个函数, 也就是在 UNIX 下的 C 语言通过 getenv() 这个方法来获取当前的环境变量。
也就是说 process.env
拿的直接是当前系统的环境变量,( ˘・з・)那为什么用 sudo -u admin xxx
启动后的环境变量却不是 admin 下环境变量的呢 ?
sudo -u
接下来看了下 sudo -h
里面对 -u
的解释:
1 | -u user run command (or edit file) as specified user //指定用户运行命令(或编辑文件) |
那么,问题又来了( ˘•ω•˘ ),这个命令(sudo -u admin
)和真正切到该用户下(sudo su admin
)去执行命令有什么区别呢?是不是因为这些区别所以才引起一些奇怪的权限问题呢?
首先先来试下 sudo -u xxx
,由于本地电脑上没有其他的用户,就直接用 root
代替了。
1 | ➜ ~ sudo -u root node |
可以看到这里的 HOME
字段并不是 /var/root
而是 /Users/当前用户
,用了 root
的权限来执行 node
, 但是它的类似 HOME
字段的环境变量仍未被改变,还是在当前状态,(´-ω-`)这是为什么呢?
我们可以执行 sudo -l
来看当前的 sudo 配置或者直接查看 /etc/sudoers
:
1 | ➜ ~ sudo -l |
可以看到这里有很多的 env_keep
,也就是当使用 sudo
时,这些环境变量保持不变,所以如果我们在配置里把 env_keep+="HOME MAIL"
给注释了再用上面的方法执行的时候就不应该依旧是 HOME: '/Users/minary'
了,来验证下吧( ¯•ω•¯ ):
1 | sudo -l |
可以看到上面已经没有了 env_keep+="HOME MAIL"
,那么结果怎么样呢?
1 | ➜ ~ sudo -u root node |
ヽ(✿゚▽゚)ノ HOME
变成了 /var/root
,验证成功!
但是在内部的服务器上,/etc/sudoers
被单独配置过,怎么可以不修改这个配置但依旧获取到 root
的环境变量呢?
其实也很简单,直接用 sudo -u root -i xxx
执行就可以了, 来看下 sudo -h
里面对 -i
的解释:
1 | -i [command] run a login shell as target user |
相当于直接把整个环境切到了 admin 下去执行,所以获得的环境变量也就肯定是 root 下的了,再来做下验证:
1 | ➜ ~ sudo -u root -i node |
可以看到这里的 HOME
也变成了 /var/root
, 切换完成~ヾ(✿❛ω❛ฺฺ)ノ,撒花~
os.homedir
那么 node 下有没有直接可以 获取当前用户的 HOME 路径 的方法呢?
ヽ(•̀ω•́ )ゝ这时候突然想到了 node.js 中还有个方法,也就是 os.homedir()
,
看了下官网对这个函数的定义戳这里:
os.homedir() #Returns the home directory of the current user.
翻译过来是获取的是当前用户的 home 路径,感觉这个靠谱像是我需要的,(。˘•ε•˘。) 所以迫不及待的试了下:
1 | ➜ ~ sudo -u root node |
然后…=͟͟͞͞( •̀д•́),教练,这和想象中的不一样啊,于是…又去翻了 os.homedir()
的实现:
1 | // node/blob/master/lib/os.js |
1 | // node/blob/master/src/node_os.cc |
接下来再来看看 libuv 中 uv_os_homedir
的实现戳这里的时候突然发现了这个:
1 | /* Check if the HOME environment variable is set first */ |
(゚д゚≡゚д゚), 为什么,为什么第一步还是去拿当前的环境变量里的HOME
!当拿不到的时候才去获取当前用户的HOME
路径, 为什么要故意加一步?
然后我开始找这个特性的pr,果然看到了关于这个的讨论戳这里
I wonder if this should check $HOME first? It’s something of a UNIX tradition to be able to change your home directory on the fly. A problem with that is that getenv(“HOME”) is not MT-safe.
担心某些 UNIX 系统在运行的时候会把环境变量的HOME
路径给改掉了,会导致出现一系列的不安全的问题。
咦,那不是和 process.env.HOME
功能基本一样了么?然后找到了这个功能诞生前的讨论戳这里, 大致的意思就是 os.homedir()
是 process.env.HOME
的升级版(可能概括的不是很准确)
总结
所以,最后的总结就是在执行的时候注意这些细节,最好的执行方法还是直接用 sudo su xxx
去切换用户后去执行比较好,当然还是可以选择 sudo -u xxx -i
去执行,但是不推荐直接使用 sudo -u xxx
的方法来启动服务, 会导致一系列的因为环境变量引起的问题。
在 node.js 层上,也最好用os.homedir()
来替代process.env.HOME
,因为当遇到 getenv('PATH')
为空的时候,os.homedir()
会使用当前的运行用户的目录,降低bug率。