今天遇到了一个非常奇怪的问题:在root用户下,python的sys path中包含了/usr/local路径下的site-packages目录,但是在非root用户下,却没有包含,导致该用户执行程序异常,报:ModuleNotFoundError

已经知道python程序在启动过程中会通过site.py脚本动态修改python path,所以简单的看了下site.py的代码,发现下面一段逻辑:

def addsitepackages(known_paths, prefixes=None):
    """Add site-packages to sys.path

    '/usr/local' is included in PREFIXES if RPM build is not detected
    to make packages installed into this location visible.

    """
    if ENABLE_USER_SITE and 'RPM_BUILD_ROOT' not in os.environ:
        PREFIXES.insert(0, "/usr/local")
    for sitedir in getsitepackages(prefixes):
        if os.path.isdir(sitedir):
            addsitedir(sitedir, known_paths)

    return known_paths

通过ENABLE_USER_SITE这个全局变量来影响是否将/usr/local加入到site packages的查询地址中。经过查阅,ENABLE_USER_SITE这个变量是由下面这个方法决定的。

def check_enableusersite():
    """Check if user site directory is safe for inclusion

    The function tests for the command line flag (including environment var),
    process uid/gid equal to effective uid/gid.

    None: Disabled for security reasons
    False: Disabled by user (command line option)
    True: Safe and enabled
    """
    if sys.flags.no_user_site:
        return False

    if hasattr(os, "getuid") and hasattr(os, "geteuid"):
        # check process uid == effective uid
        if os.geteuid() != os.getuid():
            return None
    if hasattr(os, "getgid") and hasattr(os, "getegid"):
        # check process gid == effective gid
        if os.getegid() != os.getgid():
            return None

    return True

这里经过debug,返回了一个None
于是乎,今天的主角出场了,可以看到上面这段代码中会去判断euid/egiduid/gid是否相等,如果不相等,就返回NoneNone是啥意思呢,根据官方给出的解释:

None means it was disabled for security reasons (mismatch between user or group id and effective id) or by an administrator.

我没有disable过呀,那就是user或者group的id和effective id不匹配导致的。

继续debug,终于确认是euiduid不相同导致的,这俩是个什么货呢?
UID: 进程中获取的UID是指进程的创建者,直白点说就是这个进程是谁执行的。
EUID: 进程中的EUID是指进程对系统中文件的访问控制权限是哪个用户的权限。
通常这两个ID是相同的,在用户登陆的时候被设置为/etc/passwd中的UID。

但是Linux提供了修改的机制,可以通过设定setuid来实现。当某个可执行文件被设置了setuid后,会使得其他用户在执行该可执行文件时获得该可执行文件所有者的权限。有点绕,说简单点就是,如果一个root用户所属的可执行文件,被设置了setuid后,其他用户执行时也能获得root权限。也就是上面的EUID会变成root用户的ID,而UID依然是执行用户的ID,这样两者就可能不再相同。

于是就明白了,问题所在:启动python进程的可执行文件肯定是被设置了setuid。但是检查发现python代码文件并没有。难道问题原因找错了?当然不是,启动python进程的应该是python命令,而不是python脚本,所以应该去check python命令文件,果然/usr/bin/python3.6文件被设置了setuid改回来一切又变得熟悉起来。