cat /proc/sys/kernel/tainted prints 4609. Is that bad? A 70-line Ruby script turns the bitmask into plain English and names the module that caused it.
Step through the build below:
When a kernel is tainted, upstream maintainers may ignore your bug report, and more importantly it tells you something happened: a proprietary or unsigned module loaded, the kernel logged a WARN, a machine-check fired, or it survived an OOPS. Fleet monitoring rarely tracks this, yet a jump from 0 to 512 (a kernel WARN) after a driver update is exactly the early signal you want.
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# kernel_taint_check.rb - Decode /proc/sys/kernel/tainted into human-readable flags.
#
# A tainted kernel is one that has done something that makes upstream bug
# reports less trustworthy (proprietary/out-of-tree module, forced module load,
# machine check, oops, warning...). The value is a bitmask; this script decodes
# it, classifies each flag by operational concern, and lists out-of-tree modules
# from /sys/module/*/taint.
#
# Usage: ruby kernel_taint_check.rb [--value N] [--json]
# Exit: 0 = clean or informational only, 1 = concerning flags set
require 'json'
require 'optparse'
# bit => [letter, description, concern] (see Documentation/admin-guide/tainted-kernels.rst)
FLAGS = {
0 => ['P', 'proprietary module loaded', :info],
1 => ['F', 'module force-loaded', :warn],
2 => ['S', 'SMP kernel on unsupported CPU', :warn],
3 => ['R', 'module force-unloaded', :warn],
4 => ['M', 'machine check exception occurred', :crit],
5 => ['B', 'bad page referenced', :crit],
6 => ['U', 'taint requested by userspace', :info],
7 => ['D', 'kernel died recently (OOPS or BUG)', :crit],
8 => ['A', 'ACPI table overridden', :warn],
9 => ['W', 'kernel issued a WARN', :warn],
10 => ['C', 'staging driver loaded', :info],
11 => ['I', 'workaround for platform firmware bug', :info],
12 => ['O', 'out-of-tree module loaded', :info],
13 => ['E', 'unsigned module loaded', :warn],
14 => ['L', 'soft lockup occurred', :crit],
15 => ['K', 'kernel live-patched', :info]
}.freeze
def decode(value)
FLAGS.filter_map do |bit, (letter, desc, concern)|
next unless value[bit] == 1
{ bit: bit, letter: letter, description: desc, concern: concern }
end
end
# Modules that taint the kernel expose /sys/module/<name>/taint (e.g. "PO")
def tainting_modules(root = '/sys/module')
Dir.glob(File.join(root, '*', 'taint')).filter_map do |f|
t = File.read(f).strip
[File.basename(File.dirname(f)), t] unless t.empty?
end.sort
rescue SystemCallError
[]
end
opts = { json: false, value: nil }
OptionParser.new do |o|
o.on('--value N', Integer, 'decode this number instead of /proc') { |v| opts[:value] = v }
o.on('--json') { opts[:json] = true }
end.parse!
value = opts[:value] || Integer(File.read('/proc/sys/kernel/tainted').strip)
flags = decode(value)
mods = opts[:value] ? [] : tainting_modules
bad = flags.any? { |f| %i[warn crit].include?(f[:concern]) }
if opts[:json]
puts JSON.pretty_generate(tainted: value, flags: flags, modules: mods.to_h)
else
puts "tainted = #{value} (#{value.zero? ? 'clean' : 'TAINTED'})"
flags.each { |f| puts format(' [%-4s] bit %-2d %s (%s)', f[:concern].to_s.upcase, f[:bit], f[:letter], f[:description]) }
mods.each { |n, t| puts " module #{n} taints: #{t}" }
end
exit(bad ? 1 : 0)
1. A table of bit meanings. FLAGS maps bit numbers 0–15 to the kernel’s letter code, a description, and a concern level (:info, :warn, :crit). Proprietary and out-of-tree modules are informational; a machine check, bad page, OOPS or soft lockup is critical.
2. Test each bit. value[bit] == 1 is Ruby’s built-in Integer bit reference, so decoding is one filter_map with no manual shifting.
3. Find the culprit modules. Modules that taint the kernel expose /sys/module/NAME/taint containing letters such as PO. We glob those and list non-empty ones.
4. Exit code policy. Only :warn and :crit flags fail the check; a box running the NVIDIA driver (P, O) is reported but not paged.
tainted = 4609 (TAINTED) [INFO] bit 0 P (proprietary module loaded) [WARN] bit 9 W (kernel issued a WARN) [INFO] bit 12 O (out-of-tree module loaded)
Full script + README on GitHub: ruby-devops-toolkit/kernel-taint-check
What you need
- Ruby 2.7+ (tested on 3.3.6), standard library only
- Linux with
/proc/sys/kernel/taintedand/sys/module - No root needed
Complete code
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# kernel_taint_check.rb - Decode /proc/sys/kernel/tainted into human-readable flags.
#
# A tainted kernel is one that has done something that makes upstream bug
# reports less trustworthy (proprietary/out-of-tree module, forced module load,
# machine check, oops, warning...). The value is a bitmask; this script decodes
# it, classifies each flag by operational concern, and lists out-of-tree modules
# from /sys/module/*/taint.
#
# Usage: ruby kernel_taint_check.rb [--value N] [--json]
# Exit: 0 = clean or informational only, 1 = concerning flags set
require 'json'
require 'optparse'
# bit => [letter, description, concern] (see Documentation/admin-guide/tainted-kernels.rst)
FLAGS = {
0 => ['P', 'proprietary module loaded', :info],
1 => ['F', 'module force-loaded', :warn],
2 => ['S', 'SMP kernel on unsupported CPU', :warn],
3 => ['R', 'module force-unloaded', :warn],
4 => ['M', 'machine check exception occurred', :crit],
5 => ['B', 'bad page referenced', :crit],
6 => ['U', 'taint requested by userspace', :info],
7 => ['D', 'kernel died recently (OOPS or BUG)', :crit],
8 => ['A', 'ACPI table overridden', :warn],
9 => ['W', 'kernel issued a WARN', :warn],
10 => ['C', 'staging driver loaded', :info],
11 => ['I', 'workaround for platform firmware bug', :info],
12 => ['O', 'out-of-tree module loaded', :info],
13 => ['E', 'unsigned module loaded', :warn],
14 => ['L', 'soft lockup occurred', :crit],
15 => ['K', 'kernel live-patched', :info]
}.freeze
def decode(value)
FLAGS.filter_map do |bit, (letter, desc, concern)|
next unless value[bit] == 1
{ bit: bit, letter: letter, description: desc, concern: concern }
end
end
# Modules that taint the kernel expose /sys/module/<name>/taint (e.g. "PO")
def tainting_modules(root = '/sys/module')
Dir.glob(File.join(root, '*', 'taint')).filter_map do |f|
t = File.read(f).strip
[File.basename(File.dirname(f)), t] unless t.empty?
end.sort
rescue SystemCallError
[]
end
opts = { json: false, value: nil }
OptionParser.new do |o|
o.on('--value N', Integer, 'decode this number instead of /proc') { |v| opts[:value] = v }
o.on('--json') { opts[:json] = true }
end.parse!
value = opts[:value] || Integer(File.read('/proc/sys/kernel/tainted').strip)
flags = decode(value)
mods = opts[:value] ? [] : tainting_modules
bad = flags.any? { |f| %i[warn crit].include?(f[:concern]) }
if opts[:json]
puts JSON.pretty_generate(tainted: value, flags: flags, modules: mods.to_h)
else
puts "tainted = #{value} (#{value.zero? ? 'clean' : 'TAINTED'})"
flags.each { |f| puts format(' [%-4s] bit %-2d %s (%s)', f[:concern].to_s.upcase, f[:bit], f[:letter], f[:description]) }
mods.each { |n, t| puts " module #{n} taints: #{t}" }
end
exit(bad ? 1 : 0)
How it works
1. A table of bit meanings
FLAGS maps bit numbers 0–15 to the kernel’s letter code, a description, and a concern level (:info, :warn, :crit). Proprietary and out-of-tree modules are informational; a machine check, bad page, OOPS or soft lockup is critical.
2. Test each bit
value[bit] == 1 is Ruby’s built-in Integer bit reference, so decoding is one filter_map with no manual shifting.
3. Find the culprit modules
Modules that taint the kernel expose /sys/module/NAME/taint containing letters such as PO. We glob those and list non-empty ones.
4. Exit code policy
Only :warn and :crit flags fail the check; a box running the NVIDIA driver (P, O) is reported but not paged.
What you will see
tainted = 4609 (TAINTED)
[INFO] bit 0 P (proprietary module loaded)
[WARN] bit 9 W (kernel issued a WARN)
[INFO] bit 12 O (out-of-tree module loaded)
When it misbehaves
- Use
--value Nto decode any number, handy when pasting from a bug report or another host. - Taint is sticky until reboot: a single WARN keeps bit 9 set. Alert on change, not just presence, by storing the last value.
- Bits above 15 exist on newer kernels; unknown bits are ignored, so extend
FLAGSfrom the kernel docs.
Extending it
- Persist the last value and alert only on new bits.
- Correlate with
journalctl -klines matching ‘Tainted:’. - Expose the value as a Prometheus gauge.