starnix_core/security/selinux_hooks/
audit.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::task::{CurrentTask, Task};
6use crate::vfs::{
7    DirEntry, DirEntryHandle, FileObject, FileSystem, FsNode, FsStr, NamespaceNode,
8    PathWithReachability,
9};
10use bstr::BStr;
11use fuchsia_sync::Mutex;
12use hex;
13use linux_uapi::AUDIT_AVC;
14use selinux::permission_check::{PermissionCheck, PermissionCheckResult};
15use selinux::{ClassPermission, KernelClass, KernelPermission, SecurityId};
16use starnix_logging::{BugRef, CATEGORY_STARNIX_SECURITY, trace_instant};
17use std::collections::HashMap;
18use std::fmt::{Display, Error};
19use std::num::NonZeroU64;
20use std::sync::LazyLock;
21
22/// Represents a unique auditable instance, for rate limiting purposes.
23#[derive(Clone, Eq, Hash, PartialEq)]
24struct AuditableInstance {
25    source_sid: SecurityId,
26    target_sid: SecurityId,
27    class: KernelClass,
28    bug: NonZeroU64,
29}
30
31/// Stores count of todo_deny logged per auditable instance.
32static TODO_DENY_COUNTS: LazyLock<Mutex<HashMap<AuditableInstance, u64>>> =
33    LazyLock::new(|| Mutex::new(HashMap::new()));
34
35/// Checks whether an audit log entry should still be emitted for this audit instance.
36fn should_audit(
37    source_sid: SecurityId,
38    target_sid: SecurityId,
39    class: KernelClass,
40    bug: NonZeroU64,
41) -> bool {
42    // Audit-log the first few denials, but skip further denials to avoid logspamming.
43    const MAX_TODO_AUDIT_DENIALS: u64 = 5;
44
45    let mut counts = TODO_DENY_COUNTS.lock();
46    let count = counts.entry(AuditableInstance { source_sid, target_sid, class, bug }).or_default();
47    *count += 1;
48    *count <= MAX_TODO_AUDIT_DENIALS
49}
50
51/// Container for a reference to kernel state from which to include details when emitting audit
52/// logging.  [`Auditable`] instances are created from references to objects via `into()`, e.g:
53///
54///   fn my_lovely_hook(current_task: &CurrentTask, ...) {
55///     let audit_context = current_task.into();
56///     check_permission(..., audit_context)
57///   }
58///
59/// Call-sites which need to include context from multiple sources into audit logs can do so by
60/// creating an array of [`Auditable`] instances from those sources, and using `into()` to create
61/// an [`Auditable`] from a reference to that array, e.g:
62///
63///   fn my_lovelier_hook(current_task: &CurrentTask,..., audit_context: Auditable<'_>) {
64///     let audit_context = [audit_context, current_task.into()];
65///     check_permission(..., (&audit_context).into())
66///   }
67///
68/// [`Auditable`] instances are parameterized with the lifetime of the references they contain,
69/// which will be automagically derived by Rust. Since they only consist of a type discriminator and
70/// reference they are cheap to copy, avoiding the need to pass them by-reference if the same
71/// context is to be applied to multiple permission checks.
72#[derive(Clone, Copy)]
73pub enum Auditable<'a> {
74    // keep-sorted start
75    AuditContext(&'a [Auditable<'a>]),
76    Bug(u64),
77    CurrentTask,
78    DirEntry(&'a DirEntry),
79    FileObject(&'a FileObject),
80    FileSystem(&'a FileSystem),
81    FsNode(&'a FsNode),
82    IoctlCommand(u16),
83    Location(&'a std::panic::Location<'a>),
84    Name(&'a FsStr),
85    NamespaceNode(&'a NamespaceNode),
86    NlMsgtype(u16),
87    None,
88    SockOptArguments(u32, u32),
89    Task(&'a Task),
90    TodoCheck,
91    // keep-sorted end
92}
93
94impl Auditable<'_> {
95    fn from_bug(bug_id: u64) -> Self {
96        Auditable::Bug(bug_id)
97    }
98}
99
100impl<'a> From<&'a CurrentTask> for Auditable<'a> {
101    fn from(_value: &'a CurrentTask) -> Self {
102        // This case is vestigal and will be removed.
103        Auditable::CurrentTask
104    }
105}
106
107impl<'a> From<&'a Task> for Auditable<'a> {
108    fn from(value: &'a Task) -> Self {
109        Auditable::Task(value)
110    }
111}
112
113impl<'a> From<&'a DirEntry> for Auditable<'a> {
114    fn from(value: &'a DirEntry) -> Self {
115        Auditable::DirEntry(value)
116    }
117}
118
119impl<'a> From<&'a DirEntryHandle> for Auditable<'a> {
120    fn from(value: &'a DirEntryHandle) -> Self {
121        Auditable::DirEntry(&*value)
122    }
123}
124
125impl<'a> From<&'a FileObject> for Auditable<'a> {
126    fn from(value: &'a FileObject) -> Self {
127        Auditable::FileObject(value)
128    }
129}
130
131impl<'a> From<&'a FsNode> for Auditable<'a> {
132    fn from(value: &'a FsNode) -> Self {
133        Auditable::FsNode(value)
134    }
135}
136
137impl<'a> From<&'a FileSystem> for Auditable<'a> {
138    fn from(value: &'a FileSystem) -> Self {
139        Auditable::FileSystem(value)
140    }
141}
142
143impl<'a> From<&'a std::panic::Location<'a>> for Auditable<'a> {
144    fn from(value: &'a std::panic::Location<'a>) -> Self {
145        Auditable::Location(value)
146    }
147}
148
149impl<'a> From<&'a NamespaceNode> for Auditable<'a> {
150    fn from(value: &'a NamespaceNode) -> Self {
151        Auditable::NamespaceNode(value)
152    }
153}
154
155impl<'a, const N: usize> From<&'a [Auditable<'a>; N]> for Auditable<'a> {
156    fn from(value: &'a [Auditable<'a>; N]) -> Self {
157        Auditable::AuditContext(value)
158    }
159}
160
161/// Emits an audit log entry with the supplied details. See the SELinux Project's "AVC Audit Events"
162/// description (at https://selinuxproject.org/page/NB_AL) for details of the format and fields in
163/// audit logs.
164///
165/// The supplied `permission_check` is used to serialize the `source_sid` and `target_sid` into
166/// their string forms.
167///
168/// If the `result` has a `todo_bug` then the audit entry's decision will be "todo_deny", instead of
169/// the standard "granted" or "denied" decisions, to indicate that the check failed, but was granted
170/// nonetheless, via [`super::todo_check_permission`] or the todo-deny exceptions configuration.
171///
172/// Callers must supply an [`Auditable`] with context for the check (e.g. the calling task, target
173/// file object or filesystem node, etc.).
174pub(super) fn audit_decision(
175    current_task: &CurrentTask,
176    permission_check: &PermissionCheck<'_>,
177    result: PermissionCheckResult,
178    source_sid: SecurityId,
179    target_sid: SecurityId,
180    permission: KernelPermission,
181    audit_data: Auditable<'_>,
182) {
183    trace_instant!(
184        CATEGORY_STARNIX_SECURITY,
185        match (result.permit, result.todo_bug) {
186            (true, None) => c"audit.granted",
187            (true, Some(_)) => c"audit.todo_deny",
188            _ => c"audit.denied",
189        },
190        fuchsia_trace::Scope::Thread
191    );
192
193    let decision = if let Some(todo_bug) = result.todo_bug {
194        // If `todo_bug` is set then this check is being granted to accommodate errata, rather than
195        // the denial being enforced.
196
197        // Re-using the `track_stub!()` internals to track the denial, and determine whether
198        // too many denial audit logs have already been emit for this case.
199        if !should_audit(source_sid, target_sid, permission.class(), todo_bug) {
200            return;
201        }
202
203        // The first few of each `todo_bug` are logged as "todo_deny", and the denial tracked.
204        "todo_deny"
205    } else {
206        if result.permit { "granted" } else { "denied" }
207    };
208
209    // If there is an associated bug then add it to the audit context.
210    let audit_data_with_bug =
211        [Auditable::from_bug(result.todo_bug.map(NonZeroU64::get).unwrap_or(0)), audit_data];
212    let audit_data =
213        if result.todo_bug.is_some() { (&audit_data_with_bug).into() } else { audit_data };
214
215    let audit_logger = current_task.kernel().audit_logger();
216    audit_logger.audit_log(
217        AUDIT_AVC as u16,
218        || {
219            let tclass = permission.class().name();
220            let permission_name = permission.name();
221
222            // The source and target SIDs are by definition allocated to Security Contexts, so there is no
223            // need to handle `sid_to_security_context()` failure.
224            let security_server = permission_check.security_server();
225            let scontext = security_server.sid_to_security_context(source_sid).unwrap();
226            let scontext = BStr::new(&scontext);
227            let tcontext = security_server.sid_to_security_context(target_sid).unwrap();
228            let tcontext = BStr::new(&tcontext);
229
230            // Gather details about the calling task.
231            let pid = current_task.get_pid();
232            let command = current_task.command();
233            format!("avc: {decision} {{ {permission_name} }} for pid={pid} comm=\"{command}\"{audit_data} scontext={scontext} tcontext={tcontext} tclass={tclass}")
234        }
235    );
236}
237
238/// Emits an audit log entry for a check that failed, but will still be granted because it was made
239/// with the [`super::todo_check_permission()`] API.
240pub(super) fn audit_todo_decision(
241    current_task: &CurrentTask,
242    bug: BugRef,
243    permission_check: &PermissionCheck<'_>,
244    mut result: PermissionCheckResult,
245    source_sid: SecurityId,
246    target_sid: SecurityId,
247    permission: KernelPermission,
248    audit_context: Auditable<'_>,
249) {
250    if result.todo_bug.is_none() {
251        result.todo_bug = Some(bug.into());
252        audit_decision(
253            current_task,
254            permission_check,
255            result,
256            source_sid,
257            target_sid,
258            permission,
259            (&[Auditable::TodoCheck, audit_context]).into(),
260        )
261    } else {
262        audit_decision(
263            current_task,
264            permission_check,
265            result,
266            source_sid,
267            target_sid,
268            permission,
269            audit_context,
270        )
271    }
272}
273
274impl Display for Auditable<'_> {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), Error> {
276        match self {
277            Auditable::AuditContext(audit_context) => {
278                for item in *audit_context {
279                    item.fmt(f)?;
280                }
281                Ok(())
282            }
283            Auditable::Bug(bug_id) => {
284                write!(f, " bug={}", bug_id)
285            }
286            Auditable::CurrentTask => Ok(()),
287            Auditable::DirEntry(entry) => {
288                write!(f, " name={}", hex_escape(entry.read().local_name()))
289            }
290            Auditable::FileObject(file) => {
291                write!(f, " path={}", hex_escape(&file.name.path_escaping_chroot()))
292            }
293            Auditable::FileSystem(fs) => {
294                write!(f, " dev={}", hex_escape(&fs.options.source))
295            }
296            Auditable::FsNode(node) => {
297                write!(f, " ino={}", node.ino)
298            }
299            Auditable::IoctlCommand(ioctl) => {
300                write!(f, " ioctlcmd={:#x}", ioctl)
301            }
302            Auditable::NlMsgtype(message_type) => {
303                write!(f, " nl-msgtype={}", message_type)
304            }
305            Auditable::Location(location) => {
306                write!(f, " caller={:?}", location)
307            }
308            Auditable::Name(name) => {
309                write!(f, " name={}", hex_escape(name))
310            }
311            Auditable::NamespaceNode(node) => {
312                let PathWithReachability::Reachable(path) = node.path_from_root(None) else {
313                    return Ok(());
314                };
315                write!(f, " path={}", hex_escape(&path))
316            }
317            Auditable::SockOptArguments(level, optname) => {
318                write!(f, " level={}, optname={}", level, optname)
319            }
320            Auditable::None => Ok(()),
321            Auditable::Task(task) => {
322                write!(f, " pid={}, comm={}", task.get_pid(), task.command())
323            }
324            Auditable::TodoCheck => {
325                write!(f, " todo_check")
326            }
327        }
328    }
329}
330
331struct EscapedString<'a> {
332    value: &'a [u8],
333}
334
335impl<'a> Display for EscapedString<'a> {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), Error> {
337        // SELinux escapes strings containing spaces or control characters, to prevent userspace
338        // being able to construct names that confuse audit-log parsing tooling.
339        // Additionally enforcing that strings are valid UTF-8 encoded allows non-UTF-8 strings to
340        // be expressed losslessy (via hex escaping) rather than being formatted lossily by `bstr`.
341        let maybe_utf8 = str::from_utf8(self.value).ok();
342        if let Some(utf8) = maybe_utf8 {
343            if utf8.find(|c| c <= ' ').is_none() {
344                return write!(f, "\"{}\"", BStr::new(self.value));
345            }
346        }
347        hex::encode_upper(self.value).fmt(f)
348    }
349}
350
351fn hex_escape<'a>(value: &'a [u8]) -> EscapedString<'a> {
352    EscapedString { value }
353}