|
|
Log in / Subscribe / Register

Accessing new kernel features from Python

By Jake Edge
July 2, 2025

PyCon US

Every release of the Linux kernel has lots of new features, many of which are accessible from user space. Usually, though, the GNU C Library (glibc) and tools that access the Linux user-space API lag behind the kernel releases. Geoffrey Thomas showed how Python programs can access these new kernel features as soon as the kernel is released in his "What's New in the Linux Kernel... from Python" talk at PyCon US 2025. While he had two examples of accessing new kernel features, the real goal of the talk was to demonstrate how to go about connecting Python to the Linux kernel.

He began by noting that the kernel and its interfaces are written in C, so there would be a "tiny bit" of C in the talk. He would be explaining any of that, so "as long as you can read Python, you'll be okay". In addition, all of the code, his slides, and more are available from his GitHub repository for the talk. Since the presentation, the YouTube video of it has been released as well.

Creating files

So, he asked, "what are these new kernel features and why would you even want to use them?" Opening a file in Python looks something like this:

   open("file.txt", "w")
That will open a file named "file.txt" for writing; if the file exists, it will get overwritten, but if it does not, it will be created. It turns out that the systemd developers needed to figure out which of those two things happened, so they added a way to find out to the kernel. He pointed to the first 6.12 merge window article here at LWN as the place where he found out about the change.

[Geoffrey Thomas]

The commit spells out the change, which added a new fcntl() command called F_CREATED_QUERY that programs can use to find out if the file was created when it was opened or not. fcntl() is a Linux system call, which is a way for an unprivileged user-space program to request something from the kernel; it is, effectively, a function call, he said, but also kind of like making an API call to a server. As with network-server APIs, languages like Python often have wrappers around system calls. The fcntl module in the Python standard library serves that purpose for fcntl().

But, when looking at the Python documentation, there is no mention of the new command. The biggest reason for that is because it is new, but it also has not really been documented anywhere other than in the kernel source. He sympathizes with the kernel developers, since he also skips documenting new features at times, but it does mean that those who want to use the feature need to dig for information on it.

"Being able to search source code and figure out just enough of a large project to track down the info you need and not get lost is a valuable skill". He listed a few code-search options, starting with GitHub code search, which searches its public repositories, though you need to be logged into the site to access it. Sourcegraph search searches GitHub, GitLab, and a few other sites. Debian code search will look in anything that is part of the Debian distribution, including projects that are not in Git at all. And, of course, the "git grep" command is invaluable if you are searching in a local Git clone. One that he did not mention, but that is quite useful for searching the kernel (and its various versions) is Elixir.

Since he has a clone of the Linux kernel repository, he used "git grep" to look for F_CREATED_QUERY. It appears in two locations, fs/fcntl.c, which looks like the implementation, and include/uapi/linux/fcntl.h, which is a definition in the user-space API (uapi) of the kernel. The latter file contains:

    #define F_CREATED_QUERY (F_LINUX_SPECIFIC_BASE + 4)
Another "git grep" shows that F_LINUX_SPECIFIC_BASE is 1024, so the value of the constant for the new command is 1028. Thomas did note that the new command shows up in a few spots in the kernel's tools directory, which can be ignored for the purposes of this exercise, though that can sometimes be useful for examples and tests using a feature.

He then turned to the do_fcntl() function in fcntl.c. That function matches fairly closely with the fcntl.fcntl() described in the Python documentation, so he tried using it in the Python REPL:

    >>> F_CREATED_QUERY = 1028
    >>> import fcntl
    >>> a = open("foo.txt", "w")
    >>> fcntl.fcntl(a, F_CREATED_QUERY)
    1
    >>> a.close()
    >>> b = open("foo.txt", "w")
    >>> fcntl.fcntl(b, F_CREATED_QUERY)
    0
As can be seen, the file was created with the first open, and not on the second. The code could then be wrapped up into a tiny library as follows:
    import fcntl

    F_CREATED_QUERY = 1028

    def was_created(file):
        return bool(fcntl.fcntl(file, F_CREATED_QUERY))
Even though it is only four lines long, that library does something that nothing else in the Python API can do. It might even be worth uploading to the Python Package Index (PyPI), he said; beyond its immediate utility, it could perhaps serve as an example for someone trying to support some other new fcntl() command in the future.

