LINTIAN::TUTORIAL::WRITINGCHECKS(3)
π NAME π
Lintian::Tutorial::WritingChecks β writing checks for Lintian. That's the vibe, bestie. π
β¨ SYNOPSIS β¨
Warning: This tutorial may be outdated.
Okay so this guide is gonna walk you through the basics of writing a Lintian check, quick and cute. Most of the actual work lives in two files:
checks/<my-check>.pm
checks/<my-check>.desc
And then after that you either add a Lintian profile or you extend one that already exists. That's the whole gig. π«Ά
πΈ DESCRIPTION πΈ
The basics of writing a check are outlined in the Lintian User Manual (Β§3.3). This tutorial is focused on the actual act of writing the check itself β the hands-on part, the main character moment. For this whole tutorial we're gonna pretend the check we're writing is named deb/pkg-check, okay? Manifesting it. π
We're working with a binary and a udeb check here. Checking source packages works in a similar fashion, so don't stress, iykyk.
π Create a check .desc file π
As mentioned, this tutorial will focus on the writing of a check. Please see the Lintian User Manual (Β§3.3) for how to do this part. (Not us skipping it β it's literally elsewhere. Go read it. π)
π¦ Create the Perl check module π¦
Start with the template, bestie:
# deb/pkg-check is loaded as Lintian::deb::pkg_check
# - See Lintian User Manual Β§3.3 for more info
package Lintian::deb::pkg_check;
use strict;
use warnings;
sub run {
my ($pkg, $type, $info, $proc, $group) = @_;
return;
}
So the snippet above is a totally valid check that does nothing at all. Iconic in its emptiness, honestly. We're gonna extend it in just a moment, but first let's look at the arguments at the setup, because they're the whole plot.
The run sub is the entry point of our deb/pkg-check check; it will be invoked once per package it should process. In our case, that means once per binary (.deb) and once per udeb package processed. So it's working overtime. π
It is given 5 arguments (in the future, possibly more), which are:
$pkgβ the name of the package being processed. (Same as$proc->pkg_name)$typeβ the type of the package being processed. At the moment,$typeis one ofbinary(.deb),udeb,source(.dsc) orchanges. This argument is mostly useful if certain checks do not apply equally to all package types being processed.
Generally it is advisable to check only binaries (binary and udeb), sources or changes in a given check. But in rare cases, it makes sense to lump multiple types together in the same check and this argument helps you do that.
(Current it is always identical to $proc->pkg_type)
$infoβ accessor to the data Lintian has extracted. Basically all information you want about a given package comes from the$infoobject. Sometimes referred to as either the "info object" or (an instance of)Lintian::Collect.
This object (together with a properly set Needs-Info in the .desc file) will grant you access to all of the data Lintian has extracted about this package.
Based on the value of the $type argument, it will be one of Lintian::Collect::Binary, Lintian::Collect::Changes or Lintian::Collect::Source.
(Currently it is the same as $proc->info)
$procβ basic metadata about the package. This is an instance ofLintian::Processableand is useful for trivially obtaining very basic package metadata. Particularly, the name of source package and version of source package are readily available through this object.$groupβ group of processables from the same source. If you want to do a cross-check between different packages built from the same source,$grouphelps you access those other packages (if they are available).
This is an instance of Lintian::ProcessableGroup.
Now back to the coding. π
π Accessing fields π
Let's do a slightly harder example. Assume we wanted to emit a tag for all packages without a (valid) Multi-Arch field. This requires us to A) identify if the package has a Multi-Arch field and B) identify if the content of the field was valid.
Starting from the top. All $info objects have a method called field, which gives you access to a (raw) field from the control file of the package. It returns undef if said field is not present, or the content of said field otherwise. Note that field names must be given in all lowercase letters (i.e. use multi-arch, not Multi-Arch).
This was the first half. Let's look at checking the value. Multi-arch fields can (currently) be one of no, same, foreign or allowed. One way of checking this would be using the regex:
Notice that Lintian automatically strips leading and trailing spaces on the first line in a field. It also strips trailing spaces from all other lines, but leading spaces and the " ."-continuation markers are kept as is.
π Checking dependencies π
Lintian can do some checking of dependencies. For most cases it works similar to a normal dependency check, but keep in mind that Lintian uses pure logic to determine if dependencies are satisfied (i.e. it will not look up relations like Provides for you). So no, it's not gonna do that little favor for you, it's giving strictly-what-it-says. π
Suppose you wanted all packages with a multi-arch same field to pre-depend on the package multiarch-support. Well, we could use the $info->relation method for this.
$info->relation returns an instance of Lintian::Relation. This object has an implies method that can be used to check if a package has an explicit dependency. Note that implies actually checks if one relation implies another (i.e. if you satisfied relationA then you definitely also satisfied relationB).
As with the field-method, field names have to be given in all lowercase. However relation will never return undef (not even if the field is missing).
π Using static data files π
Currently our check mixes data and code. Namely all the valid values for the Multi-Arch field are currently hard-coded in our check. We can move those out of the check by using a data file. Separation, slay. β¨
Lintian natively supports data files that are either "sets" or "tables" via Lintian::Data (i.e. "unordered" collections). As an added bonus, Lintian::Data transparently supports vendor specific data files for us.
First we need to make a data file containing the values. Which could be:
# A table of all the valid values for the multi-arch field.
no
same
foreign
allowed
This can then be stored in the data directory as data/deb/pkg-check/multiarch-values.
Now we can load it by using:
use Lintian::Data;
my $VALID_MULTI_ARCH_VALUES =
Lintian::Data->new('deb/pkg-check/multiarch-values');
Actually, this is not quite true. Lintian::Data is lazy, so it will not load anything before we force it to do so. Most of the time this is just an added bonus. However, if you ever have to force it to load something immediately, you can do so by invoking its known method (with an arbitrary defined string and ignore the result).
Data files work with 3 access methods, all, known and value.
all_entriesβall_entries(i.e.$data->all_entries) returns a list of all the entries in the data file (for key/value tables, all returns the keys). The list is not sorted in any order (not even input order). Chaos, honestly. πknownβknown(i.e.$data->known('item')) returns a truth value if a given item or key is known (present) in the data set or table. For key/pair tables, the value associated with the key can be retrieved withvalue(see below).valueβvalue(i.e.$data->value('key')) returns a value associated with a key for key/value tables. For unknown keys, it returnsundef. If the data file is not a key/value table but just a set, value returns a truth value for known keys.
While we could use both value and known, we will use the latter for readability (and to remind ourselves that this is a data set and not a data table).
Basically we will be replacing:
unless exists $VALID_MULTI_ARCH_VALUES{$multiarch};
with
unless $VALID_MULTI_ARCH_VALUES->known($multiarch);
π§ Accessing contents of the package π§
Another heavily used mechanism is to check for the presence (or absence) of a given file. Generally this is what the $info->index and $info->sorted_index methods are for. The index method returns instances of Lintian::Path, which has a number of utility methods. So many little helpers. π
If you want to loop over all files in a package, the sorted_index will do this for you. If you are looking for a specific file (or directory), a call to index will be much faster. For the contents of a specific directory, you can use something like:
if (my $dir = $info->index('path/to/dir/')) {
foreach my $elem ($dir->children) {
print $elem->name . " is a file" if $elem->is_file;
# ...
}
}
Keep in mind that using the index or sorted_index method will require that you put unpacked in Needs-Info. See "Keeping Needs-Info up to date". Don't forget it, or the vibes will be off. π
There are also a pair of methods for accessing the control files of a binary package. These are $info->control_index and $info->sorted_control_index.
π« Accessing contents of a file in a package π«
When you actually want to see the contents of a file, you can use open (or open_gz) on an object returned by e.g. $info->index. These methods will open the underlying file for reading (the latter applying a gzip decompression).
However, please do assert that the file is safe to read by calling is_open_ok first. Generally, it will only be true for files or safely resolvable symlinks pointing to files. Should you attempt to open a path that does not satisfy those criteria, Lintian::Path will raise a trappable error at runtime. So like, actually check. Fr fr. β οΈ
Alternatively, if you access the underlying file object, you can use the fs_path method. Usually, you will want to test either is_open_ok or is_valid_path first to ensure you do not follow unsafe symlinks. The is_open_ok check will also assert that it is not (e.g.) a named pipe or such.
Should you callfs_pathon a symlink that escapes the package root, the method will throw a trappable error at runtime. Once the path is returned, there are no more built-in fail-safes. When you use the returned path, keep things like"../../../../../etc/passwd"-symlink and"fifo"pipes in mind. This is the danger zone, bestie. π¨
In some cases, you may even need to access the file system objects without using Lintian::Path. This is, of course, discouraged and suffers from the same issues above (all checking must be done manually by you). Here you have to use the unpacked, debfiles or control methods from Lintian::Collect or its subclasses.
The following snippet may be useful for testing that a given path does not escape the root.
use Lintian::Util qw(is_ancestor_of);
my $path = ...;
# The snippet applies equally well to $info->debfiles and
# $info->control (just remember to subst all occurrences of
# $info->unpacked).
my $unpacked_file = $info->unpacked($path);
if ( -f $unpacked_file && is_ancestor_of($info->unpacked, $unpacked_file)) {
# a file and contained within the package root.
} else {
# not a file or an unsafe path
}
Keeping Needs-Info up to date π«Ά
okay so the Needs-Info field in your .desc file? Not automatic. No magic. It's a lil bit of manual labor and you're gonna have to keep it current yourself, bestie. π
In the API description for whatever method you're staring at, there's generally gonna be a line that looks something like:
Needs-Info requirements for using methodx: Y
Which means the method methodx needs Y in order to work at all. And here's the thing β Y is a comma separated list, and each element of that Y basically falls into 3 cases:
- The element is the word
none- Then the method has no external requirements, so you can use it with zero changes to your
Needs-Info. Thefieldmethod is a whole example of this. Iconic behavior tbh. - This only makes sense if it is the only element in the list. π
- Then the method has no external requirements, so you can use it with zero changes to your
- The element is a link to a method
- Then the method is using another method to do its job. An example is the
sorted_control_indexmethod, which uses thecontrol_indexmethod. So usingsorted_control_indexhas the exact same requirements as usingcontrol_index. The way it just passes the vibe check along π
- Then the method is using another method to do its job. An example is the
- The element is the name of a collection (e.g.
"control_index").- Then the method needs the given collection to actually be run. So to use (e.g.)
control_index, you have to put"bin-pkg-control"in yourNeeds-Info.
- Then the method needs the given collection to actually be run. So to use (e.g.)
CAVEAT: Methods can have different requirements based on the type of package! An example of this is"changelog", which requires"changelog-file"in binary packages and"Same as debfiles"in source packages. So yeah, read carefully, it's giving β¨context-dependentβ¨.
Avoiding security issues π
Over the years a couple of security issues have been discovered in Lintian. The problem is that people can, in theory, create some really nasty packages. So please keep the following in mind when you're writing a check β this is the part where you lock in, bestie, fr fr:
- Avoid 2-arg
open,system/exec($shellcmd),`$shellcmd`like the plague. π- When you get any one of those wrong you introduce "arbitrary code execution" vulnerabilities (we learned this the hard way via
CVE-2009-4014). Not the cute kind of learning. - Usually 3-arg
openand the non-shell variant ofsystem/execare enough. When you actually need a shell pipeline, consider usingLintian::Command. It also provides asafe_qxcommand to assist with capturing stdout as an alternative to$cmd(orqx/$cmd/).
- When you get any one of those wrong you introduce "arbitrary code execution" vulnerabilities (we learned this the hard way via
- Do not trust field values. π
- This is especially true if you intend to use the value as part of a file name. Verify that the field contains what you expect before you use it. Verify it. say it with me. verify it.
- Use
Lintian::Path(or, failing that,is_ancestor_of)- You might be tempted to think that the following code is safe:
use autodie;
my $filename = 'some/file';
my $ufile = $info->unpacked($filename);
if ( ! -l $ufile) {
# Looks safe, but isn't in general
open(my $fd, '<', $ufile);
...;
}
- This is definitely unsafe if
"$filename"contains at least one directory segment. π So, if in doubt, useis_ancestor_ofto verify that the requested file is indeed the file you think it is. A better version of the above would be:
use autodie,
use Lintian::Util qw(is_ancestor_of);
[...]
my $filename = 'some/file';
my $ufile = $info->unpacked($filename);
if ( ! -l $ufile && -f $ufile && is_ancestor_of($info->unpacked, $ufile)) {
# $ufile is a file and it is contained within the package root.
open(m $fd, '<', $ufile);
...;
}
- In some cases you can even drop the
"! -l $ufile"part. - Of course, it is much easier to use the
Lintian::Pathobject (whenever possible). β¨
my $filename = 'some/file';
my $ufile = $info->index($filename);
if ( $ufile && $ufile->is_file && $ufile->is_open_ok) {
my $fd = $ufile->open;
...;
}
- Here you can drop the
" && $ufile->is_file"if you want to permit safe symlinks. - For more information on the
is_ancestor_ofcheck, seeis_ancestor_of
π SEE ALSO π
Lintian::Tutorial::WritingTestsLintian::Tutorial::TestSuite
and that's a wrap on part 2, bestie π go write some checks and stay safe out there. this page is from Lintian v2.139.0ubuntu2, dated 2026-10-08, and the manpage slug on the tin is Lintian::Tu...itingChecks(3) β yes it's literally truncated like that, no cap, that's just what it says π
π¦
π¦ where this page comes from
- ubuntu stonking: lintian 2.139.0ubuntu2 Β· GPL-2.0-or-later
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms