Skip to main content

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