Memory maps

A longstanding feature of Unix-like operating systems is to try to treat everything as a file; the tools and interfaces for working with files already exist, which makes it easy to access new features that way. Linux has long had the /proc filesystem, which has files and directories but is not anything that is stored on disk. Instead, it provides textual information about processes and other kernel objects, which Python programmers can access using the standard mechanisms for reading files. Every process in the system has a directory in /proc with its process ID as its name; various files in that directory give information about the process. For example:

    $ ls -l /proc/4067/exe
    [...] /proc/4067/exe -> /usr/bin/bash
Various utilities, such as ps and lsof, use information from /proc.

The /proc/self directory refers to the directory for the current process, so one can look at the memory mappings for their running Python with the following:

    >>> print(open("/proc/self/maps").read())
    00400000-009d8000 r-xp 00000000 00:20 3872 \
        /usr/bin/python3.13
    [...]
    f08ceae80000-f08ceaeba000 r-xp 00000000 00:20 8872 \
        /usr/lib/aarch64-linux-gnu/libncursesw.so.6.5
    [...]
That produces voluminous output about all of the memory mappings, which includes things like the Python executable and all of the libraries used by it. One way to (mostly) just look at the filenames that appear at the end of the line would be:
    >>> libs = {l.strip().split()[-1] for l in open("/proc/self/maps")}
    >>> print("\n".join(libs))
That splits each line into fields and puts the last one into a set using a set comprehension; it then prints each set element on its own line. Thomas said he uses some variant of that frequently in order to check library versions. Its output might look something like this:
    [...]
    /usr/lib/aarch64-linux-gnu/libexpat.so.1.10.0
    /usr/lib/aarch64-linux-gnu/libz.so.1.3.1
    [heap]
    /usr/bin/python3.13
    /usr/lib/aarch64-linux-gnu/libstdc++.so.6.0.34
    [...]

But that code is a bit of a hack; it shows things that are not files (e.g. "[heap]") and will not work correctly if there are spaces in the filename, for example. In general, it seems somewhat circuitous for the kernel to have to turn its internal data structures into textual output that the Python code then needs to parse in some fashion to extract what it wants.

It turns out that some high-performance tracing programs for Linux needed a way to get that information directly, so a new interface was added for the 6.11 kernel; it uses a new PROCMAP_QUERY ioctl() command on a file descriptor for the /proc/PID/maps file. It provides mechanisms to filter the output in various ways and get the information in a binary format. "So the motivation was not Python users, but that doesn't mean we can't use it from Python."

In a bit of an aside, he noted that C is sort of the "lingua franca" of computing, but that Linux (and other operating systems) could have been written in other languages. At PyCon 2018 in Cleveland, he and Alex Gaynor wondered how hard it would be to get Rust code working with the Linux kernel, so they began that effort at the sprints after the conference. That is where Rust in the kernel got its start. He strongly recommended attending the sprints, even for non-Python projects like the one they worked on.

Complications

The new PROCMAP_QUERY is a "much more complicated interface" than his other example; it starts with the struct procmap_query that is used with the new ioctl() command. He showed the lengthy structure on a slide without all of its comments so that it fit; he noted that C structures are akin to Python data classes.

One of the reasons C is used for system-level programs, though, is that its data types are simple, Thomas said. It does not have classes, dictionaries, inheritance, and so on like Python does. "C has very few data types", just numbers, integer and floating point, pointers (addresses, which are ultimately numbers), structures, and not much more. Its arrays are fixed length and its strings are simply arrays of bytes. For the most part, C completely specifies the format of its data, down to the level of bits and bytes; a __u32, for example, is a 32-bit unsigned integer, and it will take up exactly four bytes in the structure.

Turning that into a data class is fairly straightforward, but all of the types will simply be int, because Python does not care about the number of bits in a value of that sort. There will need to be a way to convert between the two representations, but the difference makes it clear that the data-class representation "doesn't tell you anything about what the bits and bytes are".

    @dataclasses.dataclass
    class ProcmapQuery:
        size: int
        query_flags: int
        query_addr: int
        vma_start: int
        vma_end: int
        vma_flags: int
        [...]

