This is Blog 3 of a 3-part series on Static Analysis Tools for Linux Device Driver Development.
Blog 1: Coccinelle — Semantic Patching & Code Transformation
Blog 2: Sparse — Type System Guardian
Blog 3 (This): Smatch — Deep Path-Sensitive Analysis
If you haven’t read Blogs 1 and 2 yet, we recommend starting there. Smatch builds on the foundation Sparse provides.
By now, you’ve used Sparse to enforce type safety and Coccinelle to detect pattern-based bugs. Both tools are excellent, but they have a fundamental limitation: they largely analyze code one statement at a time, without deep understanding of how values flow through your program across multiple code paths.
This is where Smatch steps in. Smatch performs path-sensitive analysis — it tracks the state of variables through every possible code path, understanding that a variable’s value after an if-statement is different on the true and false branches. This lets Smatch catch bugs that are fundamentally invisible to simpler tools.
Smatch was developed by Dan Carpenter, a prominent Linux kernel developer who has used it to find thousands of real bugs in the kernel over the years. It builds on Sparse’s parsing infrastructure but adds a whole new analysis engine on top.
Consider this code:
int *ptr = get_device_pointer();
if (ptr == NULL) {
pr_err(“No device!\n”);
}
ptr->status = STATUS_OK; /* Bug! */
Sparse might flag that ptr could be null from the return of get_device_pointer(). But Smatch goes further: it sees that you explicitly checked ptr == NULL in the if-block and did NOT return. Smatch knows that execution reaches ptr->status even when ptr is NULL. This is path-sensitive reasoning, and it’s why Smatch finds bugs the other tools miss.
Smatch tracks the possible integer range of variables. If a function returns a value that could be negative (an error code), Smatch knows this and will warn if you use that value in a context that requires a non-negative number:
int n = read_hw_register(dev, REG_COUNT);
/* Smatch knows n could be negative (error code) */
u8 buf[n]; /* Smatch: n could be negative, invalid VLA size */
Smatch tracks null pointers not just within a single function but across function calls, using a database of known function behaviors. It knows that kmalloc() can return NULL, that container_of() is always non-null if the member is non-null, and so on.
Smatch tracks whether locks are held or released at each point in the code. It can detect:
Smatch detects when allocated resources (memory, file handles, IRQs, device references) are not freed on all code paths. This is one of its most powerful capabilities for driver development.
Smatch builds a SQLite database of function properties during the database build phase. This database captures:
This is what enables Smatch to track state across function call boundaries, making it far more powerful than tools limited to single-function analysis.
sudo apt-get install gcc make sqlite3 libsqlite3-dev \
libdbd-sqlite3-perl libssl-dev libtry-tiny-perl
git clone https://repo.or.cz/smatch.git
cd smatch
make && sudo make install
This is what makes Smatch powerful. The database build analyzes the entire kernel and creates a function property database:
cd /path/to/your/linux-kernel/source
/path/to/smatch/smatch_scripts/build_kernel_data.sh
This step takes 30+ minutes and requires significant disk space (~2GB). Run it once after checking out a new kernel version and again when you upgrade. The resulting smatch_db.sqlite file is what enables cross-function analysis.
Tip: If you’re working on multiple kernel versions, keep separate smatch database files and point the SMATCH_DB environment variable at the right one for each project.
cd /path/to/your/linux-kernel/source
/path/to/smatch/smatch_scripts/kchecker drivers/myvendor/mydriver/mydriver.c
/path/to/smatch/smatch_scripts/kchecker drivers/myvendor/mydriver/
make CHECK=”/path/to/smatch/smatch –full-path” \
CC=”/path/to/smatch/cgcc” | tee smatch_warnings.txt
The kchecker script is perfect for quick iterations during development. The full build integration provides comprehensive cross-functional analysis using the database.
Smatch provides detailed warnings with full context:
drivers/example/mydriver.c:123 my_function() error: potential null dereference ‘ptr’.
drivers/example/mydriver.c:115 my_function() note: pointer ‘ptr’ was previously
checked for null
The note lines are especially valuable. They tell you not just where the bug is, but the context that led Smatch to conclude there is a bug. In this case, you checked for null earlier (meaning you knew it could be null), yet you still dereference it without a return.
This is one of the most common bugs in driver probe functions and one of Smatch’s specialties:
static int my_driver_probe(struct platform_device *pdev) {
struct my_driver *drv;
int ret;
drv = kzalloc(sizeof(*drv), GFP_KERNEL);
if (!drv)
return -ENOMEM;
ret = request_irq(pdev->irq, my_irq_handler, 0, “mydrv”, drv);
if (ret) {
return ret; /* Smatch: memory leak! drv not freed here */
}
ret = register_with_subsystem(drv);
if (ret) {
free_irq(pdev->irq, drv);
return ret; /* Smatch: memory leak! drv not freed here */
}
platform_set_drvdata(pdev, drv);
return 0;
}
Smatch traces every return path and verifies that every allocated resource is freed. The output:
drivers/myvendor/mydriver.c:18 my_driver_probe() error: memory leak of ‘drv’
drivers/myvendor/mydriver.c:24 my_driver_probe() error: memory leak of ‘drv’
Correct version with proper error path cleanup:
static int my_driver_probe(struct platform_device *pdev) {
struct my_driver *drv;
int ret;
drv = kzalloc(sizeof(*drv), GFP_KERNEL);
if (!drv)
return -ENOMEM;
ret = request_irq(pdev->irq, my_irq_handler, 0, “mydrv”, drv);
if (ret)
goto err_free;
ret = register_with_subsystem(drv);
if (ret)
goto err_irq;
platform_set_drvdata(pdev, drv);
return 0;
err_irq:
free_irq(pdev->irq, drv);
err_free:
kfree(drv);
return ret;
}
Smatch’s lock state tracking catches this class of bugs that cause deadlocks under specific conditions:
static int dma_transfer_start(struct dma_controller *ctrl,
struct dma_request *req) {
spin_lock_irqsave(&ctrl->lock, ctrl->flags);
if (!ctrl->hw_ready) {
dev_err(ctrl->dev, “HW not ready\n”);
return -EBUSY; /* Smatch: lock not released! */
}
if (req->len > MAX_DMA_SIZE) {
return -EINVAL; /* Smatch: lock not released! */
}
setup_dma_descriptor(ctrl, req);
spin_unlock_irqrestore(&ctrl->lock, ctrl->flags);
return 0;
}
Smatch output:
drivers/dma/mydriver.c:45 dma_transfer_start() error: lock ‘ctrl->lock’
held on return at line 41
drivers/dma/mydriver.c:48 dma_transfer_start() error: lock ‘ctrl->lock’
held on return at line 44
Fix:
static int dma_transfer_start(struct dma_controller *ctrl,
struct dma_request *req) {
int ret = 0;
spin_lock_irqsave(&ctrl->lock, ctrl->flags);
if (!ctrl->hw_ready) {
ret = -EBUSY;
goto out;
}
if (req->len > MAX_DMA_SIZE) {
ret = -EINVAL;
goto out;
}
setup_dma_descriptor(ctrl, req);
out:
spin_unlock_irqrestore(&ctrl->lock, ctrl->flags);
return ret;
}
This is Smatch’s signature case: you checked for null and then still dereferenced on a code path where the pointer could be null:
static int configure_device(struct my_dev *dev) {
struct hw_config *cfg = get_hw_config(dev);
/* Developer added this check because they knew it could fail */
if (cfg == NULL)
pr_warn(“No config, using defaults\n”);
/* Notice: no return here! */
/* Smatch: cfg could be NULL here, you checked for it above! */
dev->clock_rate = cfg->clock_hz;
dev->burst_len = cfg->burst_size;
return 0;
}
Smatch output:
drivers/myvendor/mydriver.c:12 configure_device() error: potential null
dereference ‘cfg’.
drivers/myvendor/mydriver.c:5 configure_device() note: ‘cfg’ was checked
for NULL here.
The fix depends on intent. If the defaults should be used when cfg is NULL:
static int configure_device(struct my_dev *dev) {
struct hw_config *cfg = get_hw_config(dev);
if (cfg == NULL) {
dev->clock_rate = DEFAULT_CLOCK_HZ;
dev->burst_len = DEFAULT_BURST_SIZE;
return 0;
}
dev->clock_rate = cfg->clock_hz;
dev->burst_len = cfg->burst_size;
return 0;
}
Smatch tracks variable ranges and can detect array index out-of-bounds access:
#define MAX_CHANNELS 8
static int select_channel(struct dma_ctrl *ctrl, int ch) {
/* ch comes from user ioctl, could be anything */
return ctrl->channel[ch].status; /* Smatch: ch could be negative or > 7 */
}
Smatch knows that ch is an unchecked external value and flags it. Fix:
static int select_channel(struct dma_ctrl *ctrl, int ch) {
if (ch < 0 || ch >= MAX_CHANNELS)
return -EINVAL;
return ctrl->channel[ch].status;
}
Many kernel functions return error-encoded pointers using ERR_PTR(). Smatch knows this pattern and warns when you use such a pointer without checking:
struct clk *clk;
clk = devm_clk_get(dev, “ahb”);
/* Smatch: clk could be ERR_PTR, must check IS_ERR() before use */
clk_prepare_enable(clk); /* Crash if clk is ERR_PTR(-ENOENT) */
Fix:
struct clk *clk;
clk = devm_clk_get(dev, “ahb”);
if (IS_ERR(clk)) {
dev_err(dev, “Failed to get clock: %ld\n”, PTR_ERR(clk));
return PTR_ERR(clk);
}
clk_prepare_enable(clk);
DMA transfers often involve size calculations that can overflow if not validated:
static int dma_alloc_scatter_gather(struct sg_table *sgt,
u32 nents, u32 ent_size) {
/* Smatch: nents * ent_size could overflow u32! */
void *buf = kmalloc(nents * ent_size, GFP_KERNEL);
if (!buf)
return -ENOMEM;
…
}
Fix using size_mul() to detect overflow:
static int dma_alloc_scatter_gather(struct sg_table *sgt,
u32 nents, u32 ent_size) {
size_t total;
void *buf;
if (check_mul_overflow((size_t)nents, (size_t)ent_size, &total))
return -EOVERFLOW;
buf = kmalloc(total, GFP_KERNEL);
if (!buf)
return -ENOMEM;
…
}
In a crypto hardware driver, a common pattern is to register algorithm instances during probe and manage them in a context structure. Smatch caught a race condition where a context was freed while still being used:
/* Bug: Context freed before all users are done */
static void <drv_name>_remove(struct platform_device *pdev) {
struct <drv_name>_dev *ptr = platform_get_drvdata(pdev);
/* Unregister algorithms first… */
crypto_unregister_algs(ptr->algs, ptr->num_algs);
/* But there could still be in-flight operations using ptr! */
kfree(ptr); /* Smatch flags potential use-after-free */
}
Smatch’s cross-function analysis, combined with lock tracking, identified that the completion callbacks from in-flight operations could still access ptr after it was freed. The fix involved proper reference counting and waiting for in-flight operations to complete.
In a DMA controller driver with synchronous transfers, a timeout condition was detected, but the error code was not returned to the caller:
static int wait_for_dma_complete(struct dma_channel *ch,
unsigned long timeout_jiffies) {
int ret;
ret = wait_for_completion_timeout(&ch->done, timeout_jiffies);
if (ret == 0) {
dev_err(ch->dev, “DMA timeout!\n”);
dma_reset_channel(ch);
/* Bug: Smatch sees ret is 0 here, but we return without setting error */
}
return ret; /* Returns 0 on timeout, caller might think success! */
}
Smatch flagged that ret == 0 (timeout) is being returned as if it were a success. The caller checks if (ret < 0) for errors and treats 0 as success. Fix:
if (ret == 0) {
dev_err(ch->dev, “DMA timeout!\n”);
dma_reset_channel(ch);
return -ETIMEDOUT; /* Return proper error code */
}
return 0; /* Success */
In a multi-application resource management scenario with nested locks, Smatch’s lock tracking detected a potential deadlock:
/* Lock order: must always be global_lock then channel_lock */
void allocate_dma_channel(struct crm_daemon *crm, int app_id) {
spin_lock(&crm->global_lock);
/* … find available channel … */
spin_lock(&channel->ch_lock); /* OK, correct order */
/* … allocate … */
spin_unlock(&channel->ch_lock);
spin_unlock(&crm->global_lock);
}
void release_dma_channel(struct dma_channel *ch) {
spin_lock(&ch->ch_lock); /* Takes channel lock FIRST */
/* … */
spin_lock(&ch->crm->global_lock); /* Smatch: ABBA deadlock! */
/* … */
}
Smatch detected the ABBA lock ordering violation and flagged it. Thread A holds global_lock and waits for ch_lock. Thread B holds ch_lock and waits for global_lock. Deadlock.
A typical embedded driver probe allocates multiple resources. Smatch traces all failure paths:
static int ewmu_probe(struct platform_device *pdev) {
struct ewmu_dev *ewmu;
struct resource *res;
int ret;
ewmu = kzalloc(sizeof(*ewmu), GFP_KERNEL);
if (!ewmu) return -ENOMEM;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
ewmu->base = ioremap(res->start, resource_size(res));
if (!ewmu->base) {
/* Smatch: ewmu leaked! */
return -ENOMEM;
}
ret = request_irq(pdev->irq, ewmu_irq, 0, “ewmu”, ewmu);
if (ret) {
/* Smatch: ewmu AND ewmu->base leaked! */
return ret;
}
return 0;
}
Given Smatch’s higher overhead, integrate it strategically:
Smatch can generate many warnings, some of which are false positives. Here is how to triage them effectively:

