Commit c9ff8c6
authored
feat(server): wire video_url content blocks through chat completion handler
* feat(server): wire video_url content blocks through chat handler
Server video requests were silently ignored because `extract_chat_video_paths` sat behind `#[allow(dead_code)]` while the chat-completion handler had no path to feed the helper's output through to the VLM runtime. Wire it up, and add the path-traversal guard that 's security review flagged as latent-HIGH so the wiring lands with the safety check, not after.
Path-traversal guard (`src/server/media.rs`):
The `file://` and bare-local-path branches now require the resolved canonical path to sit under one of the entries in the `MLXCEL_VIDEO_DIR_ALLOWLIST` env var (comma-separated, canonical directories). Empty/unset is fail-closed — every local-filesystem video reference is rejected until an operator explicitly opts in. The guard also rejects symlinks whose canonical target escapes the allowlist (canonicalize resolves symlinks before the prefix check), non-regular files (directories, FIFOs, devices), and files whose extension is not in the project's video allowlist. The public helper now reads the env var; tests use a sibling `extract_chat_video_paths_with_allowlist` so they don't depend on process-wide state.
Handler wiring (`src/server/routes/chat.rs`, `src/server/chat_request.rs`):
`prepare_chat_request_with_cache` now resolves video paths alongside images and audio. The video paths flow through a new `videos` field on `ModelRequest::Generate`, threaded through `BatchScheduler::enqueue_request` and into `prepare_request_vlm_embeddings`, where a new `prepare_request_video_embeddings` helper mirrors the CLI's `compute_gemma4_video_embeddings`: probe ffmpeg, decode each video at the per-request FPS (defaulting to `multimodal::video::DEFAULT_FPS`), expand `<|video|>` placeholders into per-frame `<boi> image_token*N <eoi>` runs, and dispatch through the Gemma 4 vision tower.
Static media-support detection (`src/server/state.rs`, `src/server/startup.rs`):
`AppState` gains a `media_support: ModelMediaSupport` field resolved once at startup from the model directory's `config.json` via `crate::models::get_model_type`. The chat handler short-circuits `video_url` blocks for non-video-capable models with a 400 (`invalid_request_error`) before any allowlist resolution or worker dispatch runs — no token cost, no silent drop. Currently only `Gemma4VLM` flips `video=true`; the detection helper is the single edit point for adding future video-capable variants.
Default-deny rationale: the `MLXCEL_VIDEO_DIR_ALLOWLIST` default is intentionally empty so a server stood up without the env var set cannot be tricked into reading `/etc/passwd` (or any other absolute path) by a request body. Operators trade explicit configuration for a guaranteed-zero attack surface — the same posture the upstream image-fetch helper uses for HTTP timeouts.
Tests:
- `src/server/media_tests.rs` adds six new cases covering the guard: empty allowlist (default state), path outside allowlist, symlink whose target escapes the allowlist (Unix-only), non-regular file with a video extension, happy-path inside the allowlist, and the canonicalization-into-sandbox parent-dir case. The pre-existing `file://`-scheme and bare-local-path tests now construct an explicit sandbox per test rather than relying on process-wide allowlist state.
- `src/server/routes/chat.rs` adds three unit tests for `request_has_video_blocks` covering text-only, image+text, and explicit `video_url` payloads.
- `src/server/startup_tests.rs` adds three tests for `detect_model_media_support` covering Gemma 4 VLM detection, missing config fallback, and text-only model behavior.
`#[allow(dead_code)]` is removed from `extract_chat_video_paths` and its supporting helpers (`resolve_video_url`, `decode_video_data_uri`, `fetch_remote_video`, `infer_video_extension`, `write_video_temp_file`, `sanitize_video_extension`, `MAX_VIDEO_PAYLOAD_SIZE`).
Closes
* fix(server): close temp-file leak and HTTP buffer DoS in video handler
review surfaced four security findings in the video URL handler. This commit closes all four on the same branch.
HIGH-1 — Temp-file leak / disk-fill DoS. Every `data:video/...;base64,...` request and HTTP fetch wrote up to 1 GiB into `/tmp` and never cleaned up, because `write_video_temp_file` returned a bare `PathBuf` and no caller deleted it. The resolver now returns `(PathBuf, Option<TempFile>)` where `TempFile` is the existing `crate::multimodal::video::TempFile` Drop guard for caller-owned paths (data URIs and HTTP-fetched files); pre-existing `file://` paths are not wrapped because we don't own them. `PreparedChatRequest` stashes a `Vec<TempFile>` so the guards live for the duration of the request handling — the scheduler still receives only paths, but the guards drop alongside `PreparedChatRequest` once the response is sent, removing every server-owned temp file regardless of which return path we took (success, error, panic).
HIGH-2 — HTTP fetch buffered the entire body before the size check. `response.bytes.await` would read the entire response body into memory before we could enforce `MAX_VIDEO_PAYLOAD_SIZE`, allowing a hostile origin to OOM the server by lying about `Content-Length`. The fetch now uses `response.bytes_stream` and accumulates per-chunk into a `Vec<u8>` with a `len + chunk_len > cap` check that fires before extending the buffer; on overflow the partial buffer is dropped and the request fails. The shared HTTP client also gained a 5 s connect timeout and a 5-hop redirect cap on top of the existing 10 s total timeout. New `reqwest` feature flag `stream` enables `bytes_stream`.
MEDIUM-1 — Blocking syscalls on the Tokio runtime. `std::fs::canonicalize` and `std::fs::metadata` ran on Tokio worker threads, where a slow disk or NFS mount could stall the executor. Both calls in `resolve_local_video_path` now use `tokio::fs::*` and the function is `async`. `extract_chat_video_paths_with_allowlist` is propagated as `async` to all callers and tests.
MEDIUM-2 — TOCTOU race + writable allowlist warning. The path-based resolver returns a path that ffmpeg then re-opens, so an attacker with write access inside an allowlisted directory can swap the file for a symlink between canonicalization and ffmpeg's open call. The full FD-passing fix is out of scope for this PR (tracked). For this PR a startup-time helper `scan_insecure_allowlist_dirs` walks every directory in `MLXCEL_VIDEO_DIR_ALLOWLIST`, checks Unix permissions via `PermissionsExt::mode`, and emits a `tracing::warn!` for any entry with group/world write bits set. Cfg-gated to `#[cfg(unix)]` and unit-tested with both world-writable and strict (0750) directories. The doc comment on `extract_chat_video_paths_with_allowlist` and the `VIDEO_DIR_ALLOWLIST_ENV` constant call out the residual TOCTOU window so future readers know the mitigation is operational, not technical.
Tests:
- `extract_chat_video_paths_async_canonicalize_works` exercises the async refactor with a parent-dir traversal that forces real canonicalize work.
- `fetch_remote_video_streaming_rejects_oversized` starts an in-process HTTP server that advertises a Content-Length above the cap and confirms the streaming fetch returns no path and no temp guard.
- `chat_request_drops_temp_files_on_completion` issues a `data:video/mp4;base64,...` request, asserts the temp file exists during processing, drops `PreparedChatRequest`, and asserts the temp file is gone.
- `scan_insecure_allowlist_dirs_flags_world_writable_directory` / `_passes_strict_directory` cover the writability scan in both polarities. Mirrored as `startup_warns_on_world_writable_allowlist_dir` / `startup_passes_strict_allowlist_dir` in `startup_tests.rs`.
Validated:
- `cargo build --release --features cuda` clean.
- `cargo test --features cuda --lib -- server::media server::routes::chat server::startup multimodal::video server::chat_request` 143 passed / 1 ignored.
- `cargo clippy --features cuda --lib --tests -- -D warnings` clean (incidental `cloned_ref_to_slice_refs` lint hits in pre-existing tests cleaned up alongside the new ones).
- `cargo fmt --all` clean.
Refs:,.1 parent 19c064d commit c9ff8c6
13 files changed
Lines changed: 1352 additions & 110 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
103 | | - | |
| 103 | + | |
104 | 104 | | |
105 | 105 | | |
106 | 106 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1023 | 1023 | | |
1024 | 1024 | | |
1025 | 1025 | | |
| 1026 | + | |
1026 | 1027 | | |
1027 | 1028 | | |
1028 | 1029 | | |
1029 | | - | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
1030 | 1039 | | |
1031 | 1040 | | |
1032 | 1041 | | |
| |||
1042 | 1051 | | |
1043 | 1052 | | |
1044 | 1053 | | |
| 1054 | + | |
1045 | 1055 | | |
1046 | 1056 | | |
1047 | 1057 | | |
| |||
1067 | 1077 | | |
1068 | 1078 | | |
1069 | 1079 | | |
1070 | | - | |
1071 | | - | |
1072 | | - | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
1073 | 1083 | | |
1074 | 1084 | | |
1075 | 1085 | | |
| |||
1096 | 1106 | | |
1097 | 1107 | | |
1098 | 1108 | | |
1099 | | - | |
1100 | | - | |
1101 | | - | |
1102 | | - | |
1103 | | - | |
1104 | | - | |
1105 | | - | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
1106 | 1116 | | |
1107 | 1117 | | |
1108 | 1118 | | |
| |||
1139 | 1149 | | |
1140 | 1150 | | |
1141 | 1151 | | |
| 1152 | + | |
1142 | 1153 | | |
1143 | 1154 | | |
1144 | 1155 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | | - | |
| 68 | + | |
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
77 | 104 | | |
78 | 105 | | |
79 | 106 | | |
| |||
183 | 210 | | |
184 | 211 | | |
185 | 212 | | |
186 | | - | |
| 213 | + | |
187 | 214 | | |
188 | 215 | | |
| 216 | + | |
189 | 217 | | |
190 | 218 | | |
191 | 219 | | |
192 | 220 | | |
193 | 221 | | |
194 | 222 | | |
| 223 | + | |
| 224 | + | |
195 | 225 | | |
196 | 226 | | |
197 | 227 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1215 | 1215 | | |
1216 | 1216 | | |
1217 | 1217 | | |
| 1218 | + | |
| 1219 | + | |
| 1220 | + | |
| 1221 | + | |
| 1222 | + | |
| 1223 | + | |
| 1224 | + | |
| 1225 | + | |
| 1226 | + | |
| 1227 | + | |
| 1228 | + | |
| 1229 | + | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
| 1244 | + | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
| 1279 | + | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
| 1283 | + | |
| 1284 | + | |
| 1285 | + | |
| 1286 | + | |
| 1287 | + | |
| 1288 | + | |
| 1289 | + | |
| 1290 | + | |
| 1291 | + | |
| 1292 | + | |
| 1293 | + | |
| 1294 | + | |
| 1295 | + | |
| 1296 | + | |
| 1297 | + | |
| 1298 | + | |
0 commit comments