The Python representation is great for doing calculations and not having to worry about the number of bytes involved, but it does not work so well when you need to store the data in an interoperable way. That's where C's rigid specification helps. The Python struct module can convert between Python types and the bits and bytes needed by C. So, using struct, he changed the ProcmapQuery class to automatically generate the size field:

    @dataclasses.dataclass
    class ProcmapQuery:
        _STRUCT = struct.Struct("@9L4I2L")

        size: int = _STRUCT.size
        [...]

The format string ("@9L4I2L") specifies nine 64-bit values, then four 32-bit values, ending with two 64-bit fields, all of which are encoded in the native byte order ("@"). The size field is used to allow the kernel to extend the API by adding more entries to the structure, thus increasing its size.

The structure for the query consists of both parameters that are passed into the call, others that are returned from the call, and two that have both roles. The idea is that the programmer fills in the input fields and the kernel responds by filling in the output fields. Given that he wants to extract the names of the files from the query, he focused on two pairs of fields in struct procmap_query:

    __u32 vma_name_size; /* in/out */
    __u32 build_id_size; /* in/out */
    __u64 vma_name_addr; /* in */
    __u64 build_id_addr; /* in */
Those fields highlight some big differences between C and Python.

In both cases, for the virtual memory area (VMA) name and the build ID (which he did not use), the kernel is being passed the address of a buffer in the *_addr field and the length of the buffer in *_size. The size fields are both input and output parameters because they tell the kernel how many bytes are available in the buffer and the kernel returns how many of those bytes it filled in. If the returned size is the same as the passed-in size, Thomas said, the code should probably retry the query with a larger buffer, but he planned to leave that as an enhancement that could be added.

The query_flags field uses values from the enum procmap_query_flags, most of which are uninteresting for getting a list of the file mappings. But two are of use: PROCMAP_QUERY_FILE_BACKED_VMA asks for VMAs that are backed by a file, while PROCMAP_QUERY_COVERING_OR_NEXT_VMA allows code to step through all of the VMAs in a process by asking for the next VMA starting at address zero.

He then showed a few member functions added to the ProcmapQuery class:

    def pack(self) -> bytes:
        return self._STRUCT.pack(*dataclasses.astuple(self))

    @classmethod
    def unpack(cls, packed: bytes) -> Self:
        return cls(*cls._STRUCT.unpack(packed))

    def ioctl(self, fd: int) -> Self:
        return self.unpack(fcntl.ioctl(fd, PROCMAP_QUERY, self.pack()))
The first two simply convert the data class from its Python form to the binary form expected by C—or the reverse. For pack(), which converts to the C representation, the pack() member function of the struct.Struct class expects a tuple, which dataclasses.astuple() creates. unpack() turns the C structure into a ProcmapQuery object by using the @classmethod decorator and the struct.Struct unpack() method. The ioctl() function ties it all together by packing up the data class, passing it to the ioctl() system call, and unpacking the result.

PROCMAP_QUERY

One important thing that is needed, though, is a value for the PROCMAP_QUERY constant, which is more difficult to determine than F_CREATED_QUERY was, he said. It is a #define in fs.h to:

    _IOWR(PROCFS_IOCTL_MAGIC, 17, struct procmap_query)
That definition creates a constant value that encodes the type of the file the command should operate on along with other information that the ioctl() machinery uses to try to fend off invalid calls. It is not at all straightforward to work all of that out and turn it into an integer, especially within the time constraints of his talk, so he simply took the path of least resistance and wrote a little C program to print it out. With the appropriate C header files, a simple printf() of PROCMAP_QUERY gives the magic integer needed: 3228067345. That number may be architecture-specific, he cautioned, so production code would need a better mechanism to obtain it, possibly following the lead of his procmapquery-cffi example.

The last thing needed before this can all be put together into a program, Thomas said, is to figure out how to get a buffer to pass in. There are a number of different ways to get a buffer in Python, but some of them do not provide a mechanism to get its address, which is not useful for Python, generally, but is needed here. For this, he is using the ctypes module for its create_string_buffer() and addressof() functions:

    >>> buf = ctypes.create_string_buffer(1024)
    >>> ctypes.addressof(buf)
    4806710272
