Linux下出现 pam_get_item 的原因分析

本文博客链接:https://zohead.com/archives/linux-pam-get-item-error-library/

近日在将一个 RHEL6 上的服务程序移植到老的 Fedora Core 2 Linux 上时出现了一些问题,该程序的功能为预先加载几个动态库,这几个动态库中再需要根据不同的需求做 PAM 用户验证操作,程序在 RHEL6 上编译和使用都没有问题,换到 Fedora Core 2 环境之后,编译时一切正常,但在运行时出现比较奇怪的 pam_get_item 错误:

pam: PAM [dlerror: /lib/security/../../lib/security/pam_env.so: undefined symbol: pam_get_item]
pam: PAM [dlerror: /lib/security/../../lib/security/pam_unix.so: undefined symbol: pam_get_item]
pam: PAM [dlerror: /lib/security/../../lib/security/pam_succeed_if.so: undefined symbol: pam_get_item]

程序所加载的动态库中使用的 PAM 配置文件 /etc/pam.d/system-auth 里就用到了 pam_env.so、pam_unix.so、pam_succeed_if.so 等几个 PAM 模块。

但实际系统中还有很多服务都使用 /etc/pam.d/system-auth 这个 PAM 配置来进行用户验证,都能正常工作,因此做下简单分析。

由于此服务程序太复杂,就打定写个简单的模拟程序来进行分析。

1、动态库部分(checkpam.c):

PAM 验证的实现,调用 PAM 函数进行验证,具体 PAM 函数的调用就不详细写出了。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <security/pam_appl.h>

int check_pam(const char * user, const char * pass)
{
    int ret = -1;

    pam_start("system-auth", user, …, …);
    …
    // 实际 PAM 代码省略,成功返回0,失败返回1 //
    …
    pam_end(…);

    return ret;
}

和实际服务程序一致,通过 system-auth PAM 配置进行验证,编译生成动态库:

cc -c checkpam.c
cc -shared -fPIC -lpam -o libcheckpam.so checkpam.o

ldd libcheckpam.so,查看此动态库的依赖列表:

[root@linux root]# ldd libcheckpam.so
        linux-gate.so.1 =>  (0x00c67000)
        libpam.so.0 => /lib/libpam.so.0 (0x0025b000)
        libc.so.6 => /lib/tls/libc.so.6 (0x0041d000)
        libdl.so.2 => /lib/libdl.so.2 (0x00153000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x00876000)


2、主程序部分(pamtest.c):

加载动态库,调用 PAM 进行用户验证,比较丑陋,只需能验证,HOHO。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>

int main(int argc, char ** argv)
{
    int ret = 0;
    void * lpso = NULL;
    int (* lpfunc)(const char *, const char *) = NULL;

    lpso = dlopen("./libcheckpam.so", RTLD_LAZY);
    if (lpso == NULL) {
        printf("Library load failed: %s.\n", dlerror());
        return 1;
    }

    lpfunc = dlsym(lpso, "check_pam");
    if (lpfunc == NULL) {
        printf("Load symbol failed: %s.\n", dlerror());
        dlclose(lpso);
        return 2;
    }

    ret = lpfunc(argv[1], argv[2], NULL);

    dlclose(lpso);

    printf("pam ret: %d.\n", ret);

    return ret;
}

功能很简单,dlopen 加载刚才生成的 libcheckpam.so,然后 dlsym 找到 check_pam 函数,接着调用函数进行用户验证。

编译程序:

cc -o pamtest pamtest.c -ldl

ldd pamtest,查看程序的依赖列表,可以看到此时已不需要 pam 库,和实际使用的服务程序的实现方式一致了:

[root@linux root]# ldd pamtest
        linux-gate.so.1 =>  (0x00631000)
        libdl.so.2 => /lib/libdl.so.2 (0x009ac000)
        libc.so.6 => /lib/tls/libc.so.6 (0x0088f000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x00876000)

然后运行 ./pamtest username password,参数分别指定用户名和密码,在 Fedora Core 2 上不管指定的用户名和密码是否正确,始终返回错误,并在系统日志中产生如文章开头所述的错误,而在 RHEL6 上就可以得到正确的结果。

原因分析:

经过查看 man 帮助并简单分析,终于找到原因,dlopen 的参数是这个问题的元凶之一。

