acme_proxy_core/script_hook.rs
1//! The contract every `custom` hook in this server runs under: one script, a
2//! cleared environment, JSON on stdin, an exit code for the verdict.
3//!
4//! Three subsystems delegate to an operator-supplied script —
5//! `signer::custom` (issue/revoke/crl/renewal_info),
6//! `filter::custom` (connection/identifiers) and
7//! `notify::custom` (one event). They differ in what
8//! they put in the environment, what they do with stdout, and how they read the
9//! exit code. They differ in nothing else.
10//!
11//! What they shared was a *security* contract — clear the environment so a
12//! script cannot read the RFC 2136 TSIG secret or the SMTP password, restore a
13//! minimal `PATH`, kill the child when its deadline passes, and bound how much
14//! it may write ([`MAX_SCRIPT_OUTPUT_BYTES`]) — written out three times, token
15//! for token. That is exactly the kind of thing that has to exist once: a
16//! hardening applied to one copy is silently absent from the other two, and
17//! nobody reviewing one of them can tell.
18
19use std::path::{Path, PathBuf};
20use std::process::{Output, Stdio};
21use std::time::Duration;
22
23use tokio::io::AsyncWriteExt;
24use tokio::process::Command;
25use tracing::debug;
26
27/// The `PATH` given to the script, since the server environment is cleared
28/// before each execution. Without it, a script starting with
29/// `#!/usr/bin/env …` would not find its interpreter.
30pub(crate) const DEFAULT_PATH: &str =
31 "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
32
33/// Most a script may write to one stream before it is refused.
34///
35/// Per stream, not combined, so a script that logs to stderr does not spend the
36/// budget its answer needs on stdout.
37///
38/// The value is generous on purpose — every legitimate answer is far below it. A
39/// signer's PEM chain is kilobytes; a megabyte of IPAM names is on the order of
40/// twenty thousand of them. What the ceiling exists for is the runaway case: a
41/// script in a loop, or one that `cat`s something it should not, whose output
42/// this process would otherwise buffer whole. The hook timeout bounds how *long*
43/// that goes on and says nothing about how large it gets, which on the `ipam`
44/// and `filter` hooks is a per-request cost.
45///
46/// Deliberately its own constant rather than `http_client::MAX_RESPONSE_BYTES`,
47/// which happens to carry the same number: that one is a ceiling on a remote
48/// party's HTTP body and this is a ceiling on a local child's pipe, and a future
49/// reason to move one is not a reason to move the other.
50pub(crate) const MAX_SCRIPT_OUTPUT_BYTES: usize = 1024 * 1024;
51
52/// An operator-supplied script, and the budget it runs under.
53#[derive(Debug, Clone)]
54pub struct ScriptHook {
55 path: PathBuf,
56 args: Vec<String>,
57 timeout: Duration,
58}
59
60/// What to hand the script on stdin.
61pub enum ScriptStdin<'a> {
62 /// `/dev/null`. The script gets no payload and cannot block on a read.
63 Null,
64 /// A JSON object, written and then closed.
65 Json(&'a serde_json::Value),
66}
67
68/// Why a script produced no verdict at all — as opposed to producing one this
69/// caller did not like, which is [`ScriptOutcome`]'s business.
70#[derive(Debug, thiserror::Error)]
71pub enum ScriptError {
72 #[error("failed to spawn script {}: {detail}", path.display())]
73 Spawn { path: PathBuf, detail: String },
74 #[error("failed to serialize JSON stdin: {0}")]
75 Serialize(String),
76 #[error("script failed: {0}")]
77 Wait(String),
78 #[error("script timed out after {} ms", .0.as_millis())]
79 Timeout(Duration),
80 /// The script wrote more than [`MAX_SCRIPT_OUTPUT_BYTES`] to one stream.
81 ///
82 /// An error rather than a silent truncation, and the `signer` hook is why:
83 /// a PEM chain cut off in the middle would arrive as an unparsable
84 /// certificate, and the operator would go looking at their CA instead of at
85 /// the script. A named refusal says which it was.
86 #[error("script wrote more than {limit} bytes to {stream}")]
87 OutputTooLarge { limit: usize, stream: &'static str },
88}
89
90/// What a script answered, plus whether it ever read the question.
91#[derive(Debug)]
92pub struct ScriptOutcome {
93 pub output: Output,
94 /// Set when writing the JSON payload to the child's stdin failed — in
95 /// practice `EPIPE`, a script that exited without reading it.
96 ///
97 /// Deliberately not an error on its own. A script whose payload is
98 /// `{"hook":"crl"}` and which exits 0 without reading stdin is behaving
99 /// perfectly reasonably, and turning that into a failure would break
100 /// working deployments. It only matters when the script *also* failed, and
101 /// then it matters a great deal: for the signer's `issue` hook, "the script
102 /// never saw the CSR" reads nothing like "the script rejected the CSR".
103 pub stdin_error: Option<String>,
104}
105
106impl ScriptHook {
107 /// Builds a hook, or `None` when no script is configured.
108 ///
109 /// Each subsystem words its own "you enabled this but gave no path" startup
110 /// error, because only it knows which configuration key to name and what to
111 /// tell the operator to remove.
112 pub fn new(script_path: &str, args: &[String], timeout_ms: u64) -> Option<Self> {
113 if script_path.trim().is_empty() {
114 return None;
115 }
116 Some(Self {
117 path: PathBuf::from(script_path),
118 args: args.to_vec(),
119 timeout: Duration::from_millis(timeout_ms),
120 })
121 }
122
123 /// The script's path, as configured.
124 pub fn path(&self) -> &Path {
125 &self.path
126 }
127
128 /// Runs the script with `envs` in an otherwise empty environment.
129 pub async fn run(
130 &self,
131 envs: &[(&str, &str)],
132 stdin: ScriptStdin<'_>,
133 ) -> Result<ScriptOutcome, ScriptError> {
134 let mut cmd = Command::new(&self.path);
135 cmd.args(&self.args);
136
137 // The script would otherwise inherit the server's entire environment,
138 // which legitimately holds secrets: every `ACME_PROXY_*` configuration
139 // overlay, including the DNS update TSIG key
140 // (`…SIGNER__RELAY__DNS01__RFC2136__TSIG_KEY_SECRET`) and
141 // `notify.email.smtp_password`. An operator-supplied script has no
142 // business receiving those, so it starts from nothing and is given only
143 // the documented variables plus a `PATH` without which a
144 // `#!/usr/bin/env bash` script would not start at all.
145 cmd.env_clear();
146 cmd.env("PATH", DEFAULT_PATH);
147 for (key, value) in envs {
148 cmd.env(key, value);
149 }
150
151 // `tokio::time::timeout` below only abandons the future. Without this,
152 // a child that ignores its deadline outlives it — and since these hooks
153 // run once per request or per event, a blocked script would leak one
154 // process per call.
155 cmd.kill_on_drop(true);
156
157 let piped_stdin = matches!(stdin, ScriptStdin::Json(_));
158 cmd.stdin(if piped_stdin {
159 Stdio::piped()
160 } else {
161 Stdio::null()
162 });
163 cmd.stdout(Stdio::piped());
164 cmd.stderr(Stdio::piped());
165
166 let mut child = cmd.spawn().map_err(|error| ScriptError::Spawn {
167 path: self.path.clone(),
168 detail: error.to_string(),
169 })?;
170
171 // Taken out of the child before anything awaits on it: `wait()` needs
172 // `&mut child`, and the reads below have to run *concurrently* with it
173 // rather than after. A script that fills a pipe buffer nobody is
174 // draining blocks in `write` and never exits, so reading only once the
175 // child had exited would deadlock until the timeout on exactly the
176 // output this function exists to collect.
177 let stdout_pipe = child.stdout.take();
178 let stderr_pipe = child.stderr.take();
179
180 let payload = match stdin {
181 ScriptStdin::Json(payload) => Some(
182 serde_json::to_vec(payload).map_err(|e| ScriptError::Serialize(e.to_string()))?,
183 ),
184 ScriptStdin::Null => None,
185 };
186 let stdin_pipe = child.stdin.take();
187
188 // Writing the payload is one of the joined futures, under the same
189 // timeout, not a step before them. Sequenced first, a script that never
190 // reads stdin — sleeping, or blocked writing a stdout nobody drains yet
191 // — held a payload larger than the pipe buffer in `write_all` with no
192 // deadline at all, since the timeout had not started.
193 let feed = async move {
194 let (Some(bytes), Some(mut pipe)) = (payload, stdin_pipe) else {
195 return Ok::<_, ScriptError>(None);
196 };
197 // Recorded rather than propagated; see `ScriptOutcome::stdin_error`.
198 let mut stdin_error = None;
199 if let Err(error) = pipe.write_all(&bytes).await {
200 stdin_error = Some(error.to_string());
201 } else if let Err(error) = pipe.flush().await {
202 stdin_error = Some(error.to_string());
203 }
204 if let Some(detail) = &stdin_error {
205 debug!(
206 event = "script_stdin_write_failed",
207 outcome = "failure",
208 script_path = %self.path.display(),
209 error = %detail,
210 );
211 }
212 // `pipe` drops here, closing the write end.
213 Ok(stdin_error)
214 };
215
216 // `wait_with_output()`'s job, minus its unbounded appetite: it collects
217 // both pipes with no ceiling, which on the `filter` and `ipam` hooks is
218 // a per-request allocation an operator script gets to choose the size
219 // of. The four futures are joined rather than sequenced for the reason
220 // given at the `take()` above.
221 let collect = async {
222 let (status, stdout, stderr, stdin_error) = tokio::try_join!(
223 async {
224 child
225 .wait()
226 .await
227 .map_err(|e| ScriptError::Wait(e.to_string()))
228 },
229 read_capped(stdout_pipe, "stdout"),
230 read_capped(stderr_pipe, "stderr"),
231 feed,
232 )?;
233 Ok(ScriptOutcome {
234 output: Output {
235 status,
236 stdout,
237 stderr,
238 },
239 stdin_error,
240 })
241 };
242
243 match tokio::time::timeout(self.timeout, collect).await {
244 Ok(result) => result,
245 // The child is killed here rather than left running: `kill_on_drop`
246 // is set above and `child` is dropped as this future is. That covers
247 // the `OutputTooLarge` arm too, where the script is very likely
248 // still writing into a pipe this side has stopped reading.
249 //
250 // Only the child: a process the script started itself (a `sleep`
251 // under `sh`, say) is not in reach of `kill_on_drop` and finishes
252 // on its own. A script that forks long-lived work should `exec` it
253 // or reap it.
254 Err(_) => Err(ScriptError::Timeout(self.timeout)),
255 }
256 }
257
258 /// A one-line reason a script's non-zero exit should be reported as.
259 ///
260 /// First non-empty line of stdout, else of stderr, else the exit status —
261 /// prefixed with the stdin failure when there was one, since a script that
262 /// never received its payload failed for a completely different reason than
263 /// one that read it and objected.
264 pub fn detail(outcome: &ScriptOutcome, noun: &str) -> String {
265 let stdout = String::from_utf8_lossy(&outcome.output.stdout);
266 let stderr = String::from_utf8_lossy(&outcome.output.stderr);
267 let first_line = stdout
268 .lines()
269 .find(|line| !line.trim().is_empty())
270 .or_else(|| stderr.lines().find(|line| !line.trim().is_empty()))
271 .unwrap_or("")
272 .trim();
273
274 let base = if first_line.is_empty() {
275 format!("{noun} exited with status {}", outcome.output.status)
276 } else {
277 first_line.to_string()
278 };
279
280 match &outcome.stdin_error {
281 Some(error) => format!("{base} (the script did not read its input: {error})"),
282 None => base,
283 }
284 }
285}
286
287/// Reads one of the child's pipes to EOF, refusing it past
288/// [`MAX_SCRIPT_OUTPUT_BYTES`].
289///
290/// `take(limit + 1)` rather than `take(limit)` is what makes "exactly at the
291/// limit" distinguishable from "over it": a reader capped at the limit hands
292/// back a full buffer in both cases and cannot tell whether more was waiting.
293///
294/// `None` — a pipe already taken, which cannot happen from [`ScriptHook::run`]
295/// since both are `Stdio::piped()` — reads as empty rather than as an error,
296/// matching what `wait_with_output` does with an absent pipe.
297async fn read_capped(
298 pipe: Option<impl tokio::io::AsyncRead + Unpin>,
299 stream: &'static str,
300) -> Result<Vec<u8>, ScriptError> {
301 use tokio::io::AsyncReadExt;
302
303 let Some(pipe) = pipe else {
304 return Ok(Vec::new());
305 };
306
307 let limit = MAX_SCRIPT_OUTPUT_BYTES;
308 let mut buffer = Vec::new();
309 pipe.take(limit as u64 + 1)
310 .read_to_end(&mut buffer)
311 .await
312 .map_err(|error| ScriptError::Wait(error.to_string()))?;
313
314 if buffer.len() > limit {
315 return Err(ScriptError::OutputTooLarge { limit, stream });
316 }
317 Ok(buffer)
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::testutil::{TempDir, write_script};
324
325 fn hook(path: &Path, timeout_ms: u64) -> ScriptHook {
326 ScriptHook::new(&path.display().to_string(), &[], timeout_ms).unwrap()
327 }
328
329 #[test]
330 fn a_blank_script_path_builds_no_hook() {
331 assert!(ScriptHook::new("", &[], 1000).is_none());
332 assert!(ScriptHook::new(" ", &[], 1000).is_none());
333 assert!(ScriptHook::new("/bin/true", &[], 1000).is_some());
334 }
335
336 #[tokio::test]
337 async fn a_missing_script_is_a_spawn_error() {
338 let hook = ScriptHook::new("/nonexistent/script", &[], 1000).unwrap();
339 let error = hook.run(&[], ScriptStdin::Null).await.unwrap_err();
340 assert!(matches!(error, ScriptError::Spawn { .. }), "got {error:?}");
341 assert!(error.to_string().contains("/nonexistent/script"));
342 }
343
344 /// The hardening this module exists to hold in one place: the script must
345 /// not inherit the server's environment, which carries the RFC 2136 TSIG
346 /// key and the SMTP password among other things.
347 #[tokio::test]
348 async fn the_script_does_not_inherit_the_server_environment() {
349 let dir = TempDir::new("script-hook");
350 let script = write_script(
351 &dir,
352 "env.sh",
353 "#!/bin/sh\necho \"MANIFEST=${CARGO_MANIFEST_DIR:-unset}\"\necho \"GIVEN=${ACME_TEST_VAR:-unset}\"\nexit 0\n",
354 );
355
356 let outcome = hook(&script, 5_000)
357 .run(&[("ACME_TEST_VAR", "provided")], ScriptStdin::Null)
358 .await
359 .unwrap();
360 let stdout = String::from_utf8_lossy(&outcome.output.stdout);
361
362 assert!(
363 stdout.contains("MANIFEST=unset"),
364 "the server's own environment must not leak: {stdout}"
365 );
366 assert!(
367 stdout.contains("GIVEN=provided"),
368 "the documented variables must be passed: {stdout}"
369 );
370 }
371
372 #[tokio::test]
373 async fn the_script_receives_its_json_payload_on_stdin() {
374 let dir = TempDir::new("script-hook");
375 let script = write_script(&dir, "cat.sh", "#!/bin/sh\ncat\nexit 0\n");
376
377 let payload = serde_json::json!({ "hook": "issue", "order_id": "abc" });
378 let outcome = hook(&script, 5_000)
379 .run(&[], ScriptStdin::Json(&payload))
380 .await
381 .unwrap();
382
383 let stdout = String::from_utf8_lossy(&outcome.output.stdout);
384 assert!(stdout.contains("\"order_id\":\"abc\""), "{stdout}");
385 assert!(outcome.stdin_error.is_none());
386 }
387
388 /// A script that exits without reading stdin is not a failure — the `crl`
389 /// hook's payload is `{"hook":"crl"}` and ignoring it is reasonable.
390 #[tokio::test]
391 async fn a_script_that_ignores_its_stdin_still_succeeds() {
392 let dir = TempDir::new("script-hook");
393 // Large enough that the write cannot all fit in the pipe buffer, so the
394 // failure is actually observable rather than silently absorbed.
395 let script = write_script(&dir, "ignore.sh", "#!/bin/sh\nexit 0\n");
396
397 let payload = serde_json::json!({ "blob": "x".repeat(256 * 1024) });
398 let outcome = hook(&script, 5_000)
399 .run(&[], ScriptStdin::Json(&payload))
400 .await
401 .unwrap();
402
403 assert!(outcome.output.status.success());
404 }
405
406 /// The payload write is under the timeout too. It used to run before the
407 /// timeout started, so a script that never read its stdin held a payload
408 /// larger than the pipe buffer in `write_all` until the script exited on
409 /// its own — here five seconds, against a 300 ms deadline.
410 #[tokio::test]
411 async fn a_script_that_never_reads_a_large_payload_still_times_out() {
412 let dir = TempDir::new("script-hook");
413 let script = write_script(
414 &dir,
415 "deaf.sh",
416 "#!/bin/sh
417exec sleep 5
418",
419 );
420
421 let payload = serde_json::json!({ "blob": "x".repeat(256 * 1024) });
422 let started = std::time::Instant::now();
423 let error = hook(&script, 300)
424 .run(&[], ScriptStdin::Json(&payload))
425 .await
426 .unwrap_err();
427
428 assert!(matches!(error, ScriptError::Timeout(_)), "got {error:?}");
429 assert!(
430 started.elapsed() < Duration::from_secs(3),
431 "the deadline did not bound the write: {:?}",
432 started.elapsed()
433 );
434 }
435
436 #[tokio::test]
437 async fn a_timed_out_script_is_killed_rather_than_left_running() {
438 let dir = TempDir::new("script-hook");
439 let marker = dir.path().join("still-running");
440 let script = write_script(
441 &dir,
442 "slow.sh",
443 &format!("#!/bin/sh\nsleep 1\ntouch {}\nexit 0\n", marker.display()),
444 );
445
446 let error = hook(&script, 100)
447 .run(&[], ScriptStdin::Null)
448 .await
449 .unwrap_err();
450 assert!(matches!(error, ScriptError::Timeout(_)), "got {error:?}");
451
452 // `kill_on_drop` must have taken the child with the abandoned future;
453 // without it the script would go on to create this file.
454 tokio::time::sleep(Duration::from_millis(1_500)).await;
455 assert!(
456 !marker.exists(),
457 "the script outlived its deadline and kept running"
458 );
459 }
460
461 #[tokio::test]
462 async fn detail_prefers_stdout_then_stderr_then_the_status() {
463 let dir = TempDir::new("script-hook");
464
465 let both = write_script(
466 &dir,
467 "both.sh",
468 "#!/bin/sh\necho 'from stdout'\necho 'from stderr' >&2\nexit 1\n",
469 );
470 let outcome = hook(&both, 5_000)
471 .run(&[], ScriptStdin::Null)
472 .await
473 .unwrap();
474 assert_eq!(ScriptHook::detail(&outcome, "test script"), "from stdout");
475
476 let stderr_only = write_script(
477 &dir,
478 "stderr.sh",
479 "#!/bin/sh\necho 'from stderr' >&2\nexit 1\n",
480 );
481 let outcome = hook(&stderr_only, 5_000)
482 .run(&[], ScriptStdin::Null)
483 .await
484 .unwrap();
485 assert_eq!(ScriptHook::detail(&outcome, "test script"), "from stderr");
486
487 let silent = write_script(&dir, "silent.sh", "#!/bin/sh\nexit 3\n");
488 let outcome = hook(&silent, 5_000)
489 .run(&[], ScriptStdin::Null)
490 .await
491 .unwrap();
492 let detail = ScriptHook::detail(&outcome, "test script");
493 assert!(
494 detail.starts_with("test script exited with status"),
495 "{detail}"
496 );
497 }
498
499 #[tokio::test]
500 async fn detail_says_when_the_script_never_read_its_input() {
501 let outcome = ScriptOutcome {
502 output: std::process::Output {
503 status: Default::default(),
504 stdout: b"bad CSR\n".to_vec(),
505 stderr: Vec::new(),
506 },
507 stdin_error: Some("Broken pipe (os error 32)".to_string()),
508 };
509 let detail = ScriptHook::detail(&outcome, "custom signer script");
510 assert!(detail.contains("bad CSR"), "{detail}");
511 assert!(
512 detail.contains("did not read its input"),
513 "a script that never saw the CSR must not read as one that rejected it: {detail}"
514 );
515 }
516
517 #[tokio::test]
518 async fn the_configured_arguments_are_passed() {
519 let dir = TempDir::new("script-hook");
520 let script = write_script(&dir, "args.sh", "#!/bin/sh\necho \"$1|$2\"\nexit 0\n");
521
522 let hook = ScriptHook::new(
523 &script.display().to_string(),
524 &["first".to_string(), "second".to_string()],
525 5_000,
526 )
527 .unwrap();
528 let outcome = hook.run(&[], ScriptStdin::Null).await.unwrap();
529 assert_eq!(
530 String::from_utf8_lossy(&outcome.output.stdout).trim(),
531 "first|second"
532 );
533 }
534
535 // ------------------------------------------------------- the output cap
536
537 /// A script that floods a stream is refused rather than buffered whole.
538 ///
539 /// Both streams, because they are read by two separate futures and a cap
540 /// applied to only one of them is exactly the shape this would regress into.
541 /// The generous timeout is deliberate: it must be the *size* that refuses
542 /// this, not the clock, or the test would pass against no cap at all.
543 #[tokio::test]
544 async fn a_script_that_floods_a_stream_is_refused_rather_than_buffered() {
545 let dir = TempDir::new("script-hook");
546 // `yes` is a tight loop with no sleep in it, so this reaches the cap in
547 // well under the timeout on any machine that can run the suite.
548 let cases = [
549 ("stdout", "#!/bin/sh\nyes 0123456789abcdef\n"),
550 ("stderr", "#!/bin/sh\nyes 0123456789abcdef >&2\n"),
551 ];
552
553 for (stream, body) in cases {
554 let script = write_script(&dir, &format!("flood-{stream}.sh"), body);
555 let error = hook(&script, 30_000)
556 .run(&[], ScriptStdin::Null)
557 .await
558 .unwrap_err();
559
560 match error {
561 ScriptError::OutputTooLarge { limit, stream: got } => {
562 assert_eq!(limit, MAX_SCRIPT_OUTPUT_BYTES);
563 assert_eq!(got, stream, "the wrong stream was named");
564 }
565 other => panic!("expected OutputTooLarge for {stream}, got {other:?}"),
566 }
567 }
568 }
569
570 /// The other side of the boundary, and the one that decides whether the cap
571 /// is usable: output an honest script produces must still arrive whole.
572 ///
573 /// A quarter of the ceiling is far more than any real hook writes — the
574 /// largest is a signer's PEM chain — so a cap that started truncating
575 /// legitimate answers would show up here.
576 #[tokio::test]
577 async fn output_under_the_cap_arrives_intact() {
578 let dir = TempDir::new("script-hook");
579 let count = MAX_SCRIPT_OUTPUT_BYTES / 4 / 16;
580 let script = write_script(
581 &dir,
582 "bulk.sh",
583 &format!("#!/bin/sh\nyes 0123456789abcde | head -n {count}\nexit 0\n"),
584 );
585
586 let outcome = hook(&script, 30_000).run(&[], ScriptStdin::Null).await;
587 let outcome = outcome.expect("output under the cap must not be refused");
588
589 assert!(outcome.output.status.success());
590 // 15 payload bytes plus a newline, per line.
591 assert_eq!(outcome.output.stdout.len(), count * 16);
592 assert!(outcome.output.stderr.is_empty());
593 }
594
595 /// The pipes are read *while* the child runs, not after it exits.
596 ///
597 /// This is what `wait_with_output` did for free and what taking the pipes
598 /// out by hand can quietly lose: a script writing more than one pipe buffer
599 /// (64 KiB on Linux) blocks in `write` until somebody drains it, so a
600 /// `wait()` that ran to completion first would deadlock here until the
601 /// timeout — and report `Timeout`, not this output.
602 #[tokio::test]
603 async fn a_script_writing_more_than_one_pipe_buffer_does_not_deadlock() {
604 let dir = TempDir::new("script-hook");
605 // 512 KiB, comfortably past any platform's pipe buffer and comfortably
606 // under the cap.
607 let count = 32_768;
608 let script = write_script(
609 &dir,
610 "chatty.sh",
611 &format!("#!/bin/sh\nyes 0123456789abcde | head -n {count}\nexit 0\n"),
612 );
613
614 let outcome = hook(&script, 10_000)
615 .run(&[], ScriptStdin::Null)
616 .await
617 .expect("a script filling the pipe buffer must not time out");
618 assert_eq!(outcome.output.stdout.len(), count * 16);
619 }
620}