With all of that in hand, he put up the 15 lines of Python code that constitute the guts of his "what libraries are being used?" tool:
    libraries = set()
    query = ProcmapQuery(
        query_flags=PROCMAP_QUERY_COVERING_OR_NEXT_VMA |
                    PROCMAP_QUERY_FILE_BACKED_VMA,
        query_addr=0,
        vma_name_addr=ctypes.addressof(buf),
        vma_name_size=1024,
    )
    while True:
        try:
            response = query.ioctl(proc_maps)
        except FileNotFoundError:
            break
        libraries.add(os.fsdecode(buf[:response.vma_name_size]))
        query.query_addr = response.vma_end
It creates a set to hold the libraries, sets up the query object, and then loops over the addresses, adding filenames from the response to the set (using os.fsdecode() for the filename), and then using the vma_end address returned as the next address to query. The standard FileNotFoundError exception is what fnctl.ioctl() raises when there are no more VMAs to process, which he found by trial and error. The procmapquery directory in his repository contains the code, which has a get_libraries() function to return the set of libraries for a file descriptor or that can be run from the command line to print the libraries from /proc/self/maps.

Some wrap-up

There were two web sites that he recommended using to find out about new kernel features. As readers presumably already know, LWN.net is a great place to discover new kernel features in the merge-window summaries, feature articles, and so on. While Thomas is "a happy subscriber", he noted that the paywall only lasts a week or so; all of the content he mentioned in the talk was freely available to anyone. Another site he pointed to was KernelNewbies, which has "really detailed changelogs of just about everything that goes into the kernel" (example). That summary will have links to articles and blog posts about those features, as well as links to the commits.

He also had suggestions on some areas to explore for those who are interested in digging in further, including investigating the ctypes library he used and the C Foreign Function Interface (CFFI) module that he mentioned for retrieving the PROCMAP_QUERY constant. Both of those are useful for connecting Python to C programs, including the kernel. Cython is a Python-like language that compiles to C, which is another avenue for exploration. Writing Python extensions in C will give even more power to Python programs; various modules that he used, such as fcntl and os were written that way. "If you need that much control and you don't want to write C", he suggested investigating PyO3, which allows writing Python extensions in Rust.

In the brief Q&A, an audience member wondered about how programmers could be sure that the Python version of the C procmap_query structure was correct. Thomas said that could definitely be a problem, which might be hard to track down. His longer example using the CFFI module would actually properly ensure that, but it is more involved to use; CFFI is a third-party module and requires a C compiler to use. If he was working on a production version of the query, though, he said he would likely base it on the CFFI version.

[Thanks to the Linux Foundation for its travel sponsorship that allowed me to travel to Pittsburgh for PyCon US.]

Index entries for this article
ConferencePyCon/2025
PythonLinux kernel


to post comments

Efficient Way Of Searching Source Code

