Skip to main content
Back to blog
Brian

Linux restart debt after patching: when updated packages are not the end of the work

Package updates can fix files on disk while old code keeps running in long-lived processes. Here is how restart debt happens on Linux, how to inspect it, and why it matters after a patch window.

Linux HardeningPatch OperationsVulnerability Management

The patch window is usually measured by the thing everyone can see: did the update job finish?

dnf update ran. The package versions changed. The ticket has evidence. Maybe the scanner will look better on the next cycle.

That is good progress, but it is not always the end of the work.

On a Linux host, an updated package tells you what is now installed on disk. It does not, by itself, prove that every long-running process has stopped using the old code it loaded before the patch. That gap is where restart debt lives.

Restart debt is the pile of service restarts and host reboots still needed after package updates land. Sometimes it is obvious: a new kernel is installed and the host has not rebooted. Sometimes it is quieter: a daemon still has a deleted shared object mapped from before the update.

That distinction matters because the operational question after patching is not only:

Is the fixed package installed?

It is:

Is the vulnerable code still running?

Package state and process state move on different clocks

Package managers are disk-state tools. They are very good at answering questions like:

  • Is openssl-libs installed?
  • Which version-release is installed?
  • Does the installed package match the vendor-fixed build?
  • When was the package updated?

Runtime state is a different view of the same host. It answers questions like:

  • Which kernel is this machine actually booted into?
  • Which process has a shared library mapped?
  • Is a process still using a file that has since been replaced or unlinked?
  • Which network-facing service owns the stale process?

Those two views often line up after a clean patch and restart. But Linux does not force them to line up immediately.

If nginx, sshd, postgres, a JVM, or an internal daemon loaded a shared object before a package update, the process can keep running with that mapping until it restarts. The package on disk can be fixed while the process is still alive with the old mapping.

That is normal Linux behavior. It is not a package manager bug. It is also not something an operator should pretend away.

The kernel case is the easiest one to explain

Kernel updates make the applied-versus-live split very visible.

On a RHEL-family system, you can check the running kernel:

uname -r

Then compare it with the newest installed kernel package:

rpm -q --last kernel-core 2>/dev/null | head -1

If the fixed kernel package is installed but uname -r still shows the old kernel, the host has not booted into the fixed kernel yet.

That may be perfectly understandable. Production hosts need maintenance windows. Cluster nodes need draining. Some systems have service owners who have to be present for reboots.

The problem is not that a reboot is pending.

The problem is calling the vulnerability fixed when the host is still running the vulnerable kernel.

Shared libraries are where restart debt gets messy

Shared libraries are less obvious than kernels because the package version can look clean while processes keep old mappings alive.

A quick way to look for deleted mapped libraries is:

sudo grep -H '(deleted)' /proc/[0-9]*/maps 2>/dev/null | \
  grep -E '/(usr/)?lib(64)?/.*\.so'

That is not a complete vulnerability assessment. It is a useful smell test.

If you see a process mapping a deleted .so, it usually means the file has been replaced or removed on disk while the process still has the old object mapped in memory. That can happen after package updates.

lsof can show a related view:

sudo lsof +L1

This finds open files with a link count less than one. It is useful for deleted files still held by processes, though it is not a perfect replacement for reading process maps. In practice, I like looking at both views when debugging one host.

The next question is not simply “is there a deleted file?” The useful question is:

Is the stale mapping tied to a package and CVE that actually matters?

That requires connecting several pieces:

  • process ID
  • mapped file path
  • package ownership
  • installed package EVR
  • vendor fixed EVR
  • vulnerability severity and exploitability
  • whether the process is a relevant running service

That is where the manual work starts to get annoying.

needs-restarting helps, but it is not the whole story

On RHEL-family systems, needs-restarting is one of the practical tools operators reach for. Depending on distro/version and installed plugins, you may use it through dnf or as a standalone command from the relevant utilities package.

Common checks:

sudo dnf needs-restarting
sudo dnf needs-restarting -r

The -r check is especially useful because it gives a reboot-required signal for the host.

This is good operational evidence, but it still has limits:

  • It may not be installed everywhere.
  • It does not replace vendor-aware CVE matching.
  • It does not decide which restart matters most.
  • It does not always map neatly to the ticket/security finding the team is trying to close.
  • It can still leave you doing manual translation from “these processes need restart” to “these CVEs are still live risk.”

That last part is the important one.

