Smatch: The Deep Analyzer



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.

 
Why Smatch? Going Beyond Type Checking

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.

 
The Conceptual Difference: Why Path-Sensitivity Matters

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.

 

What Smatch Tracks

Variable Ranges

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 */

 
Null Pointer Tracking Across Functions

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.


Lock State Analysis

Smatch tracks whether locks are held or released at each point in the code. It can detect:

  • Functions that acquire a lock but don’t release it on all error paths
  • Functions that release a lock they don’t hold
  • Potential deadlocks from double-locking
  • Sleeping inside spinlock-held regions
Resource Leak Detection

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.


Cross-Function Analysis via SQLite Database

Smatch builds a SQLite database of function properties during the database build phase. This database captures:

  • What values each function can return (e.g., NULL, negative error codes, valid pointers)
  • Which parameters must be non-null
  • Whether a function acquires or releases locks
  • What side effects does a function have on its arguments

This is what enables Smatch to track state across function call boundaries, making it far more powerful than tools limited to single-function analysis.

 

Installing Smatch

Prerequisites

sudo apt-get install gcc make sqlite3 libsqlite3-dev \

    libdbd-sqlite3-perl libssl-dev libtry-tiny-perl

 
Clone and Build

git clone https://repo.or.cz/smatch.git

cd smatch

make && sudo make install

 
Build the Kernel Database (Critical Step)

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.

 

Using Smatch Effectively

Quick Check on a Single File


cd /path/to/your/linux-kernel/source

/path/to/smatch/smatch_scripts/kchecker drivers/myvendor/mydriver/mydriver.c

 
Check an Entire Driver Directory

/path/to/smatch/smatch_scripts/kchecker drivers/myvendor/mydriver/

 
Integrate with Kernel Build (Full Analysis)

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.

 
Understanding Smatch Output

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.

 

Deep Dive: What Smatch Catches in Driver Code

Issue 1: Resource Leak on Error Path

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;

}

 
Issue 2: Lock Not Released on Error Path

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;

}

 
Issue 3: Null Pointer Dereference with Prior Check

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;

}

 
Issue 4: Out-of-Range Array Index

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;

}

 
Issue 5: Using ERR_PTR Without IS_ERR Check

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);

 
Issue 6: Integer Overflow in Size Calculation

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;

    …

}

 

Case Studies: Real Bugs Smatch Found

Case Study 1: Race Condition in Crypto Driver

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.

 
Case Study 2: DMA Timeout Not Propagated

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 */

 
Case Study 3: Catching a Deadlock in Multi-Application DMA

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.

 
Case Study 4: Resource Leak in Driver Probe with Multiple Resources

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;

}

 

Integrating Smatch Into Your Workflow

Development Workflow

Given Smatch’s higher overhead, integrate it strategically:

  • kchecker for quick file-level checks during active development
  • Full Smatch build before submitting any patch for review
  • Nightly CI job for full driver directory analysis with the database
  • Mandatory gate before merging to your integration branch

Triage Smatch Warnings Effectively

Smatch can generate many warnings, some of which are false positives. Here is how to triage them effectively:

  • Start with ‘error:’ severity warnings, not ‘warn:’ severity
  • Focus on null dereference and memory leak errors first (highest impact)
  • For lock errors, trace the full call path using the note: lines
  • Use git blame to understand context before fixing
  • Suppress known false positives with /* smatch: ignore */ comments (use sparingly)

Smatch vs Sparse vs Coccinelle: When to Use Which

Smatch vs Sparse vs Coccinelle

The Recommended Workflow: Using All Three Together

Now that you have all three tools in your toolkit, here is the recommended integrated workflow:

 
Step 1: During Development (Every Build)

 

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.

 
Step 2: Before Each Commit

 

# 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

 

Step 3: Before Patch Submission / PR

# 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

 

Step 4: Nightly CI

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.

 

Performance Considerations

  • Smatch initial database build: 30+ minutes, run once per kernel version
  • kchecker on a single file: seconds, use freely during development
  • Full Smatch build integration: similar to Coccinelle overhead (50-100% of compile time)
  • Recommendation: kchecker during development, full analysis in CI/CD

 

Summary: Completing Your Static Analysis Arsenal

With all three tools in hand, you now have a comprehensive static analysis system:

  • Coccinelle: Smart pattern matching that understands C semantics. Use it for API migrations, pattern detection, and encoding your own team rules.
  • Sparse: Fast type system enforcement. Catches __iomem misuse, user/kernel pointer confusion, and endianness bugs. Run on every build.
  • Smatch: Deep path-sensitive analysis. Catches resource leaks, lock imbalances, null pointer dereferences across functions, and integer overflows. Your last line of defense before submission.

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.

100% LikesVS
0% Dislikes

Author