How I became a Linux Kernel Contributor

RSS Feed
January 12, 2026 10m Linux Kernel

Since I started programming in C, it has been a life-long goal for me to contribute to the Linux kernel, since it's the biggest, most challenging, and arguably most successful Open Source project in the world.

It has thousands of contributors per release, where a big percentage of contributors work full-time on the kernel, this makes core parts of the kernel have extremely high code quality.

contributors per release

Overview of the kernel

But code quality depends on the subsystem you are working on and how well maintained it is. For those who don't know, the kernel is divided per feature type aka "subsystem", and each feature type that gets big enough becomes a subsystem, each has one to two maintainers and a bunch of reviewers.

S-tier (Excellent):
├── kernel/          (scheduler, RCU, locking, core)
├── mm/              (memory management)

A-tier (High quality):
├── fs/              (VFS core)
├── net/             (networking core)
├── block/           (block layer)
├── lib/             (kernel library functions)
├── crypto/          (crypto API)
└── security/        (security frameworks like SELinux, AppArmor)

B-tier (Good):
└── drivers/         (varies widely by driver)

Most core subsystems are S to A tier in terms of code quality, like kernel, mm, fs, net, etc, but drivers on average are lower quality, this actually is the place where most of the code lives. Meanwhile core parts of the kernel only occupy around 20% of the total lines of code, drivers take 80%!

pie chart comparing core vs drivers total LOC

Just so you can imagine, a single driver like AMD Radeon contains ~5M lines alone!

S-tier:
├── drivers/gpu/drm/              (graphics - especially core DRM)
└── drivers/net/ethernet/         (varies by driver, some excellent)

A-tier:
├── drivers/nvme/                 (NVMe subsystem)
├── drivers/pci/                  (PCI core)
└── drivers/usb/                  (USB core, driver quality varies)

B-tier:
├── drivers/block/                (block device drivers)
├── drivers/scsi/                 (SCSI subsystem)
└── drivers/ata/                  (ATA/SATA)

D-tier:
└── drivers/staging/              (staging area for unmaintained/low-quality code)

Each driver group has its own subsystem and each has different code quality which depends on the maintainers and vendors.

Among all of these, the lowest quality ones live in drivers/staging/, which contains unmaintained and low quality drivers for all the subsystems, these usually need big changes for promotion, as well as documentation and testing.

Approaching the kernel

Knowing this, you can imagine that the lower the code quality, the easier it is to find stuff to contribute to, and this is how you delve your way into kernel hacking.

Starting by contributing to the core parts of the kernel is almost impossible, because you most likely don't know how they work (yet)!

A lot of people recommend starting to contribute by adding or changing documentation, but the reality is that to write good documentation you must understand the feature/driver you are documenting, which is pretty hard as a beginner.

The best way to start I'd say is by doing janitor work where you basically clean up unmaintained code. The reason why it's a great way to introduce yourself into the kernel is because you usually don't need much context on what you are changing, a list of things you can look for are:

Removing unused dependencies:

#include <linux/bitfield.h>
#include <linux/bits.h>          // NOT USED
#include <linux/cleanup.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <linux/err.h>           // NOT USED
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/regulator/consumer.h>
#include <linux/slab.h>
#include <linux/spi/spi.h>
#include <linux/sysfs.h>

Using Guards for locking or RAII for memory management:

// old way - manual lock/unlock
mutex_lock(&chip->lock);
for (i = 0; i < ARRAY_SIZE(isl29018_scales[chip->int_time]); ++i)
    len += sysfs_emit_at(buf, len, "%d.%06d ",
                        isl29018_scales[chip->int_time][i].scale,
                        isl29018_scales[chip->int_time][i].uscale);
mutex_unlock(&chip->lock);

// new way - automatic with guard
guard(mutex)(&chip->lock);
for (i = 0; i < ARRAY_SIZE(isl29018_scales[chip->int_time]); ++i)
    len += sysfs_emit_at(buf, len, "%d.%06d ",
                        isl29018_scales[chip->int_time][i].scale,
                        isl29018_scales[chip->int_time][i].uscale);
// mutex_unlock happens automatically when guard goes out of scope

Static analyzers:

- Compiler warnings (compile with `W=1`)
- Sparse/Smatch static analyzer warnings
- `./scripts/checkpatch.pl` warnings
- Syzkaller fuzzing results (more advanced)

These are all mechanical changes that will teach you the workflow and won't need you to know a huge amount of context.

My Journey

In my case, the first contribution I made was pretty simple, in the subsystem I wanted to contribute to, I saw they were migrating from the unsafe function sprintf() to sysfs_emit() so I made the change where these functions were being used and sent the patch to the mailing list.

--- a/drivers/iio/light/isl29018.c
+++ b/drivers/iio/light/isl29018.c
@@ -273,7 +273,7 @@ static ssize_t in_illuminance_scale_available_show
 
 	mutex_lock(&chip->lock);
 	for (i = 0; i < ARRAY_SIZE(isl29018_scales[chip->int_time]); ++i)
