运行PAM函数触发Segmentation Fault问题排查求助
排查PAM模块嵌入Python触发Segmentation Fault的问题
刚接触C语言踩这种坑太正常了,我帮你拆解下代码里几个绝对会触发段错误的点,还有一些隐藏的风险:
1. 最致命的错误:直接把PyObject*和整数比较
你这段代码里的判断:
if (ret1 == 1){
完全错误!ret1是PyObject*类型(指针),而1是整数。你这里是在比较指针地址是否等于0x1,这几乎不可能成立,而且如果ret1是NULL(函数调用失败),或者返回的是Python的True/False对象,这种比较都会导致非法内存访问,直接触发段错误。
正确的做法是用Python的API来判断返回值:
- 如果你的
detect()函数返回Python的布尔值,用PyBool_Check(ret1)和ret1 == Py_True来判断 - 如果返回整数,用
PyLong_Check(ret1)然后转成C整数再判断
2. 缺少关键的空指针检查
你的代码里很多地方没有检查API调用的返回值,比如:
dlopen的返回值没检查,如果加载失败会返回NULL,后续操作会出问题PyObject_GetAttrString获取函数后,没检查func1是否是可调用对象(比如模块里没有这个函数,或者它不是函数),直接调用PyObject_CallObject会触发段错误PyObject_CallObject的返回值ret1可能是NULL(函数调用抛出异常),你直接用它也会出问题
3. 手动dlopen Python库是多余且危险的
Py_Initialize()会自动加载Python的共享库,你手动调用dlopen会导致重复加载,可能引发符号冲突或者内存错误,直接删掉这一行就行。
4. chdir的风险
你调用chdir("../code/facial")但没有检查是否成功,如果这个目录不存在,或者PAM模块运行的用户(比如root)没有权限访问,后续Python导入模块会失败,甚至可能导致其他奇怪的错误。一定要检查chdir的返回值:
if (chdir("../code/facial") != 0) { perror("Failed to change directory"); return PAM_AUTH_ERR; }
5. Python解释器的初始化和销毁时机问题
PAM模块可能会被多次调用(比如同一个进程里多次认证),Py_Initialize()和Py_Finalize()不应该每次认证都调用,频繁初始化销毁解释器不仅效率低,还可能导致内存泄漏或者段错误。建议在模块初始化时只初始化一次Python解释器,模块销毁时再销毁。
修正后的代码示例
我把你的代码改了一下,修复了上面的问题:
#define PY_SSIZE_T_CLEAN #define PAM_SM_AUTH #define PAM_SM_ACCOUNT //#define PAM_SM_SESSION #include <dlfcn.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <security/pam_appl.h> #include <security/pam_modules.h> #include </usr/include/python3.6m/Python.h> // 全局标记,确保Python解释器只初始化一次 static int python_initialized = 0; /* expected hook */ PAM_EXTERN int pam_sm_setcred( pam_handle_t *pamh, int flags, int argc, const char **argv ) { return PAM_SUCCESS; } PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { printf("Acct mgmt\n"); return PAM_SUCCESS; } /* expected hook, this is where custom stuff happens */ PAM_EXTERN int pam_sm_authenticate( pam_handle_t *pamh, int flags,int argc, const char **argv ) { // 检查目录切换是否成功 if (chdir("../code/facial") != 0) { perror("chdir failed"); return PAM_AUTH_ERR; } // 只初始化一次Python解释器 if (!python_initialized) { Py_Initialize(); // 添加当前目录到Python路径 PyRun_SimpleString("import os\nimport sys\nsys.path.append(os.getcwd())"); python_initialized = 1; } PyObject *mymod, *func1, *ret1; mymod = PyImport_ImportModule("pam_detect"); if (mymod == NULL) { PyErr_Print(); // 打印Python导入错误,方便排查 return PAM_AUTH_ERR; } func1 = PyObject_GetAttrString(mymod, "detect"); // 检查func1是否是可调用对象 if (func1 == NULL || !PyCallable_Check(func1)) { if (func1 == NULL) PyErr_Print(); else printf("detect is not a callable function\n"); Py_XDECREF(mymod); return PAM_AUTH_ERR; } // 调用函数,无参数 ret1 = PyObject_CallObject(func1, NULL); if (ret1 == NULL) { PyErr_Print(); // 打印函数调用时的Python异常 Py_XDECREF(func1); Py_XDECREF(mymod); return PAM_AUTH_ERR; } int auth_result = PAM_AUTH_ERR; // 假设detect返回布尔值 if (PyBool_Check(ret1)) { if (ret1 == Py_True) { auth_result = PAM_SUCCESS; } } // 如果返回整数,用下面的判断 else if (PyLong_Check(ret1)) { long val = PyLong_AsLong(ret1); if (val == 1) { auth_result = PAM_SUCCESS; } } else { printf("detect returned unexpected type\n"); } // 释放Python对象的引用,避免内存泄漏 Py_XDECREF(ret1); Py_XDECREF(func1); Py_XDECREF(mymod); // 注意:不要在这里调用Py_Finalize(),留到模块销毁时处理 return auth_result; } // 模块卸载时销毁Python解释器 PAM_EXTERN void _pam_destroy(pam_handle_t *pamh) { if (python_initialized) { Py_Finalize(); python_initialized = 0; } }
额外的排查技巧
- 用
gdb调试PAM模块:比如运行gdb --args sshd -d,然后触发认证,当段错误发生时,用bt命令查看调用栈,就能定位到具体哪一行出错 - 打开Python的错误输出:在
Py_Initialize()后调用PyErr_Print(),能看到Python层面的错误(比如模块导入失败、函数调用抛出异常),这些错误经常会间接导致C层面的段错误
内容的提问来源于stack exchange,提问作者Pythoncrypt
相关产品推荐
相关产品推荐

