Ctypes libc.fdopen(<Python file object>.fileno(), b"a") leads to segfaults on write

Hello.

I have this C API Python function from the rpm module:

static PyObject * setLogFile (PyObject * self, PyObject *arg)
{
    FILE *fp;
    int fdno = PyObject_AsFileDescriptor(arg);

    if (fdno >= 0) {
        /* XXX we dont know the mode here.. guessing append for now */
        fp = fdopen(fdno, "a");
        if (fp == NULL) {
            PyErr_SetFromErrno(PyExc_IOError);
            return NULL;
        }
    } else if (arg == Py_None) {
        fp = NULL;
    } else {
        PyErr_SetString(PyExc_TypeError, "file object or None expected");
        return NULL;
    }

    (void) rpmlogSetFile(fp);
    Py_RETURN_NONE;
}

The librpm function call is rpmlogSetFile(fdopen(PyObject_AsFileDescriptor(arg), "a")). The rest is glue and arg/error handling.

This function is used from Python with sys.stderr and tempfile.NamedTemporaryFile instance and both seem to work fine.

I was about to reimplement this in ctypes.

I have this:

import ctypes
from ctypes.util import find_library

LIBC = ctypes.cdll.LoadLibrary(find_library("c"))
LIBRPM = ctypes.cdll.LoadLibrary(find_library("rpm"))

def rpm_set_log_file(fileobj):
    LIBRPM.rpmlogSetFile(LIBC.fdopen(fileobj.fileno(), b"a"))

However, when I call rpm_set_log_file(tempfile.NamedTemporaryFile(...)) and RPM tries to log something, I get a segfault. Same with sys.stderr.

I debugged this a bit and I get:

>>> LIBC.fdopen(sys.stderr.fileno(), b"a")
-12324400

(Sometimes I get a positive number and it makes no difference.)

Let’s try to write something:

>>> LIBC.fwrite(b"x", 1, 2, -12324400)
Segmentation fault (core dumped)

Whta am I doing wrong? The documentation for PyObject_AsFileDescriptor says it returns .fileno(). So I expected I could do this.

Thanks.

You need to setup the correct argument and return types for the functions you use: ctypes — A foreign function library for Python — Python 3.13.1 documentation

Otherwise you get only 32bit values out of the function and into the function, which doesn’t work on 64bit systems for obvious reasons.

1 Like

Thanks!

For the record for the debugging example, that is:

LIBC.fdopen.argtypes = [ctypes.c_int, ctypes.c_char_p]
LIBC.fdopen.restype = ctypes.c_void_p

LIBC.fwrite.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p]

And for the RPM example:

def rpm_set_log_file(fileobj):
    LIBC.fdopen.argtypes = [ctypes.c_int, ctypes.c_char_p]
    LIBC.fdopen.restype = ctypes.c_void_p
    LIBRPM.rpmlogSetFile.argtypes = [ctypes.c_void_p]
    LIBRPM.rpmlogSetFile(LIBC.fdopen(fileobj.fileno(), b"a"))