-		len += sprintf(buf + len, "%d.%06d ",
+		len += sysfs_emit_at(buf, len, "%d.%06d ",
 			       isl29018_scales[chip->int_time][i].scale,
 			       isl29018_scales[chip->int_time][i].uscale);
 	mutex_unlock(&chip->lock);
@@ -293,7 +293,7 @@ static ssize_t in_illuminance_integration_time_available_show
 	int len = 0;
 
 	for (i = 0; i < ARRAY_SIZE(isl29018_int_utimes[chip->type]); ++i)
-		len += sprintf(buf + len, "0.%06d ",
+		len += sysfs_emit_at(buf, len, "0.%06d ",
 			       isl29018_int_utimes[chip->type][i]);
 
 	buf[len - 1] = '\n';
@@ -330,7 +330,7 @@ static ssize_t proximity_on_chip_ambient_infrared_suppression_show
 	 * Return the "proximity scheme" i.e. if the chip does on chip
 	 * infrared suppression (1 means perform on chip suppression)
 	 */
-	return sprintf(buf, "%d\n", chip->prox_scheme);
+	return sysfs_emit(buf, "%d\n", chip->prox_scheme);
 }
 
 static ssize_t proximity_on_chip_ambient_infrared_suppression_store

For those who don't know, the linux kernel predates github, since linus created git himself (heh), and at the time a lot of communication happened through mailing lists. So to send your commits you format them into patches and send them through email with git (yes git can send emails).

git format-patch -1

git send-email --confirm=always \
               --to='maintainer@example.com' \
               --cc='reviewer@example.com' \
               patch-file.patch

Before sending a patch, you are supposed to check with a script who are the maintainers and reviewers of a file:

lewboski@Lewboski:~/programming/learn/linux$ ./scripts/get_maintainer.pl drivers/iio/light/isl29018.c
Jonathan Cameron <jic23@kernel.org> (maintainer:IIO SUBSYSTEM AND DRIVERS)
David Lechner <dlechner@baylibre.com> (reviewer:IIO SUBSYSTEM AND DRIVERS)
"Nuno Sá" <nuno.sa@analog.com> (reviewer:IIO SUBSYSTEM AND DRIVERS)
Andy Shevchenko <andy@kernel.org> (reviewer:IIO SUBSYSTEM AND DRIVERS)
linux-iio@vger.kernel.org (open list:IIO SUBSYSTEM AND DRIVERS)
linux-kernel@vger.kernel.org (open list)

So the command for us would be:

git send-email --confirm=always \
               --to='Jonathan Cameron <jic23@kernel.org>' \
               --cc='David Lechner <dlechner@baylibre.com>' \
               --cc='"Nuno Sá" <nuno.sa@analog.com>' \
               --cc='Andy Shevchenko <andy@kernel.org>' \
               --cc='linux-iio@vger.kernel.org' \
               --cc='linux-kernel@vger.kernel.org' \
               test.patch

And with it you send it to the maintainers and cc the rest of the reviewers. At the time I copied all of them and sent them as shown in the script but accidentally added nonsense.

From: Tomas Borquez <tomasborquez13@gmail.com>
To: jic23@kernel.org (maintainer:IIO SUBSYSTEM AND DRIVERS,added_lines:860/863=100%),
    tomasborquez13@gmail.com 
    (authored:1/1=100%,added_lines:860/863=100%,removed_lines:3/3=100%),
    linux-iio@vger.kernel.org (open list:IIO SUBSYSTEM AND DRIVERS),
    linux-kernel@vger.kernel.org (open list)
Cc: [more of the same mess]
Subject: [PATCH] iio: isl29018.c: replace sprintf with safer alternatives

As you can see here, I even sent it to myself and other invalid emails? So I quickly sent the v2 of the patch and after just 2h I got a response from a very active reviewer:

v2 response from reviewer

He kindly advised me on how I should format my description and my code, to which I followed his feedback and created v3 and sent it. And here I made yet another mistake, since I resent the patch even though all those changes could have been applied manually by the maintainer.

v3 response from reviewer

Then around two weeks later I received a message from the maintainer saying he merged my patch and I was welcomed to the community warmly :D

Here I learned the workflow for sending a patch, responding to feedback and resending subsequent versions, and also learned not to add noise to the mailing list, since maintainers have huge backlogs and are usually very busy.

> Updated both commit message and indentation in v3
Hi Tomas

I moaning a bit about this today as I have a very long
email backlog.  This comment belongs only in the changelog
of v3.  Sending a reply here just adds noise to the email
queue of reviewers. i.e. Don't reply to just say this.

Jonathan

Better Contributions

After my first patch, I wanted to tackle something more meaningful. But for that I needed to understand drivers better and the different APIs of the kernel.

For this, I read the LDD3 which goes over the core driver API and helps you understand how everything connects.

This book is pretty old (2006) but the fundamentals of the API have not changed much. Then I learned the abstractions of my subsystem of choice, pretty much every subsystem will have its own abstractions over the core driver API which makes it a lot easier to write drivers.

For example here is how you would write a driver with the Core driver API:

static int temp_probe(struct platform_device *pdev)
{
    int ret;

    temp_base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(temp_base))
        return PTR_ERR(temp_base);

    ret = alloc_chrdev_region(&temp_devt, 0, 1, "temp_sensor");
    if (ret < 0)
        return ret;

    temp_class = class_create("temp_sensor");
    if (IS_ERR(temp_class)) {
        unregister_chrdev_region(temp_devt, 1);
        return PTR_ERR(temp_class);
    }

    cdev_init(&temp_cdev, &temp_fops);
    temp_cdev.owner = THIS_MODULE;

    ret = cdev_add(&temp_cdev, temp_devt, 1);
    if (ret < 0) {
        class_destroy(temp_class);
        unregister_chrdev_region(temp_devt, 1);
        return ret;
    }

    temp_device = device_create(temp_class, &pdev->dev,
                                temp_devt, NULL, "temp_sensor");
    if (IS_ERR(temp_device)) {
        cdev_del(&temp_cdev);
        class_destroy(temp_class);
        unregister_chrdev_region(temp_devt, 1);
        return PTR_ERR(temp_device);
    }

    return 0;
}

