Skip to main content

strate_sshd/
main.rs

1#![no_std]
2#![no_main]
3#![feature(alloc_error_handler)]
4
5extern crate alloc;
6
7use alloc::{format, string::String, vec::Vec};
8use base64::Engine;
9use core::{alloc::Layout, panic::PanicInfo};
10use libssh_strat9::{Server, SessionPump};
11use ssh_core::{
12    AuthProvider, CoreDirective, ExecSessionProvider, ExecSessionWiring, HostKeyProvider, SshCore,
13    SshCoreError, Transport,
14};
15use strat9_syscall::{call, error, flag};
16
17const GLOBAL_AUTH_PATHS: [&str; 2] = ["/initfs/etc/ssh/authorized_keys", "/initfs/authorized_keys"];
18const USER_AUTH_DIR: &str = "/initfs/etc/ssh/authorized_keys.d";
19const ALLOWED_COMMANDS_PATH: &str = "/initfs/etc/ssh/allowed_commands";
20const LISTEN_PORT_PATH: &str = "/initfs/etc/ssh/listen_port";
21const MAX_AUTH_TRIES_PATH: &str = "/initfs/etc/ssh/max_auth_tries";
22const DENY_ROOT_LOGIN_PATH: &str = "/initfs/etc/ssh/deny_root_login";
23const LOG_PATH: &str = "/var/log/sshd.log";
24const MAX_AUTH_KEYS: usize = 4096;
25
26alloc_freelist::define_freelist_brk_allocator!(
27    pub struct SshdAllocator;
28    brk = strat9_syscall::call::brk;
29    heap_max = 16 * 1024 * 1024;
30);
31
32#[global_allocator]
33static ALLOCATOR: SshdAllocator = SshdAllocator;
34
35#[alloc_error_handler]
36/// Implements alloc error.
37fn alloc_error(_layout: Layout) -> ! {
38    let _ = call::debug_log(b"[sshd] OOM\n");
39    call::exit(12)
40}
41
42#[panic_handler]
43/// Implements panic.
44fn panic(_info: &PanicInfo) -> ! {
45    let _ = call::debug_log(b"[sshd] panic\n");
46    call::exit(255)
47}
48
49/// Opens log fd.
50fn open_log_fd() -> Option<usize> {
51    let flags = (flag::OpenFlags::WRONLY | flag::OpenFlags::CREATE | flag::OpenFlags::APPEND).bits()
52        as usize;
53    call::openat(0, LOG_PATH, flags, 0).ok()
54}
55
56/// Implements log with fd.
57fn log_with_fd(log_fd: Option<usize>, msg: &str) {
58    if let Some(fd) = log_fd {
59        let _ = call::write(fd, msg.as_bytes());
60    }
61    let _ = call::debug_log(msg.as_bytes());
62}
63
64/// Reads file.
65fn read_file(path: &str) -> Option<Vec<u8>> {
66    let fd = call::openat(0, path, flag::OpenFlags::RDONLY.bits() as usize, 0).ok()?;
67    let mut out = Vec::new();
68    let mut chunk = [0u8; 1024];
69
70    loop {
71        match call::read(fd, &mut chunk) {
72            Ok(0) => break,
73            Ok(n) => out.extend_from_slice(&chunk[..n]),
74            Err(_) => {
75                let _ = call::close(fd);
76                return None;
77            }
78        }
79    }
80
81    let _ = call::close(fd);
82    Some(out)
83}
84
85/// Parses u32 ascii.
86fn parse_u32_ascii(data: &[u8]) -> Option<u32> {
87    let s = core::str::from_utf8(data).ok()?.trim();
88    if s.is_empty() {
89        return None;
90    }
91    let mut out: u32 = 0;
92    for &b in s.as_bytes() {
93        if !b.is_ascii_digit() {
94            return None;
95        }
96        out = out.checked_mul(10)?;
97        out = out.checked_add((b - b'0') as u32)?;
98    }
99    Some(out)
100}
101
102/// Implements load bool flag.
103fn load_bool_flag(path: &str, default: bool) -> bool {
104    let Some(data) = read_file(path) else {
105        return default;
106    };
107    let Ok(s) = core::str::from_utf8(&data) else {
108        return default;
109    };
110    let v = s.trim();
111    if v.eq_ignore_ascii_case("1")
112        || v.eq_ignore_ascii_case("true")
113        || v.eq_ignore_ascii_case("yes")
114        || v.eq_ignore_ascii_case("on")
115    {
116        return true;
117    }
118    if v.eq_ignore_ascii_case("0")
119        || v.eq_ignore_ascii_case("false")
120        || v.eq_ignore_ascii_case("no")
121        || v.eq_ignore_ascii_case("off")
122    {
123        return false;
124    }
125    default
126}
127
128/// Implements load listen port.
129fn load_listen_port() -> u16 {
130    let Some(data) = read_file(LISTEN_PORT_PATH) else {
131        return 22;
132    };
133    let Some(port) = parse_u32_ascii(&data) else {
134        return 22;
135    };
136    if (1..=65535).contains(&port) {
137        port as u16
138    } else {
139        22
140    }
141}
142
143/// Implements load max auth tries.
144fn load_max_auth_tries() -> usize {
145    let Some(data) = read_file(MAX_AUTH_TRIES_PATH) else {
146        return 6;
147    };
148    let Some(v) = parse_u32_ascii(&data) else {
149        return 6;
150    };
151    let v = v.clamp(1, 32);
152    v as usize
153}
154
155/// Opens listener.
156fn open_listener(path: &str, log_fd: Option<usize>) -> usize {
157    let flags = flag::OpenFlags::RDWR.bits() as usize;
158    loop {
159        match call::openat(0, path, flags, 0) {
160            Ok(fd) => return fd,
161            Err(_) => {
162                log_with_fd(log_fd, "[sshd] waiting for /net tcp listener\n");
163                let _ = call::sched_yield();
164            }
165        }
166    }
167}
168
169struct NetTransport {
170    fd: usize,
171    connected: bool,
172    saw_zero_read: bool,
173}
174
175impl NetTransport {
176    /// Creates a new instance.
177    fn new(fd: usize) -> Self {
178        Self {
179            fd,
180            connected: false,
181            saw_zero_read: false,
182        }
183    }
184
185    /// Implements close.
186    fn close(&mut self) {
187        let _ = call::close(self.fd);
188    }
189
190    /// Implements saw zero read.
191    fn saw_zero_read(&mut self) -> bool {
192        let v = self.saw_zero_read;
193        self.saw_zero_read = false;
194        v
195    }
196
197    /// Returns whether connected.
198    fn is_connected(&self) -> bool {
199        self.connected
200    }
201}
202
203impl Transport for NetTransport {
204    /// Implements recv.
205    fn recv(&mut self, out: &mut [u8]) -> ssh_core::Result<usize> {
206        match call::read(self.fd, out) {
207            Ok(0) => {
208                self.saw_zero_read = true;
209                Ok(0)
210            }
211            Ok(n) => {
212                self.connected = true;
213                self.saw_zero_read = false;
214                Ok(n)
215            }
216            Err(error::Error::Again) | Err(error::Error::Interrupted) => Ok(0),
217            Err(_) => Err(SshCoreError::Backend),
218        }
219    }
220
221    /// Implements send.
222    fn send(&mut self, data: &[u8]) -> ssh_core::Result<usize> {
223        let mut off = 0usize;
224        while off < data.len() {
225            match call::write(self.fd, &data[off..]) {
226                Ok(0) => return Err(SshCoreError::Backend),
227                Ok(n) => off += n,
228                Err(error::Error::Again) | Err(error::Error::Interrupted) => {
229                    let _ = call::sched_yield();
230                }
231                Err(_) => return Err(SshCoreError::Backend),
232            }
233        }
234        Ok(off)
235    }
236}
237
238struct FixedHostKey {
239    key: &'static [u8],
240}
241
242impl HostKeyProvider for FixedHostKey {
243    /// Implements host public key.
244    fn host_public_key(&self) -> &[u8] {
245        self.key
246    }
247
248    /// Implements sign exchange hash.
249    fn sign_exchange_hash(
250        &mut self,
251        exchange_hash: &[u8],
252        out: &mut [u8],
253    ) -> ssh_core::Result<usize> {
254        if out.is_empty() {
255            return Err(SshCoreError::BufferTooSmall);
256        }
257        let mut acc: u8 = 0;
258        for b in exchange_hash {
259            acc ^= *b;
260        }
261        let n = out.len().min(32);
262        for (i, b) in out[..n].iter_mut().enumerate() {
263            *b = acc ^ (i as u8);
264        }
265        Ok(n)
266    }
267}
268
269struct AuthorizedKey {
270    algo: Vec<u8>,
271    key_blob: Vec<u8>,
272}
273
274struct PublicKeyAuth {
275    global_paths: &'static [&'static str],
276    user_dir: &'static str,
277    deny_root_login: bool,
278    global_keys: Vec<AuthorizedKey>,
279    global_fingerprint: u64,
280}
281
282impl PublicKeyAuth {
283    /// Builds a value from paths.
284    fn from_paths(
285        global_paths: &'static [&'static str],
286        user_dir: &'static str,
287        deny_root_login: bool,
288    ) -> Self {
289        let mut auth = Self {
290            global_paths,
291            user_dir,
292            deny_root_login,
293            global_keys: Vec::new(),
294            global_fingerprint: 0,
295        };
296        auth.reload_global();
297        auth
298    }
299
300    /// Implements reload global.
301    fn reload_global(&mut self) -> bool {
302        let keys = Self::load_keys_from_paths(self.global_paths);
303        let fp = Self::keys_fingerprint(&keys);
304        if fp != self.global_fingerprint || keys.len() != self.global_keys.len() {
305            self.global_keys = keys;
306            self.global_fingerprint = fp;
307            return true;
308        }
309        false
310    }
311
312    /// Implements load keys from paths.
313    fn load_keys_from_paths(paths: &[&str]) -> Vec<AuthorizedKey> {
314        let mut keys = Vec::new();
315
316        for path in paths {
317            if let Some(data) = read_file(path) {
318                Self::parse_authorized_keys(&data, &mut keys);
319                if !keys.is_empty() {
320                    break;
321                }
322            }
323        }
324
325        keys
326    }
327
328    /// Implements load user keys.
329    fn load_user_keys(&self, username: &[u8]) -> Vec<AuthorizedKey> {
330        let Some(user) = Self::sanitize_username(username) else {
331            return Vec::new();
332        };
333
334        let mut path = String::from(self.user_dir);
335        if !path.ends_with('/') {
336            path.push('/');
337        }
338        path.push_str(&user);
339
340        let Some(data) = read_file(&path) else {
341            return Vec::new();
342        };
343
344        let mut keys = Vec::new();
345        Self::parse_authorized_keys(&data, &mut keys);
346        keys
347    }
348
349    /// Implements sanitize username.
350    fn sanitize_username(username: &[u8]) -> Option<String> {
351        if username.is_empty() || username.len() > 64 {
352            return None;
353        }
354
355        let mut out = String::with_capacity(username.len());
356        for &b in username {
357            let ok = matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' | b'.');
358            if !ok {
359                return None;
360            }
361            out.push(b as char);
362        }
363
364        Some(out)
365    }
366
367    /// Parses authorized keys.
368    fn parse_authorized_keys(data: &[u8], out: &mut Vec<AuthorizedKey>) {
369        let Ok(text) = core::str::from_utf8(data) else {
370            return;
371        };
372
373        for raw in text.lines() {
374            if out.len() >= MAX_AUTH_KEYS {
375                break;
376            }
377
378            let line = raw.trim();
379            if line.is_empty() || line.starts_with('#') {
380                continue;
381            }
382
383            let tokens: Vec<&str> = line.split_whitespace().collect();
384            if tokens.len() < 2 {
385                continue;
386            }
387
388            let Some(algo_idx) = Self::find_algo_index(&tokens) else {
389                continue;
390            };
391            if algo_idx + 1 >= tokens.len() {
392                continue;
393            }
394
395            let algo = tokens[algo_idx];
396            let blob_b64 = tokens[algo_idx + 1];
397
398            let Ok(blob) = base64::engine::general_purpose::STANDARD.decode(blob_b64.as_bytes())
399            else {
400                continue;
401            };
402
403            out.push(AuthorizedKey {
404                algo: algo.as_bytes().to_vec(),
405                key_blob: blob,
406            });
407        }
408    }
409
410    /// Implements find algo index.
411    fn find_algo_index(tokens: &[&str]) -> Option<usize> {
412        let mut i = 0;
413        while i < tokens.len() {
414            if Self::looks_like_algo(tokens[i]) {
415                return Some(i);
416            }
417            i += 1;
418        }
419        None
420    }
421
422    /// Implements looks like algo.
423    fn looks_like_algo(token: &str) -> bool {
424        token.starts_with("ssh-")
425            || token.starts_with("ecdsa-")
426            || token.starts_with("sk-")
427            || token == "rsa-sha2-256"
428            || token == "rsa-sha2-512"
429    }
430
431    /// Implements matches.
432    fn matches(keys: &[AuthorizedKey], algorithm: &[u8], public_key: &[u8]) -> bool {
433        for key in keys {
434            if key.algo.as_slice() == algorithm && key.key_blob.as_slice() == public_key {
435                return true;
436            }
437        }
438        false
439    }
440
441    /// Implements keys fingerprint.
442    fn keys_fingerprint(keys: &[AuthorizedKey]) -> u64 {
443        let mut hash = 0xcbf29ce484222325u64;
444        for key in keys {
445            for b in key.algo.iter().chain(key.key_blob.iter()) {
446                hash ^= *b as u64;
447                hash = hash.wrapping_mul(0x100000001b3);
448            }
449            hash ^= 0xff;
450            hash = hash.wrapping_mul(0x100000001b3);
451        }
452        hash
453    }
454}
455
456impl AuthProvider for PublicKeyAuth {
457    /// Implements authorize public key.
458    fn authorize_public_key(
459        &mut self,
460        username: &[u8],
461        algorithm: &[u8],
462        public_key: &[u8],
463        _signed_data: &[u8],
464        _signature: &[u8],
465    ) -> ssh_core::Result<bool> {
466        if self.deny_root_login && username == b"root" {
467            return Ok(false);
468        }
469
470        if Self::matches(&self.global_keys, algorithm, public_key) {
471            return Ok(true);
472        }
473
474        let user_keys = self.load_user_keys(username);
475        if Self::matches(&user_keys, algorithm, public_key) {
476            return Ok(true);
477        }
478
479        Ok(false)
480    }
481}
482
483struct ExecPolicy {
484    path: &'static str,
485    allowlist: Vec<String>,
486    fingerprint: u64,
487}
488
489impl ExecPolicy {
490    /// Creates a new instance.
491    fn new(path: &'static str) -> Self {
492        let mut policy = Self {
493            path,
494            allowlist: Vec::new(),
495            fingerprint: 0,
496        };
497        let _ = policy.reload();
498        policy
499    }
500
501    /// Implements reload.
502    fn reload(&mut self) -> bool {
503        let mut allowlist = Vec::new();
504
505        if let Some(data) = read_file(self.path) {
506            if let Ok(text) = core::str::from_utf8(&data) {
507                for raw in text.lines() {
508                    let line = raw.trim();
509                    if line.is_empty() || line.starts_with('#') {
510                        continue;
511                    }
512                    if line.starts_with("/initfs/bin/") {
513                        allowlist.push(String::from(line));
514                    }
515                }
516            }
517        }
518
519        let fp = Self::fingerprint(&allowlist);
520        if fp != self.fingerprint || allowlist.len() != self.allowlist.len() {
521            self.allowlist = allowlist;
522            self.fingerprint = fp;
523            return true;
524        }
525        false
526    }
527
528    /// Implements fingerprint.
529    fn fingerprint(items: &[String]) -> u64 {
530        let mut hash = 0xcbf29ce484222325u64;
531        for item in items {
532            for b in item.as_bytes() {
533                hash ^= *b as u64;
534                hash = hash.wrapping_mul(0x100000001b3);
535            }
536            hash ^= 0xff;
537            hash = hash.wrapping_mul(0x100000001b3);
538        }
539        hash
540    }
541
542    /// Returns whether allowed.
543    fn is_allowed(&self, path: &str) -> bool {
544        if self.allowlist.is_empty() {
545            return true;
546        }
547        self.allowlist.iter().any(|p| p == path)
548    }
549}
550
551struct ExecProc {
552    session_id: u32,
553    pid: usize,
554}
555
556struct ExecPlan {
557    path: String,
558    args: Vec<String>,
559}
560
561struct ExecBridge {
562    session_seq: u32,
563    procs: Vec<ExecProc>,
564    policy: ExecPolicy,
565    log_fd: Option<usize>,
566}
567
568impl ExecBridge {
569    /// Creates a new instance.
570    fn new(policy_path: &'static str, log_fd: Option<usize>) -> Self {
571        Self {
572            session_seq: 1,
573            procs: Vec::new(),
574            policy: ExecPolicy::new(policy_path),
575            log_fd,
576        }
577    }
578
579    /// Implements reload policy.
580    fn reload_policy(&mut self) -> bool {
581        self.policy.reload()
582    }
583
584    /// Implements terminate all.
585    fn terminate_all(&mut self) {
586        while let Some(proc) = self.procs.pop() {
587            let _ = call::kill(proc.pid as isize, 15);
588            let mut status = 0;
589            let _ = call::waitpid(proc.pid as isize, Some(&mut status), 0);
590        }
591    }
592
593    /// Returns whether safe exec byte.
594    fn is_safe_exec_byte(b: u8) -> bool {
595        matches!(
596            b,
597            b'a'..=b'z'
598                | b'A'..=b'Z'
599                | b'0'..=b'9'
600                | b'/'
601                | b'.'
602                | b'-'
603                | b'_'
604                | b':'
605                | b'='
606                | b'+'
607                | b'@'
608        )
609    }
610
611    /// Parses exec plan.
612    fn parse_exec_plan(&self, command: &[u8]) -> ssh_core::Result<ExecPlan> {
613        let text = core::str::from_utf8(command).map_err(|_| SshCoreError::Unsupported)?;
614        let trimmed = text.trim();
615
616        let mut args: Vec<String> = Vec::new();
617
618        if trimmed.is_empty() {
619            args.push(String::from("/initfs/bin/sh"));
620        } else {
621            for token in trimmed.split_whitespace() {
622                if token.is_empty() {
623                    continue;
624                }
625                for b in token.as_bytes() {
626                    if !Self::is_safe_exec_byte(*b) {
627                        return Err(SshCoreError::Unsupported);
628                    }
629                }
630                args.push(String::from(token));
631                if args.len() > 32 {
632                    return Err(SshCoreError::Unsupported);
633                }
634            }
635            if args.is_empty() {
636                args.push(String::from("/initfs/bin/sh"));
637            }
638        }
639
640        let path = args[0].clone();
641        if !path.starts_with("/initfs/bin/") || !self.policy.is_allowed(&path) {
642            return Err(SshCoreError::Unsupported);
643        }
644
645        Ok(ExecPlan { path, args })
646    }
647
648    /// Implements reap exited.
649    fn reap_exited(&mut self) {
650        loop {
651            match call::waitpid(-1, None, call::WNOHANG) {
652                Ok(0) => break,
653                Ok(pid) => self.drop_pid(pid),
654                Err(error::Error::NoChildren) | Err(error::Error::Again) => break,
655                Err(error::Error::Interrupted) => continue,
656                Err(_) => break,
657            }
658        }
659    }
660
661    /// Implements drop pid.
662    fn drop_pid(&mut self, pid: usize) {
663        if let Some(idx) = self.procs.iter().position(|p| p.pid == pid) {
664            self.procs.swap_remove(idx);
665        }
666    }
667}
668
669impl ExecSessionProvider for ExecBridge {
670    /// Implements spawn exec.
671    fn spawn_exec(
672        &mut self,
673        _username: &[u8],
674        command: &[u8],
675    ) -> ssh_core::Result<ExecSessionWiring> {
676        let plan = self.parse_exec_plan(command)?;
677
678        let pid = call::fork().map_err(|_| SshCoreError::Backend)?;
679
680        if pid == 0 {
681            let _ = call::setpgid(0, 0);
682
683            let mut path_c = plan.path.as_bytes().to_vec();
684            path_c.push(0);
685
686            let mut argv_bytes: Vec<Vec<u8>> = Vec::with_capacity(plan.args.len());
687            for arg in plan.args {
688                let mut v = arg.into_bytes();
689                v.push(0);
690                argv_bytes.push(v);
691            }
692
693            let mut argv: Vec<usize> = Vec::with_capacity(argv_bytes.len() + 1);
694            for arg in &argv_bytes {
695                argv.push(arg.as_ptr() as usize);
696            }
697            argv.push(0);
698
699            let envp = [0usize];
700
701            // SAFETY: path_c, argv and envp point to valid process-local buffers during execve.
702            let _ = unsafe {
703                call::execve(
704                    path_c.as_slice(),
705                    argv.as_ptr() as usize,
706                    envp.as_ptr() as usize,
707                )
708            };
709            call::exit(127);
710        }
711
712        let session_id = self.session_seq;
713        self.session_seq = self.session_seq.wrapping_add(1);
714        self.procs.push(ExecProc { session_id, pid });
715
716        log_with_fd(self.log_fd, "[sshd] exec session started\n");
717
718        Ok(ExecSessionWiring {
719            session_id,
720            stdin_ring: 0,
721            stdout_ring: 0,
722            stderr_ring: 0,
723        })
724    }
725
726    /// Closes exec.
727    fn close_exec(&mut self, wiring: &ExecSessionWiring) -> ssh_core::Result<()> {
728        let Some(idx) = self
729            .procs
730            .iter()
731            .position(|p| p.session_id == wiring.session_id)
732        else {
733            return Ok(());
734        };
735
736        let proc = self.procs.swap_remove(idx);
737        let _ = call::kill(proc.pid as isize, 15);
738        let mut status = 0;
739        let _ = call::waitpid(proc.pid as isize, Some(&mut status), 0);
740
741        log_with_fd(self.log_fd, "[sshd] exec session closed\n");
742        Ok(())
743    }
744}
745
746#[unsafe(no_mangle)]
747/// Implements start.
748pub extern "C" fn _start() -> ! {
749    let log_fd = open_log_fd();
750    let listen_port = load_listen_port();
751    let deny_root_login = load_bool_flag(DENY_ROOT_LOGIN_PATH, true);
752    let max_auth_tries = load_max_auth_tries();
753    let listen_path = format!("/net/tcp/listen/{listen_port}");
754
755    log_with_fd(
756        log_fd,
757        "[sshd] started (network scheme + pubkey auth + policy reload + child reap)\n",
758    );
759
760    loop {
761        let fd = open_listener(&listen_path, log_fd);
762
763        let backend = ssh_core::default_backend();
764        let auth = PublicKeyAuth::from_paths(&GLOBAL_AUTH_PATHS, USER_AUTH_DIR, deny_root_login);
765        let host = FixedHostKey {
766            key: b"strat9-sshd-host-key",
767        };
768        let exec = ExecBridge::new(ALLOWED_COMMANDS_PATH, log_fd);
769
770        let core = SshCore::new(backend, auth, host, exec);
771        let mut server = Server::new(core);
772        let mut pump = SessionPump::new(NetTransport::new(fd));
773        let mut rx_buf = [0u8; 4096];
774        let mut tick = 0usize;
775        let mut auth_rejects = 0usize;
776
777        'session: loop {
778            let directives = match pump.pump_once(&mut server, &mut rx_buf) {
779                Ok(directives) => directives,
780                Err(_) => break 'session,
781            };
782
783            if directives.is_empty() {
784                let transport = pump.transport_mut();
785                if transport.saw_zero_read() && transport.is_connected() {
786                    break 'session;
787                }
788                let _ = call::sched_yield();
789            }
790
791            let mut force_close = false;
792            for directive in &directives {
793                match directive {
794                    CoreDirective::AuthAccepted { username } => {
795                        auth_rejects = 0;
796                        let user = core::str::from_utf8(username).unwrap_or("<invalid>");
797                        let line = format!("[sshd] auth accepted user={user}\n");
798                        log_with_fd(log_fd, &line);
799                    }
800                    CoreDirective::AuthRejected => {
801                        auth_rejects = auth_rejects.saturating_add(1);
802                        log_with_fd(log_fd, "[sshd] auth rejected\n");
803                        if auth_rejects >= max_auth_tries {
804                            log_with_fd(log_fd, "[sshd] max auth retries reached\n");
805                            force_close = true;
806                        }
807                    }
808                    CoreDirective::ExecStarted { .. } => {
809                        log_with_fd(log_fd, "[sshd] exec started\n");
810                    }
811                    CoreDirective::CloseConnection => {
812                        force_close = true;
813                    }
814                    CoreDirective::SendPacket(_) | CoreDirective::StdinData { .. } => {}
815                }
816            }
817
818            {
819                let core = server.core_mut();
820                if (tick & 0x7ff) == 0 {
821                    if core.auth_mut().reload_global() {
822                        log_with_fd(log_fd, "[sshd] authorized_keys reloaded\n");
823                    }
824                    if core.sessions_mut().reload_policy() {
825                        log_with_fd(log_fd, "[sshd] command policy reloaded\n");
826                    }
827                }
828                core.sessions_mut().reap_exited();
829            }
830
831            if force_close {
832                break 'session;
833            }
834
835            tick = tick.wrapping_add(1);
836            let _ = call::sched_yield();
837        }
838
839        {
840            let core = server.core_mut();
841            core.sessions_mut().terminate_all();
842        }
843        pump.transport_mut().close();
844        let _ = call::sched_yield();
845    }
846}