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