1use secure_exec_kernel::resource_accounting::ResourceLimits;
9use secure_exec_vm_config::{ResourceLimitsConfig, VmLimitsConfig};
10
11use crate::state::SidecarError;
12use crate::wire::DEFAULT_MAX_FRAME_BYTES;
13
14pub const DEFAULT_MAX_FETCH_RESPONSE_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
17
18pub const DEFAULT_TOOL_TIMEOUT_MS: u64 = 30_000;
19pub const MAX_TOOL_TIMEOUT_MS: u64 = 300_000;
20pub const MAX_REGISTERED_TOOLKITS: usize = 64;
21pub const MAX_REGISTERED_TOOLS_PER_VM: usize = 256;
22pub const MAX_TOOLS_PER_TOOLKIT: usize = 64;
23pub const MAX_TOOL_SCHEMA_BYTES: usize = 16 * 1024;
24pub const MAX_TOOL_EXAMPLES_PER_TOOL: usize = 16;
25pub const MAX_TOOL_EXAMPLE_INPUT_BYTES: usize = 4 * 1024;
26
27pub const MAX_PERSISTED_MANIFEST_BYTES: usize = 64 * 1024 * 1024;
28pub const MAX_PERSISTED_MANIFEST_FILE_BYTES: u64 = 1024 * 1024 * 1024;
29
30pub const DEFAULT_ACP_MAX_READ_LINE_BYTES: usize = 16 * 1024 * 1024;
31pub const DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT: usize = 1024 * 1024;
32
33pub const DEFAULT_JS_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024;
34pub const DEFAULT_JS_STDIN_BUFFER_LIMIT_BYTES: usize = 16 * 1024 * 1024;
35pub const DEFAULT_JS_EVENT_PAYLOAD_LIMIT_BYTES: usize = 1024 * 1024;
36pub const DEFAULT_V8_IPC_MAX_FRAME_BYTES: u32 = 64 * 1024 * 1024;
37
38pub const DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES: usize = 1024 * 1024;
39pub const DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS: u64 = 5 * 60 * 1000;
40pub const DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS: u64 = 30 * 1000;
41
42pub const DEFAULT_WASM_MAX_MODULE_FILE_BYTES: u64 = 256 * 1024 * 1024;
43pub const DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024;
44pub const DEFAULT_WASM_SYNC_READ_LIMIT_BYTES: usize = 16 * 1024 * 1024;
45
46#[derive(Debug, Clone, PartialEq, Eq, Default)]
50pub struct VmLimits {
51 pub resources: ResourceLimits,
53 pub http: HttpLimits,
54 pub tools: ToolLimits,
55 pub plugins: PluginLimits,
56 pub acp: AcpLimits,
57 pub js_runtime: JsRuntimeLimits,
58 pub python: PythonLimits,
59 pub wasm: WasmLimits,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct HttpLimits {
64 pub max_fetch_response_bytes: usize,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct ToolLimits {
70 pub default_tool_timeout_ms: u64,
71 pub max_tool_timeout_ms: u64,
72 pub max_registered_toolkits: usize,
73 pub max_registered_tools_per_vm: usize,
74 pub max_tools_per_toolkit: usize,
75 pub max_tool_schema_bytes: usize,
76 pub max_tool_examples_per_tool: usize,
77 pub max_tool_example_input_bytes: usize,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct PluginLimits {
82 pub max_persisted_manifest_bytes: usize,
83 pub max_persisted_manifest_file_bytes: u64,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct AcpLimits {
88 pub max_read_line_bytes: usize,
90 pub stdout_buffer_byte_limit: usize,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct JsRuntimeLimits {
96 pub v8_heap_limit_mb: Option<u32>,
99 pub captured_output_limit_bytes: usize,
100 pub stdin_buffer_limit_bytes: usize,
101 pub event_payload_limit_bytes: usize,
102 pub v8_ipc_max_frame_bytes: u32,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct PythonLimits {
109 pub output_buffer_max_bytes: usize,
110 pub execution_timeout_ms: u64,
111 pub vfs_rpc_timeout_ms: u64,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct WasmLimits {
116 pub max_module_file_bytes: u64,
117 pub captured_output_limit_bytes: usize,
118 pub sync_read_limit_bytes: usize,
120}
121
122impl Default for HttpLimits {
123 fn default() -> Self {
124 Self {
125 max_fetch_response_bytes: DEFAULT_MAX_FETCH_RESPONSE_BYTES,
126 }
127 }
128}
129
130impl Default for ToolLimits {
131 fn default() -> Self {
132 Self {
133 default_tool_timeout_ms: DEFAULT_TOOL_TIMEOUT_MS,
134 max_tool_timeout_ms: MAX_TOOL_TIMEOUT_MS,
135 max_registered_toolkits: MAX_REGISTERED_TOOLKITS,
136 max_registered_tools_per_vm: MAX_REGISTERED_TOOLS_PER_VM,
137 max_tools_per_toolkit: MAX_TOOLS_PER_TOOLKIT,
138 max_tool_schema_bytes: MAX_TOOL_SCHEMA_BYTES,
139 max_tool_examples_per_tool: MAX_TOOL_EXAMPLES_PER_TOOL,
140 max_tool_example_input_bytes: MAX_TOOL_EXAMPLE_INPUT_BYTES,
141 }
142 }
143}
144
145impl Default for PluginLimits {
146 fn default() -> Self {
147 Self {
148 max_persisted_manifest_bytes: MAX_PERSISTED_MANIFEST_BYTES,
149 max_persisted_manifest_file_bytes: MAX_PERSISTED_MANIFEST_FILE_BYTES,
150 }
151 }
152}
153
154impl Default for AcpLimits {
155 fn default() -> Self {
156 Self {
157 max_read_line_bytes: DEFAULT_ACP_MAX_READ_LINE_BYTES,
158 stdout_buffer_byte_limit: DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT,
159 }
160 }
161}
162
163impl Default for JsRuntimeLimits {
164 fn default() -> Self {
165 Self {
166 v8_heap_limit_mb: None,
167 captured_output_limit_bytes: DEFAULT_JS_CAPTURED_OUTPUT_LIMIT_BYTES,
168 stdin_buffer_limit_bytes: DEFAULT_JS_STDIN_BUFFER_LIMIT_BYTES,
169 event_payload_limit_bytes: DEFAULT_JS_EVENT_PAYLOAD_LIMIT_BYTES,
170 v8_ipc_max_frame_bytes: DEFAULT_V8_IPC_MAX_FRAME_BYTES,
171 }
172 }
173}
174
175impl Default for PythonLimits {
176 fn default() -> Self {
177 Self {
178 output_buffer_max_bytes: DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES,
179 execution_timeout_ms: DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS,
180 vfs_rpc_timeout_ms: DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS,
181 }
182 }
183}
184
185impl Default for WasmLimits {
186 fn default() -> Self {
187 Self {
188 max_module_file_bytes: DEFAULT_WASM_MAX_MODULE_FILE_BYTES,
189 captured_output_limit_bytes: DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES,
190 sync_read_limit_bytes: DEFAULT_WASM_SYNC_READ_LIMIT_BYTES,
191 }
192 }
193}
194
195pub fn vm_limits_from_config(
196 config: Option<&VmLimitsConfig>,
197 sidecar_max_frame_bytes: usize,
198) -> Result<VmLimits, SidecarError> {
199 let mut limits = VmLimits::default();
200 let Some(config) = config else {
201 validate_vm_limits(&limits, sidecar_max_frame_bytes)?;
202 return Ok(limits);
203 };
204
205 if let Some(resources) = config.resources.as_ref() {
206 apply_resource_limits_config(&mut limits.resources, resources)?;
207 }
208 if let Some(http) = config.http.as_ref() {
209 set_usize(
210 &mut limits.http.max_fetch_response_bytes,
211 http.max_fetch_response_bytes,
212 "limits.http.maxFetchResponseBytes",
213 )?;
214 }
215 if let Some(tools) = config.tools.as_ref() {
216 set_u64(
217 &mut limits.tools.default_tool_timeout_ms,
218 tools.default_tool_timeout_ms,
219 "limits.tools.defaultToolTimeoutMs",
220 )?;
221 set_u64(
222 &mut limits.tools.max_tool_timeout_ms,
223 tools.max_tool_timeout_ms,
224 "limits.tools.maxToolTimeoutMs",
225 )?;
226 set_usize(
227 &mut limits.tools.max_registered_toolkits,
228 tools.max_registered_toolkits,
229 "limits.tools.maxRegisteredToolkits",
230 )?;
231 set_usize(
232 &mut limits.tools.max_registered_tools_per_vm,
233 tools.max_registered_tools_per_vm,
234 "limits.tools.maxRegisteredToolsPerVm",
235 )?;
236 set_usize(
237 &mut limits.tools.max_tools_per_toolkit,
238 tools.max_tools_per_toolkit,
239 "limits.tools.maxToolsPerToolkit",
240 )?;
241 set_usize(
242 &mut limits.tools.max_tool_schema_bytes,
243 tools.max_tool_schema_bytes,
244 "limits.tools.maxToolSchemaBytes",
245 )?;
246 set_usize(
247 &mut limits.tools.max_tool_examples_per_tool,
248 tools.max_tool_examples_per_tool,
249 "limits.tools.maxToolExamplesPerTool",
250 )?;
251 set_usize(
252 &mut limits.tools.max_tool_example_input_bytes,
253 tools.max_tool_example_input_bytes,
254 "limits.tools.maxToolExampleInputBytes",
255 )?;
256 }
257 if let Some(plugins) = config.plugins.as_ref() {
258 set_usize(
259 &mut limits.plugins.max_persisted_manifest_bytes,
260 plugins.max_persisted_manifest_bytes,
261 "limits.plugins.maxPersistedManifestBytes",
262 )?;
263 set_u64(
264 &mut limits.plugins.max_persisted_manifest_file_bytes,
265 plugins.max_persisted_manifest_file_bytes,
266 "limits.plugins.maxPersistedManifestFileBytes",
267 )?;
268 }
269 if let Some(acp) = config.acp.as_ref() {
270 set_usize(
271 &mut limits.acp.max_read_line_bytes,
272 acp.max_read_line_bytes,
273 "limits.acp.maxReadLineBytes",
274 )?;
275 set_usize(
276 &mut limits.acp.stdout_buffer_byte_limit,
277 acp.stdout_buffer_byte_limit,
278 "limits.acp.stdoutBufferByteLimit",
279 )?;
280 }
281 if let Some(js_runtime) = config.js_runtime.as_ref() {
282 if let Some(value) = js_runtime.v8_heap_limit_mb {
283 limits.js_runtime.v8_heap_limit_mb = Some(
284 u32::try_from(value)
285 .map_err(|_| integer_too_large("limits.jsRuntime.v8HeapLimitMb", value))?,
286 );
287 }
288 set_usize(
289 &mut limits.js_runtime.captured_output_limit_bytes,
290 js_runtime.captured_output_limit_bytes,
291 "limits.jsRuntime.capturedOutputLimitBytes",
292 )?;
293 set_usize(
294 &mut limits.js_runtime.stdin_buffer_limit_bytes,
295 js_runtime.stdin_buffer_limit_bytes,
296 "limits.jsRuntime.stdinBufferLimitBytes",
297 )?;
298 set_usize(
299 &mut limits.js_runtime.event_payload_limit_bytes,
300 js_runtime.event_payload_limit_bytes,
301 "limits.jsRuntime.eventPayloadLimitBytes",
302 )?;
303 if let Some(value) = js_runtime.v8_ipc_max_frame_bytes {
304 limits.js_runtime.v8_ipc_max_frame_bytes = u32::try_from(value)
305 .map_err(|_| integer_too_large("limits.jsRuntime.v8IpcMaxFrameBytes", value))?;
306 }
307 }
308 if let Some(python) = config.python.as_ref() {
309 set_usize(
310 &mut limits.python.output_buffer_max_bytes,
311 python.output_buffer_max_bytes,
312 "limits.python.outputBufferMaxBytes",
313 )?;
314 set_u64(
315 &mut limits.python.execution_timeout_ms,
316 python.execution_timeout_ms,
317 "limits.python.executionTimeoutMs",
318 )?;
319 set_u64(
320 &mut limits.python.vfs_rpc_timeout_ms,
321 python.vfs_rpc_timeout_ms,
322 "limits.python.vfsRpcTimeoutMs",
323 )?;
324 }
325 if let Some(wasm) = config.wasm.as_ref() {
326 set_u64(
327 &mut limits.wasm.max_module_file_bytes,
328 wasm.max_module_file_bytes,
329 "limits.wasm.maxModuleFileBytes",
330 )?;
331 set_usize(
332 &mut limits.wasm.captured_output_limit_bytes,
333 wasm.captured_output_limit_bytes,
334 "limits.wasm.capturedOutputLimitBytes",
335 )?;
336 set_usize(
337 &mut limits.wasm.sync_read_limit_bytes,
338 wasm.sync_read_limit_bytes,
339 "limits.wasm.syncReadLimitBytes",
340 )?;
341 }
342
343 validate_vm_limits(&limits, sidecar_max_frame_bytes)?;
344 Ok(limits)
345}
346
347fn apply_resource_limits_config(
348 limits: &mut ResourceLimits,
349 config: &ResourceLimitsConfig,
350) -> Result<(), SidecarError> {
351 set_optional_usize(
352 &mut limits.virtual_cpu_count,
353 config.cpu_count,
354 "limits.resources.cpuCount",
355 )?;
356 set_optional_usize(
357 &mut limits.max_processes,
358 config.max_processes,
359 "limits.resources.maxProcesses",
360 )?;
361 set_optional_usize(
362 &mut limits.max_open_fds,
363 config.max_open_fds,
364 "limits.resources.maxOpenFds",
365 )?;
366 set_optional_usize(
367 &mut limits.max_pipes,
368 config.max_pipes,
369 "limits.resources.maxPipes",
370 )?;
371 set_optional_usize(
372 &mut limits.max_ptys,
373 config.max_ptys,
374 "limits.resources.maxPtys",
375 )?;
376 set_optional_usize(
377 &mut limits.max_sockets,
378 config.max_sockets,
379 "limits.resources.maxSockets",
380 )?;
381 set_optional_usize(
382 &mut limits.max_connections,
383 config.max_connections,
384 "limits.resources.maxConnections",
385 )?;
386 set_optional_usize(
387 &mut limits.max_socket_buffered_bytes,
388 config.max_socket_buffered_bytes,
389 "limits.resources.maxSocketBufferedBytes",
390 )?;
391 set_optional_usize(
392 &mut limits.max_socket_datagram_queue_len,
393 config.max_socket_datagram_queue_len,
394 "limits.resources.maxSocketDatagramQueueLen",
395 )?;
396 set_optional_u64(
397 &mut limits.max_filesystem_bytes,
398 config.max_filesystem_bytes,
399 );
400 set_optional_usize(
401 &mut limits.max_inode_count,
402 config.max_inode_count,
403 "limits.resources.maxInodeCount",
404 )?;
405 set_optional_u64(
406 &mut limits.max_blocking_read_ms,
407 config.max_blocking_read_ms,
408 );
409 set_optional_usize(
410 &mut limits.max_pread_bytes,
411 config.max_pread_bytes,
412 "limits.resources.maxPreadBytes",
413 )?;
414 set_optional_usize(
415 &mut limits.max_fd_write_bytes,
416 config.max_fd_write_bytes,
417 "limits.resources.maxFdWriteBytes",
418 )?;
419 set_optional_usize(
420 &mut limits.max_process_argv_bytes,
421 config.max_process_argv_bytes,
422 "limits.resources.maxProcessArgvBytes",
423 )?;
424 set_optional_usize(
425 &mut limits.max_process_env_bytes,
426 config.max_process_env_bytes,
427 "limits.resources.maxProcessEnvBytes",
428 )?;
429 set_optional_usize(
430 &mut limits.max_readdir_entries,
431 config.max_readdir_entries,
432 "limits.resources.maxReaddirEntries",
433 )?;
434 set_optional_u64(&mut limits.max_wasm_fuel, config.max_wasm_fuel);
435 set_optional_u64(
436 &mut limits.max_wasm_memory_bytes,
437 config.max_wasm_memory_bytes,
438 );
439 set_optional_usize(
440 &mut limits.max_wasm_stack_bytes,
441 config.max_wasm_stack_bytes,
442 "limits.resources.maxWasmStackBytes",
443 )?;
444 Ok(())
445}
446
447fn set_usize(target: &mut usize, value: Option<u64>, key: &str) -> Result<(), SidecarError> {
448 if let Some(value) = value {
449 *target = usize::try_from(value).map_err(|_| integer_too_large(key, value))?;
450 }
451 Ok(())
452}
453
454fn set_u64(target: &mut u64, value: Option<u64>, _key: &str) -> Result<(), SidecarError> {
455 if let Some(value) = value {
456 *target = value;
457 }
458 Ok(())
459}
460
461fn set_optional_usize(
462 target: &mut Option<usize>,
463 value: Option<u64>,
464 key: &str,
465) -> Result<(), SidecarError> {
466 if let Some(value) = value {
467 *target = Some(usize::try_from(value).map_err(|_| integer_too_large(key, value))?);
468 }
469 Ok(())
470}
471
472fn set_optional_u64(target: &mut Option<u64>, value: Option<u64>) {
473 if let Some(value) = value {
474 *target = Some(value);
475 }
476}
477
478fn integer_too_large(key: &str, value: u64) -> SidecarError {
479 SidecarError::InvalidState(format!("{key} value {value} does not fit this platform"))
480}
481
482pub(crate) fn validate_vm_limits(
485 limits: &VmLimits,
486 sidecar_max_frame_bytes: usize,
487) -> Result<(), SidecarError> {
488 if limits.http.max_fetch_response_bytes == 0 {
489 return Err(SidecarError::InvalidState(
490 "limits.http.max_fetch_response_bytes must be greater than zero".to_string(),
491 ));
492 }
493 if limits.http.max_fetch_response_bytes > sidecar_max_frame_bytes {
494 return Err(SidecarError::InvalidState(format!(
495 "limits.http.max_fetch_response_bytes ({}) must be <= the sidecar wire frame cap ({})",
496 limits.http.max_fetch_response_bytes, sidecar_max_frame_bytes
497 )));
498 }
499
500 if limits.tools.default_tool_timeout_ms > limits.tools.max_tool_timeout_ms {
501 return Err(SidecarError::InvalidState(format!(
502 "limits.tools.default_tool_timeout_ms ({}) must be <= limits.tools.max_tool_timeout_ms ({})",
503 limits.tools.default_tool_timeout_ms, limits.tools.max_tool_timeout_ms
504 )));
505 }
506
507 let nonzero_usize: [(&str, usize); 13] = [
508 (
509 "limits.tools.max_registered_toolkits",
510 limits.tools.max_registered_toolkits,
511 ),
512 (
513 "limits.tools.max_registered_tools_per_vm",
514 limits.tools.max_registered_tools_per_vm,
515 ),
516 (
517 "limits.tools.max_tools_per_toolkit",
518 limits.tools.max_tools_per_toolkit,
519 ),
520 (
521 "limits.tools.max_tool_schema_bytes",
522 limits.tools.max_tool_schema_bytes,
523 ),
524 (
525 "limits.tools.max_tool_example_input_bytes",
526 limits.tools.max_tool_example_input_bytes,
527 ),
528 (
529 "limits.plugins.max_persisted_manifest_bytes",
530 limits.plugins.max_persisted_manifest_bytes,
531 ),
532 (
533 "limits.acp.max_read_line_bytes",
534 limits.acp.max_read_line_bytes,
535 ),
536 (
537 "limits.acp.stdout_buffer_byte_limit",
538 limits.acp.stdout_buffer_byte_limit,
539 ),
540 (
541 "limits.js_runtime.captured_output_limit_bytes",
542 limits.js_runtime.captured_output_limit_bytes,
543 ),
544 (
545 "limits.js_runtime.stdin_buffer_limit_bytes",
546 limits.js_runtime.stdin_buffer_limit_bytes,
547 ),
548 (
549 "limits.js_runtime.event_payload_limit_bytes",
550 limits.js_runtime.event_payload_limit_bytes,
551 ),
552 (
553 "limits.python.output_buffer_max_bytes",
554 limits.python.output_buffer_max_bytes,
555 ),
556 (
557 "limits.wasm.captured_output_limit_bytes",
558 limits.wasm.captured_output_limit_bytes,
559 ),
560 ];
561 for (key, value) in nonzero_usize {
562 if value == 0 {
563 return Err(SidecarError::InvalidState(format!(
564 "{key} must be greater than zero"
565 )));
566 }
567 }
568
569 if limits.wasm.sync_read_limit_bytes == 0 {
570 return Err(SidecarError::InvalidState(
571 "limits.wasm.sync_read_limit_bytes must be greater than zero".to_string(),
572 ));
573 }
574 if limits.wasm.max_module_file_bytes == 0 {
575 return Err(SidecarError::InvalidState(
576 "limits.wasm.max_module_file_bytes must be greater than zero".to_string(),
577 ));
578 }
579 if limits.js_runtime.v8_ipc_max_frame_bytes == 0 {
580 return Err(SidecarError::InvalidState(
581 "limits.js_runtime.v8_ipc_max_frame_bytes must be greater than zero".to_string(),
582 ));
583 }
584 if limits.python.execution_timeout_ms == 0 {
585 return Err(SidecarError::InvalidState(
586 "limits.python.execution_timeout_ms must be greater than zero".to_string(),
587 ));
588 }
589 if limits.python.vfs_rpc_timeout_ms == 0 {
590 return Err(SidecarError::InvalidState(
591 "limits.python.vfs_rpc_timeout_ms must be greater than zero".to_string(),
592 ));
593 }
594 if let Some(0) = limits.js_runtime.v8_heap_limit_mb {
595 return Err(SidecarError::InvalidState(
596 "limits.js_runtime.v8_heap_limit_mb must be greater than zero".to_string(),
597 ));
598 }
599
600 Ok(())
601}