Now that you have all three tools in your toolkit, here is the recommended integrated workflow:
make C=1 CHECK=”sparse” M=drivers/myvendor/mydriver/
Run Sparse on every build. Zero tolerance for Sparse warnings. This catches type errors immediately while the code is fresh.
# Coccinelle – check against kernel semantic patches
make C=1 CHECK=”scripts/coccicheck” M=drivers/myvendor/mydriver/
# Smatch quick check on changed files
/path/to/smatch/smatch_scripts/kchecker drivers/myvendor/mydriver/changed_file.c
# Full Sparse
make C=2 CHECK=”sparse” M=drivers/myvendor/mydriver/
# Full Coccinelle with all kernel scripts
make C=2 CHECK=”scripts/coccicheck” M=drivers/myvendor/mydriver/
# Full Smatch with database
make CHECK=”/path/to/smatch/smatch –full-path” \
CC=”/path/to/smatch/cgcc” \
M=drivers/myvendor/mydriver/ | tee smatch_final.txt
Full analysis of the entire driver tree with all three tools, archived results, and trend tracking. Any new warning introduced in a commit fails the nightly build.
With all three tools in hand, you now have a comprehensive static analysis system:
Series Complete! You have now completed the 3-part series on Static Analysis Tools for Linux Driver Developers. Blog 1: Coccinelle — The Semantic Patch Master Blog 2: Sparse — The Type System Guardian Blog 3: Smatch — The Deep Analyzer
Start integrating these into your workflow today. The best bug is the one you never ship. |