solti_exec/subprocess/
backend.rs1use tokio::process::Command;
6use tracing::trace;
7
8use crate::ExecError::InvalidRunnerConfig;
9use crate::subprocess::logger::LogConfig;
10use crate::utils::{CgroupLimits, RlimitConfig, SecurityConfig};
11use crate::utils::{attach_cgroup, attach_rlimits, attach_security};
12
13#[derive(Debug, Clone, Default)]
26pub struct SubprocessBackendConfig {
27 rlimits: Option<RlimitConfig>,
29 cgroups: Option<CgroupLimits>,
31 security: Option<SecurityConfig>,
33 logger: LogConfig,
35}
36
37impl SubprocessBackendConfig {
38 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn with_rlimits(mut self, rlimits: RlimitConfig) -> Self {
45 self.rlimits = Some(rlimits);
46 self
47 }
48
49 pub fn with_cgroups(mut self, cgroups: CgroupLimits) -> Self {
51 self.cgroups = Some(cgroups);
52 self
53 }
54
55 pub fn with_security(mut self, security: SecurityConfig) -> Self {
57 self.security = Some(security);
58 self
59 }
60
61 pub fn with_logger(mut self, config: LogConfig) -> Self {
63 self.logger = config;
64 self
65 }
66
67 pub(crate) fn log_config(&self) -> &LogConfig {
69 &self.logger
70 }
71
72 pub(crate) fn is_empty(&self) -> bool {
74 self.rlimits.is_none() && self.cgroups.is_none() && self.security.is_none()
75 }
76
77 pub(crate) fn validate(&self) -> Result<(), crate::ExecError> {
79 if let Some(cgroups) = &self.cgroups {
80 if let Some(cpu) = &cgroups.cpu {
81 if cpu.period == 0 {
82 return Err(InvalidRunnerConfig(
83 "cgroups.cpu.period cannot be zero".into(),
84 ));
85 }
86 if let Some(q) = cpu.quota
87 && q == 0
88 {
89 return Err(InvalidRunnerConfig(
90 "cgroups.cpu.quota cannot be zero (process would get no CPU)".into(),
91 ));
92 }
93 if let Some(q) = cpu.quota
94 && q > cpu.period
95 {
96 return Err(InvalidRunnerConfig(
97 "cgroups.cpu.quota exceeds period (>100% of one core)".into(),
98 ));
99 }
100 }
101 if let Some(mem) = cgroups.memory
102 && mem == 0
103 {
104 return Err(InvalidRunnerConfig("cgroups.memory cannot be zero".into()));
105 }
106 if let Some(pids) = cgroups.pids
107 && pids == 0
108 {
109 return Err(InvalidRunnerConfig("cgroups.pids cannot be zero".into()));
110 }
111 }
112 if let Some(rlimits) = &self.rlimits
113 && let Some(fsize) = rlimits.max_file_size_bytes
114 && fsize == 0
115 {
116 return Err(InvalidRunnerConfig(
117 "rlimits.max_file_size_bytes cannot be zero".into(),
118 ));
119 }
120 if self.logger.max_line_length == 0 {
121 return Err(InvalidRunnerConfig(
122 "log_config.max_line_length cannot be zero".into(),
123 ));
124 }
125 Ok(())
126 }
127
128 pub(crate) fn has_cgroups(&self) -> bool {
130 self.cgroups.is_some()
131 }
132
133 pub(crate) fn prepare_cgroups(&self, cgroup_name: &str) -> Result<bool, crate::ExecError> {
138 if let Some(cgroups) = &self.cgroups {
139 trace!(
140 "subprocess backend: preparing cgroup: {:?} (group={})",
141 cgroups, cgroup_name
142 );
143 crate::utils::prepare_cgroup(cgroup_name, cgroups)
144 } else {
145 Ok(false)
146 }
147 }
148
149 pub(crate) fn apply_to_command(
158 &self,
159 cmd: &mut Command,
160 cgroup_name: &str,
161 ) -> Result<(), crate::ExecError> {
162 if self.is_empty() {
163 trace!("subprocess backend: nothing to apply (empty config)");
164 return Ok(());
165 }
166
167 if let Some(rlimits) = &self.rlimits {
168 trace!("subprocess backend: attaching rlimits: {:?}", rlimits);
169 attach_rlimits(cmd, rlimits);
170 }
171 if let Some(cgroups) = &self.cgroups {
172 trace!(
173 "subprocess backend: attaching cgroup join hook (group={})",
174 cgroup_name
175 );
176 attach_cgroup(cmd, cgroup_name, cgroups)?;
177 }
178 if let Some(security) = &self.security {
179 trace!(
180 "subprocess backend: attaching security config: {:?}",
181 security
182 );
183 attach_security(cmd, security);
184 }
185 Ok(())
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::utils::CpuMax;
193
194 #[test]
195 fn valid_cpu_config_passes() {
196 let cfg = SubprocessBackendConfig::new().with_cgroups(CgroupLimits {
197 cpu: Some(CpuMax {
198 quota: Some(50_000),
199 period: 100_000,
200 }),
201 ..Default::default()
202 });
203 assert!(cfg.validate().is_ok());
204 }
205
206 #[test]
207 fn cpu_period_zero_rejected() {
208 let cfg = SubprocessBackendConfig::new().with_cgroups(CgroupLimits {
209 cpu: Some(CpuMax {
210 quota: Some(50_000),
211 period: 0,
212 }),
213 ..Default::default()
214 });
215 let err = cfg.validate().unwrap_err().to_string();
216 assert!(err.contains("period"), "expected period error, got: {err}");
217 }
218
219 #[test]
220 fn cpu_quota_zero_rejected() {
221 let cfg = SubprocessBackendConfig::new().with_cgroups(CgroupLimits {
222 cpu: Some(CpuMax {
223 quota: Some(0),
224 period: 100_000,
225 }),
226 ..Default::default()
227 });
228 let err = cfg.validate().unwrap_err().to_string();
229 assert!(err.contains("quota"), "expected quota error, got: {err}");
230 }
231
232 #[test]
233 fn cpu_quota_exceeds_period_rejected() {
234 let cfg = SubprocessBackendConfig::new().with_cgroups(CgroupLimits {
235 cpu: Some(CpuMax {
236 quota: Some(200_000),
237 period: 100_000,
238 }),
239 ..Default::default()
240 });
241 let err = cfg.validate().unwrap_err().to_string();
242 assert!(
243 err.contains("exceeds period"),
244 "expected exceeds error, got: {err}"
245 );
246 }
247
248 #[test]
249 fn cpu_unlimited_quota_passes() {
250 let cfg = SubprocessBackendConfig::new().with_cgroups(CgroupLimits {
251 cpu: Some(CpuMax {
252 quota: None,
253 period: 100_000,
254 }),
255 ..Default::default()
256 });
257 assert!(cfg.validate().is_ok());
258 }
259}