1mod cgroup_path;
7#[cfg(any(test, feature = "fuzzing"))]
8pub(crate) mod fuzzing;
9mod inotify;
10mod v2;
11
12use anyhow::Context;
13use nix::{
14 sys::{
15 signal::Signal,
16 statfs::{CGROUP2_SUPER_MAGIC, statfs},
17 },
18 unistd::Pid,
19};
20use std::{
21 num::ParseIntError,
22 path::PathBuf,
23 sync::{
24 LazyLock,
25 atomic::{AtomicBool, Ordering},
26 },
27};
28use tokio::fs::OpenOptions;
29use tokio_util::sync::CancellationToken;
30use tracing::{debug, error, warn};
31
32const CGROUP_ROOT: &str = "/sys/fs/cgroup";
33
34const WATCH_EVENT_CHANNEL_SIZE: usize = 32;
36
37static IS_CGROUP_V2: LazyLock<bool> = LazyLock::new(|| {
38 statfs(CGROUP_ROOT).is_ok_and(|sts| sts.filesystem_type() == CGROUP2_SUPER_MAGIC)
39});
40
41static UNSUPPORTED_WARNED: AtomicBool = AtomicBool::new(false);
44
45pub(crate) struct OOMWatcher;
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
51pub(crate) struct OOMEvent {
52 pub(crate) oom: bool,
53
54 pub(crate) possible_oom: bool,
60}
61
62impl OOMEvent {
63 fn new(oom: bool) -> Self {
64 Self {
65 oom,
66 ..Self::default()
67 }
68 }
69
70 pub(crate) fn possible(possible: bool) -> Self {
72 Self {
73 oom: false,
74 possible_oom: possible,
75 }
76 }
77
78 pub(crate) fn is_oom(self, exit_code: i32) -> bool {
80 self.oom || (self.possible_oom && exit_code == 128 + Signal::SIGKILL as i32)
81 }
82}
83
84impl OOMWatcher {
85 pub(crate) async fn watch(
91 token: &CancellationToken,
92 pid: Pid,
93 exit_paths: &[PathBuf],
94 ) -> Option<OOMEvent> {
95 if !Self::supported(*IS_CGROUP_V2, &UNSUPPORTED_WARNED) {
96 return None;
97 }
98 let event = Self::oom_handling_cgroup_v2(token, pid)
99 .await
100 .context("set up cgroup v2 OOM handling")
101 .inspect_err(|e| error!("Unable to watch OOM: {e:#}"))
102 .ok()
103 .flatten();
104 if event.is_some_and(|event| event.oom) {
105 Self::write_oom_files(exit_paths).await;
106 }
107 debug!("Done watching for OOMs");
108 event
109 }
110
111 fn supported(cgroup_v2: bool, warned: &AtomicBool) -> bool {
117 if !cgroup_v2 && !warned.swap(true, Ordering::Relaxed) {
118 warn!(
119 "No cgroup v2 at {CGROUP_ROOT}, the OOM kills of containers do not get detected: cgroup v1 and hybrid setups are not supported"
120 );
121 }
122 cgroup_v2
123 }
124
125 fn counters(content: &str) -> impl Iterator<Item = (&str, Result<u64, ParseIntError>)> {
128 content
129 .lines()
130 .filter_map(|line| line.split_once(' '))
131 .map(|(key, value)| (key, value.trim().parse()))
132 }
133
134 pub(crate) async fn write_oom_files(exit_paths: &[PathBuf]) {
138 for path in exit_paths {
139 debug!("Writing OOM file: {}", path.display());
140 let created = OpenOptions::new()
141 .write(true)
142 .create(true)
143 .truncate(true)
144 .custom_flags(libc::O_NOFOLLOW)
145 .open(path)
146 .await;
147 if let Err(e) = created {
148 error!("Unable to create OOM file {}: {e:#}", path.display());
149 }
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::test_util::Log;
158
159 #[test]
160 fn only_cgroup_v2_gets_watched() -> anyhow::Result<()> {
161 let (log, _guard) = Log::capture();
162 let warned = AtomicBool::new(false);
163 assert!(OOMWatcher::supported(true, &warned));
164 assert!(!warned.load(Ordering::Relaxed));
165
166 for _ in 0..3 {
168 assert!(!OOMWatcher::supported(false, &warned));
169 }
170 assert!(warned.load(Ordering::Relaxed));
171 assert!(OOMWatcher::supported(true, &warned));
172 let log = log.contents()?;
173 assert_eq!(log.matches("WARN").count(), 1, "{log}");
174 assert_eq!(log.matches("No cgroup v2 at").count(), 1, "{log}");
175 Ok(())
176 }
177
178 #[test]
179 fn oom_event_needs_kill_for_ancestor_oom() {
180 assert!(OOMEvent::new(true).is_oom(0));
181 assert!(!OOMEvent::default().is_oom(137));
182 let ancestor = OOMEvent {
183 oom: false,
184 possible_oom: true,
185 };
186 assert!(ancestor.is_oom(137));
187 assert!(!ancestor.is_oom(143));
188 assert!(!ancestor.is_oom(1));
189 }
190
191 #[tokio::test]
192 async fn oom_files_do_not_follow_symlinks() -> anyhow::Result<()> {
193 let dir = tempfile::tempdir()?;
194 let target = dir.path().join("target");
195 std::fs::write(&target, "untouched")?;
196 let link = dir.path().join("oom");
197 std::os::unix::fs::symlink(&target, &link)?;
198 let plain = dir.path().join("plain");
199
200 OOMWatcher::write_oom_files(&[link.clone(), plain.clone()]).await;
203 assert_eq!(std::fs::read_to_string(&target)?, "untouched");
204 assert!(std::fs::symlink_metadata(&link)?.is_symlink());
205 assert!(plain.is_file());
206 Ok(())
207 }
208}