Posted Jul 2, 2025 14:29 UTC (Wed) by PengZheng (subscriber, #108006) [Link] (6 responses)

> ""Being able to search source code and figure out just enough of a large project to track down the info you need and not get lost is a valuable skill"". He listed a few code-search options, starting with GitHub code search, which searches its public repositories, though you need to be logged into the site to access it.

I suspect it will be more efficient to ask ChatGPT/Gemini/DeekSeek than to search code repositories that you are unfamiliar with.

PROCMAP_QUERY ioctl in Rust for Syd on Linux>=6.11

Posted Jul 2, 2025 23:58 UTC (Wed) by alip (subscriber, #170176) [Link] (2 responses)

Thanks to reading this article and having learned about the new ioctl request, I went on to implement support for this in the Syd sandbox[1]. The implementation together with unit tests are contained in the file src/proc.rs[2] which is licensed GPL-3.0 and forever free. Feel free to reuse. I do _not_ plan to split this code into its own crate, because it is fairly trivial and I believe this is better implemented in common crates such as nix, rustix etc.

One peculiarity which is not directly obvious about the implementation is Landlock[3] allows access to this procfs ioctl request by default so we do _not_ grant "LANDLOCK_ACCESS_FS_IOCTL_DEV" access to the procfs directory. As careful reader of the commit will see, this ioctl request is protected by system call argument cookies[4] for hardening to raise the bar against an attacker who has compromised Syd and who is looking into using this ioctl request to leak information about process memory.

For context, Syd is a sandbox written in Rust. By default, Syd sandbox reads proc_pid_maps at ptrace boundary to access check exec(3) calls. At PTRACE_EVENT_EXEC time, typically only the binary itself and the dynamic linker is loaded in memory so even textual parsing of proc_pid_maps using the procfs crate gives Syd a fair performance which continues to be the fallback for kernels older than 6.11.

To ease common use and testing of this kernel feature, I have taught the syd-x[5] utility the _-l pid..._ option to list the executables of the given processes in line-based compact JSON using PROCMAP_QUERY ioctl on Linux>=6.11 and using textual parsing on older kernels. This also allows simple benchmarking of both methods with the use of the SYD_ASSUME_KERNEL environment variable. Benchmark conducted on a high-performance Arch Linux x86_64 system with a 16-thread AMD Ryzen 7 7700 @ 5.4 GHz CPU, 64 GB of RAM, running Linux kernel 6.15.2, and ~350 running processes, shows roughly a 25% speedup over textual parsing of proc_pid_maps using the procfs
crate.

Finally, I feel deeply grateful for the work and the article. Thank you very much!

This is going to be released with Syd-3.36.1 soon. The game is on!

[1]: https://gitlab.exherbo.org/sydbox/sydbox/-/commit/d952e5d...
[2]: https://gitlab.exherbo.org/sydbox/sydbox/-/blob/main/src/...
[3]: https://landlock.io/
[4]: https://man.exherbo.org/syd.7.html#Syscall_Argument_Cookies
[5]: https://man.exherbo.org/syd-x.1.html

PROCMAP_QUERY ioctl in Rust for Syd on Linux>=6.11

Posted Jul 3, 2025 2:53 UTC (Thu) by irogers (subscriber, #121692) [Link] (1 responses)

There was some benchmarking done by Andrii Nakryiko with perf. Perf processes /proc/pid/maps a lot in order to be able to say which object file a sample is within. Perf has an internal benchmark to measure this performance, which over a year ago showed that processing with text was about 32ms per entry and similar with the ioctl:
https://lore.kernel.org/linux-perf-users/CAEf4BzbdGJzMuRg...

The text processing in perf uses a stack allocated buffer to read into and isn't doing things like fscanf. Perhaps we can explore the benchmark again to see if there is now a performance win by not using the text approach.

PROCMAP_QUERY ioctl in Rust for Syd on Linux>=6.11

Posted Jul 6, 2025 15:01 UTC (Sun) by alip (subscriber, #170176) [Link]

Thanks for the interesting input. The procfs Rust crate uses a standard buffered reader which typically allocates at least 2 pages of memory so I can imagine how perf's stack allocated buffer can make things faster. I have never gotten to writing our own proc_pid_maps parser which uses stack allocation like I had done with couple of procfs interfaces that Syd uses a lot (such as proc_pid_stat, proc_pid_status etc) so sadly I cannot provide more input on that.

Efficient Way Of Searching Source Code

Posted Jul 3, 2025 7:42 UTC (Thu) by taladar (subscriber, #68407) [Link] (2 responses)

I suspect the results will be just as disappointing as everything else LLMs have produced.

Efficient Way Of Searching Source Code

Posted Jul 5, 2025 8:04 UTC (Sat) by LtWorf (subscriber, #124958) [Link] (1 responses)

You don't enjoy made up flags that do not exist, or flags that exist but have an incorrect value?

I am every day more convinced that LLM proposers did not actually try to do the thing they propose.

Efficient Way Of Searching Source Code

Posted Jul 9, 2025 11:03 UTC (Wed) by cpitrat (subscriber, #116459) [Link]

That's why LLM are vastly superior, they do not only search present code and old code, they also return results for future code and code from parallel universes.

Golang project for PROCMAP_QUERY

Posted Jul 12, 2025 16:26 UTC (Sat) by rbranco (subscriber, #129813) [Link]

I created a Golang project for this:
https://github.com/ricardobranco777/go-procmap/

The interface is horrible imho.

In addition to the 3 system calls (because you need a file descriptor for `/proc/<pid>/maps`) you also need another one to call ioctl for each address. Too much overhead when the only advantage seems to be the build ID ELF note)

I expected something like a simple KERN_PROC_VMMAP FreeBSD sysctl (which currently lacks the ELF note).


Copyright © 2025, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds