the shed // linux // diagnostics

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:

kernel_taint_check.rb

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.

$ ruby kernel_taint_check.rb
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)
Get the code

Full script + README on GitHub: ruby-devops-toolkit/kernel-taint-check

Decode the Linux Kernel Taint Flag with Ruby

How the pieces fit together
prerequisites

What you need

  • Ruby 2.7+ (tested on 3.3.6), standard library only
  • Linux with /proc/sys/kernel/tainted and /sys/module
  • No root needed
the full script

Complete code

kernel_taint_check.rbruby
#!/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)
walkthrough

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.

example output

What you will see

outputtext
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)
troubleshooting

When it misbehaves

  • Use --value N to 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 FLAGS from the kernel docs.
next steps

Extending it

  • Persist the last value and alert only on new bits.
  • Correlate with journalctl -k lines matching ‘Tainted:’.
  • Expose the value as a Prometheus gauge.

Tha-Shed

Type a brief, get a finished film. Characters, voices and music included.

Start creating