Search code examples
cpython-2.7numpypampython-embedding

ImportError and PyExc_SystemError while embedding Python Script within C for PAM modules (.so files)


I'm trying to write a demo PAM module in C, which uses Embedding Python in C concept to run a script written in python (2.7), inside pam_sm_authenticate() function, which is written in C file (pam_auth.c).

This is the python script: test.py

import math
import numpy
def test_func():
   a = "test"
   return a 

The path for test.py is /usr/lib/Python2.7/ so that I can easily import it.

This is the C file:

#define PAM_SM_AUTH
#define PAM_SM_ACCOUNT
#define PAM_SM_SESSION

#include <security/pam_modules.h>
#include <security/_pam_macros.h>
#include <security/pam_appl.h>
#include<python2.7/Python.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#define NOBODY "nobody"


/*PAM Stuffs*/

PAM_EXTERN int pam_sm_authenticate(
  pam_handle_t* pamh, int flags, int argc, const char** argv)
{
    const char *user;
    int retval;
    user = NULL;
    retval = pam_get_user(pamh, &user, NULL);
    if(retval != PAM_SUCCESS)
    {
        fprintf(stderr, "%s", pam_strerror(pamh, retval));
//      return (retval);
    }
    fprintf(stdout, "retval= %d user=%s\n", retval,user);
    if (user == NULL || *user =='\0')
        pam_set_item(pamh, PAM_USER, (const char*)NOBODY);

    /* Python Wrapper */    

    // Set PYTHONPATH TO working directory
    //int res = setenv("PYTHONPATH",".",1);
    //fprintf(stdout, "%d", res);

    PyObject *pName, *pModule, *pDict, *pFunc, *pValue, *pResult;

    // Initialize the Python Interpreter
    Py_Initialize();

    // Build the name object
    pName = PyString_FromString((char*)"test");

    // Load the module object
    pModule = PyImport_Import(pName);

    // pDict is a borrowed reference 

    PyErr_Print();
    pDict = PyModule_GetDict(pModule);

    // pFunc is also a borrowed reference 
    pFunc = PyDict_GetItemString(pDict, (char*)"test_func");

    if (PyCallable_Check(pFunc))
    {
        pValue=NULL;
        PyErr_Print();
        pResult=PyObject_CallObject(pFunc,pValue);
        PyErr_Print();
    }else 
    {
           PyErr_Print();
    }
    printf("Result is %s\n",PyString_AsString(pResult));

    // Clean up
    Py_DECREF(pModule);
    Py_DECREF(pName);/* */

    // Finish the Python Interpreter
    Py_Finalize();      

    return PAM_SUCCESS;
}

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)
{
    return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_open_session(
  pam_handle_t* pamh, int flags, int argc, const char** argv)
{
    return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_close_session(
  pam_handle_t* pamh, int flags, int argc, const char** argv)
{
    return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_chauthtok(
  pam_handle_t* pamh, int flags, int argc, const char** argv)
{
    return PAM_SUCCESS;
}

The C-file is just a modification of pam_permit.c. The C file is compiled using gcc ( gcc -shared -o pam_auth.so -fPIC pam_auth.c -I/usr/include/python2.7 -lpython2.7 ) to obtain an .so file (pam_auth.so) and is put inside the folder /lib/security/

I changed the PAM configuration of 'sudo' file in /etc/pam.d as follows:

#%PAM-1.0

auth       required   pam_env.so readenv=1 user_readenv=0
auth       required   pam_env.so readenv=1 envfile=/etc/default/locale user_readenv=0
#@include common-auth #this line is commented to make it use my pam module
auth       required   pam_auth.so
@include common-account
@include common-session-noninteractive

The line "auth required pam_auth.so" forces the system to use my module for authentication everytime I use the command "sudo". (for ex- sudo nautilus)

Now the problem is: This line in C file " pModule = PyImport_Import(pName); " gives an import error, which is printed by PyErr_Print() as follows:

stitches@Andromida:~$ sudo nautilus
retval= 0 user=stitches
Traceback (most recent call last):
  File "/usr/lib/python2.7/subho_auth.py", line 8, in <module>
    import numpy
  File "/usr/lib/python2.7/dist-packages/numpy/__init__.py", line 153, in <module>
    from . import add_newdocs
  File "/usr/lib/python2.7/dist-packages/numpy/add_newdocs.py", line 13, in <module>
    from numpy.lib import add_newdoc
  File "/usr/lib/python2.7/dist-packages/numpy/lib/__init__.py", line 8, in <module>
    from .type_check import *
  File "/usr/lib/python2.7/dist-packages/numpy/lib/type_check.py", line 11, in <module>
    import numpy.core.numeric as _nx
  File "/usr/lib/python2.7/dist-packages/numpy/core/__init__.py", line 6, in <module>
    from . import multiarray
ImportError: /usr/lib/python2.7/dist-packages/numpy/core/multiarray.so: undefined symbol: PyExc_SystemError
Segmentation fault (core dumped)

As per I can understand,it fails to import numpy library as specified in test.py file. How to solve this problem of ImportError & PyExc_SystemError?

The python script works as charm if I run in as follows:

#include <Python.h>
#include <stdlib.h>
#include <string.h>
int main()
{   
    // Set PYTHONPATH TO working directory
    //int res = setenv("PYTHONPATH",".",1);
    //fprintf(stdout, "%d", res);

    PyObject *pName, *pModule, *pDict, *pFunc, *pValue, *pResult;

    // Initialize the Python Interpreter
    Py_Initialize();

    // Build the name object
    pName = PyString_FromString((char*)"test");

    // Load the module object
    pModule = PyImport_Import(pName);

    // pDict is a borrowed reference 

    PyErr_Print();
    pDict = PyModule_GetDict(pModule);

    // pFunc is also a borrowed reference 
    pFunc = PyDict_GetItemString(pDict, (char*)"test_func");

    if (PyCallable_Check(pFunc))
    {
        pValue=NULL;
        PyErr_Print();
        pResult=PyObject_CallObject(pFunc,pValue);
        PyErr_Print();
    }else 
    {
           PyErr_Print();
    }
    printf("Result is %s\n",PyString_AsString(pResult));

    // Clean up
    Py_DECREF(pModule);
    Py_DECREF(pName);/* */

    // Finish the Python Interpreter
    Py_Finalize();      

    return 0;
}

If it works under general python embedding examples, why its not working in PAM-based embedding examples (where .so files are used)?

PS: I'm importing numpy for a particular reason. Don't ask why I've not used in anywhere in python script as this is just a demo script of what I'm trying to achieve. Moreover, import math doesn't give any import error. I get import error for SciPY too.

PPS: Numpy and Scipy packages works perfect in python scripts and is installed under /usr/lib/python2.7/dist-packages/. I'm using ubuntu 14.04.

Please help!!!!


Solution

  • I don't know the answer to your question, but I am wondering why it didn't fail earlier. The host application does not know your PAM module will be needed using libpython2.7.so.1, so somehow that must being loaded dynamically otherwise the Py_Initialize() call would fail with the same error.

    Given you say it doesn't fail there it must be loaded. However from the error you are getting we can deduce the symbols it contains (such as PyExc_SystemError) are not visible to dynamic libraries subsequently loaded. This is the default when libraries are loaded using dlopen() (see RTLD_LOCAL in man 3 dlopen). To override it, you must pass RTLD_GLOBAL to dlopen(). Maybe that's your problem.

    Other comments about your code:

    • Calling Py_Initialise() for each pm_sm_...() call is going to be expensive and possibly surprising to the python modules. It means all data the python module accumulated within one call (like say voice or the user name) will be discarded when the next call is made. You are better off loading libpython2.7.so.1 and initialising PAM once, then using the cleanup function of pam_set_data() to unload it when you are done.

    • In a related issue, your PAM module isn't usable from Python programs because you always call Py_Initialise() (and I presume the matching call to Py_Finalize()).

    • If you program hadn't fallen over where it did, it would have fallen over on the line printf("Result is %s\n",PyString_AsString(pResult)) because pResult isn't initialised.

    • As I think you know, all the boilerplate you have here to let you wring PAM modules in Python is provided by pam-python - no C required. Since you are evidently writing your PAM module in Python anyway, you are already exposed to the overheads it incurs but are missing out on the features it provides like logging uncaught Python exceptions. And most importantly, using it means you can avoid C entirely. Your PAM module will be loaded into programs that guard the security of the machine - programs like login, sudo, and xdm/gdm3. Avoiding C means also avoiding the legions of security bugs C programs can have that are impossible in Python - buffer overruns, uninitialised pointers and accessing free'ed memory. Since you have one of those bugs in your the C code you posted here, avoiding it sounds like a good idea.