Operators do not only need to know that something should restart. They need to know whether that restart is tied to a critical/high finding, a KEV-listed vulnerability, an internet-facing service, a kernel reboot, or a low-risk library in a process nobody exposes.

Restart debt is operational debt, not just security debt

Security teams often see restart debt as exposure that did not actually go away. That is fair.

Operators feel the same debt differently.

For the person running the patch window, restart debt means:

  • another maintenance window
  • another application owner conversation
  • another drain/reboot sequence
  • another “can this service restart during business hours?” question
  • another ticket that looks done but is not quite done

The annoying part is that not all restart debt deserves the same urgency.

A stale library in a service bound to 0.0.0.0:443 is different from a stale mapping in a batch job that will exit in five minutes. A kernel issue on a public host is different from a low-severity library in an internal utility process. A CISA KEV item is different from a theoretical low-priority CVE in a package that is installed but not actually in use.

Good post-patch evidence should help split those cases apart.

What I want from a post-patch finding

After a patch window, the most useful finding is not a generic instruction like:

Update openssl-libs.

If the package is already updated, that instruction is stale.

A better finding says what is actually left:

Package:
  openssl-libs

Applied state:
  fixed package is installed

Live state:
  process 1842 still maps an old deleted libssl object

Service context:
  nginx worker process

Recommended action:
  restart nginx during the approved service window

Verification:
  rerun process-map check and confirm the old mapping is gone

For a kernel issue, the shape should be different:

Applied state:
  fixed kernel package is installed

Live state:
  host is still running the previous kernel

Recommended action:
  reboot into the fixed kernel

Verification:
  compare uname -r with installed kernel package after reboot

That is a much better task for a Linux team. It is concrete. It says whether the work is an update, a restart, a reboot, or follow-up investigation. It gives security enough context to understand why a finding should or should not close.

A careful manual inspection flow

For a single RHEL-family host, a practical inspection pass might look like this.

Start with host and OS evidence:

hostname
date -u
cat /etc/os-release
uname -a

Check the running kernel:

uname -r
rpm -q --last kernel-core 2>/dev/null | head -1

Check reboot/restart hints if the tooling is present:

sudo dnf needs-restarting -r
sudo dnf needs-restarting

Look for deleted shared libraries still mapped by processes:

sudo grep -H '(deleted)' /proc/[0-9]*/maps 2>/dev/null | \
  grep -E '/(usr/)?lib(64)?/.*\.so'

Map PIDs back to commands:

ps -fp <pid>

Check whether the process owns listening sockets:

sudo ss -tulpn | grep "pid=<pid>,"

This is enough to understand a single host. It is not enough to comfortably run a fleet program.

The hard parts are scale and correlation:

  • Which stale mappings correspond to vulnerable packages?
  • Which fixed package should have removed the risk?
  • Which process owners need to take action?
  • Which findings are critical/high?
  • Which items are KEV-listed?
  • Which hosts can safely be called clean?
  • Which hosts need a reboot versus service restart?

That is where a one-off command line session turns into a spreadsheet, and then into a debate, and then into “we will check it next cycle.”

Be careful with false confidence

There are a few traps worth calling out.

First, a deleted mapping does not automatically mean exploitable risk. You still need to know what the object is, which package it came from, which CVE applies, and whether the process is relevant.

Second, no deleted mapping does not prove there is no vulnerability. It only means that specific evidence was not found. You still need package/advisory matching and good collection coverage.

Third, enterprise Linux version strings can be misleading if you compare them naively. Vendors backport security fixes. A package can look old compared with upstream and still contain the vendor fix.

Fourth, containers complicate the view. Host package state, container image contents, process namespaces, and mounted filesystems need to be interpreted carefully. Do not treat host RPM state as a complete answer for every process on a modern Linux machine.

The goal is not to replace judgment with one magic command.

The goal is to stop pretending installed package state is the same thing as live remediation.

The useful end state

The post-patch state I want is boring and specific:

This host has no vulnerable package findings that are live.
This host has one package update required.
This host has restart debt for nginx.
This host has reboot debt for the kernel.
This host has incomplete evidence and should not be called clean yet.

Those are operationally different answers. They create different tickets. They go to different owners. They carry different urgency.

That is why restart debt deserves to be tracked as a first-class post-patch outcome.

Not because teams need another dashboard.

Because the person closing the ticket deserves to know whether the host is actually fixed.