cloudfox-coreshift-core 2.14.0

Low-level Linux and Android systems primitives for CoreShift (CloudFox)
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/

use super::LogLevel;
use libc::STDERR_FILENO;

/// Strip C0/C1 control characters (except newline) from a log message before
/// writing it to stderr. The daemon logs attacker-controlled strings (package
/// names, paths, error text); without this, ANSI escapes (`\x1b[...]`) and
/// forged newlines pass through verbatim and can spoof log lines or inject
/// terminal-escape sequences into anything that renders the stream.
fn sanitize(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for ch in s.chars() {
        let keep = match ch {
            '\n' | '\t' => true,
            '\u{20}'..='\u{7e}' => true,
            '\u{80}'..='\u{9f}' => false, // C1 control block (incl. ESC)
            '\u{a0}'..='\u{10ffff}' => true, // non-ASCII printable/space
            _ => false,                   // C0 controls except newline/tab (incl. ESC, BEL)
        };
        if keep {
            out.push(ch);
        }
    }
    out
}

pub fn log(level: LogLevel, tag: &str, msg: &str) {
    let level_str = match level {
        LogLevel::Verbose => "VERBOSE",
        LogLevel::Debug => "DEBUG",
        LogLevel::Info => "INFO",
        LogLevel::Warn => "WARN",
        LogLevel::Error => "ERROR",
        LogLevel::Fatal => "FATAL",
    };

    let line = format!("[{level_str}][{tag}] {}\n", sanitize(msg));
    let bytes = line.as_bytes();

    let mut written = 0;
    while written < bytes.len() {
        let r = unsafe {
            libc::write(
                STDERR_FILENO,
                bytes[written..].as_ptr() as *const libc::c_void,
                bytes.len() - written,
            )
        };
        if r < 0 {
            // EINTR is a transient interruption, not a failure: retry the same
            // slice. Any other error (EBADF, ENOSPC, ...) is permanent — give
            // up rather than spin.
            let err = std::io::Error::last_os_error();
            if err.raw_os_error() == Some(libc::EINTR) {
                continue;
            }
            break;
        }
        if r == 0 {
            break;
        }
        written += r as usize;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sanitize_keeps_plain_text_and_newlines() {
        assert_eq!(sanitize("hello world\n"), "hello world\n");
        assert_eq!(sanitize("tab\there"), "tab\there");
    }

    #[test]
    fn sanitize_strips_esc_and_control_chars() {
        // ESC is dropped (breaking the escape sequence) even though the
        // printable bytes that followed it survive; BEL and CRLF are removed.
        assert_eq!(sanitize("a\x1b[2Jb"), "a[2Jb");
        assert_eq!(sanitize("a\x07b"), "ab");
        assert_eq!(sanitize("a\rb"), "ab");
    }

    #[test]
    fn sanitize_keeps_unicode() {
        assert_eq!(sanitize("app名"), "app名");
    }
}