And this would be how you would do it with the IIO subsystem abstraction:

static int temp_probe(struct platform_device *pdev)
{
    struct iio_dev *indio_dev;
    struct temp_device *dev;

    indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*dev));
    if (!indio_dev)
        return -ENOMEM;

    dev = iio_priv(indio_dev);
    dev->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(dev->base))
        return PTR_ERR(dev->base);

    indio_dev->name = "temp_sensor";
    indio_dev->info = &temp_info;
    indio_dev->channels = temp_channels;
    indio_dev->num_channels = 1;

    return devm_iio_device_register(&pdev->dev, indio_dev);
}

Then I started to read the mailing list every day to see what types of changes people were making and what was left to do, as well as learning typical mistakes made when contributing.

rejected patch thread

And after digging for a while, I found someone trying to fix a TODO but their patch not being accepted because other changes were expected instead, so I decided to take that staging driver and fix that TODO + modernize it.

When making big changes, you are usually expected to have the hardware to test devices you are writing drivers for, but if you set up a VM and make sure to test your changes thoroughly by stubbing, then maintainers will usually be okay with it, just never lie about having the hardware.

But if the changes you are making don't change functionality of the driver nor the ABI then having the hardware is not necessary either.

High-level Roadmap

If you find it interesting and want to know what you need to learn to make your first Linux Kernel contribution here are a list of things you need to know:

Intermediate C knowledge

You should be familiar with C, and know how macros, gotos, and some GNU extensions work, since they are used heavily throughout the codebase. For example:

Cleanup macros (__free and guard): The __free attribute automatically frees memory when a variable goes out of scope:

// memory is automatically freed when dsm goes out of scope
struct tio_dsm *dsm __free(kfree) = kzalloc(sizeof(*dsm), GFP_KERNEL);
if (!dsm)
    return -ENOMEM;

// the macro definitions look like this:
#define __free(_name)  __cleanup(__free_##_name)

And the guard macro for automatic lock/unlock:

#define guard(_name) \
    CLASS(_name, __UNIQUE_ID(guard))

Utility macros:

#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

Or macros for creating channel definitions:

#define AD9832_CHAN_PHASE(_name, _select) {      \
    .name = _name,                                \
    .write = ad9832_write_phase,                  \
    .read = ad9832_read_phase,                    \
    .private = _select,                           \
    .shared = IIO_SEPARATE,                       \
}

Error handling with goto: goto are used a lot for cleanup though can be removed with RAII:

if (ret < 0)
    goto out;

// ...

out:
    iio_device_release_direct(indio_dev);
    return ret;

Compiler optimization hints:

#define LIKELY(x)   (__builtin_expect((x), 1))
#define UNLIKELY(x) (__builtin_expect((x), 0))

if (UNLIKELY(ret < 0))
    goto error;

Git knowledge

Apart from basic git knowledge, you should set up your git mail and learn how to send patches.

Kernel Scripts and Make

And finally learn to use these two kernel scripts, which will help you check style guides for your code, and get the maintainers you need to send your patches to, as well as some basic make experience to be able to build the kernel, your own changes and check for errors.

# Check code style
./scripts/checkpatch.pl your-patch.patch

# Find maintainers
./scripts/get_maintainer.pl path/to/file.c

# Compile with extra warnings and static analysis
make W=1 C=2 drivers/staging/iio/frequency/ad9832.o

As for advanced material, after you make some contributions and read the docs you'll have a pretty good idea of what you have to learn.

Like static analysis tools, using b4 for sending your patches, QEMU for testing your changes and stubbing, GDB for debugging stuff.

You can find comprehensive documentation at the kernel development process guide.

Conclusion

What I want you to take away from this is that you don't need to be a genius to contribute to the Linux Kernel nor have huge amounts of knowledge, you will slowly become better over time and contribute better and better patches as you continue learning.

I hope you enjoyed reading and consider subscribing to the newsletter if you did :D