节选 man dlopen 的说明如下:

The  value  of flag can be either RTLD_LAZY or RTLD_NOW. When RTLD_NOW is specified, or the environment variable LD_BIND_NOW is set to a  nonempty  string, all undefined symbols in the library are resolved before dlopen() returns. If this cannot be done, an error is returned.  Otherwise binding is lazy: symbol values are first resolved when needed.

Optionally,  RTLD_GLOBAL  may  be  or’ed  into  flag, in which case the external symbols defined in the library will be made available for symbol  resolution  of  subsequently  loaded  libraries. (The converse of RTLD_GLOBAL is RTLD_LOCAL. This is the default.)

简单说就是如果指定了 RTLD_NOW 参数,在调用 dlopen 时就解析所有未定义符号,如果出错就 dlopen 失败,RTLD_LAZY 则不这样。
比较重要的是 RTLD_GLOBAL 参数,如果这个设置上,则 dlopen 加载的动态库的外部符号可以被后续加载的库解析到。

关于库加载时的符号解析还有一段:

External references in the library are resolved using the libraries  in that  library’s  dependency  list  and  any  other libraries previously opened with the RTLD_GLOBAL flag.  If the executable  was  linked  with the  flag  "-rdynamic" (or, synonymously, "--export-dynamic"), then the global symbols in the executable will also be used  to  resolve  references in a dynamically loaded library.

由于本例中没有使用 -rdynamic 参数,因此看前面一部分,库中的外部引用首先从库的依赖列表(也就是万能的 ldd 中的结果,哈哈)中查找,然后再从之前用了 RTLD_GLOBAL 参数加载的库中查找。

具体到本例中,此测试程序使用的 dlopen 的参数为单独的 RTLD_LAZY,没有指定 RTLD_GLOBAL,PAM 验证时假设首先走 pam_env.so,这个 pam_env.so 也是在 pam_start、pam_end 等函数过程中通过 dlopen 动态加载的。

虽然我们的 libcheckpam.so 动态库在编译时有依赖 libpam 库,但由于 pamtest 程序在 dlopen 时未指定 RTLD_GLOBAL,libpam 的符号不能被后续加载的 pam_env.so 等 PAM 库解析到,所以就出现文章开头的那些错误。

解决方法:

因此有几种方法让此程序能在 Fedora Core 2 系统中正常运行,以下三种都已经经过测试确认过:

1、编译 pamtest 时指定依赖 pam 库,让 pam_env.so 等 PAM 库能解析到:
    cc -o pamtest pamtest.c -ldl -lpam
2、加载 libcheckpam.so 时增加 RTLD_GLOBAL 参数,将 pamtest.c 中 dlopen 行改为:
    lpso = dlopen("./libcheckpam.so", RTLD_LAZY | RTLD_GLOBAL);
3、如果碰到比较特殊的情况,pamtest 无法更改,就可以在运行 pamtest 前用 LD_PRELOAD 预先加载 libpam:
    export LD_PRELOAD="/lib/libpam.so.0"
    ./pamtest username password

另外提下为什么在 RHEL6 下没问题,Fedora Core 2 下有问题,看下两个系统下 pam_env.so 的依赖列表:

Fedora Core 2 下

[root@linux root]# ldd /lib/security/pam_env.so
        linux-gate.so.1 =>  (0x00674000)
        libc.so.6 => /lib/tls/libc.so.6 (0x00111000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x00876000)

RHEL6 下

[root@localhost ~]# ldd /lib64/security/pam_env.so
        linux-vdso.so.1 =>  (0x00007fff171ff000)
        libpam.so.0 => /lib64/libpam.so.0 (0x00007f6656472000)
        libaudit.so.1 => /lib64/libaudit.so.1 (0x00007f665625a000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007f6656056000)
        libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00007f6655e1f000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f6655a8d000)
        /lib64/ld-linux-x86-64.so.2 (0x000000328c400000)
        libfreebl3.so => /lib64/libfreebl3.so (0x00007f665582b000)

显然在 RHEL6 下,pam_env.so 本身就已经依赖了 libpam,这样 libpam 的符号肯定能被解析到,ldd 看看 /lib64/security/pam*.so 你会所有 PAM 动态库都已经依赖 libpam 库,这应该是新的系统中做的改动了。





*