Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/third_party/rust/minidump-writer/src/mac/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 24 kB image not shown  

Quelle  mach.rs   Sprache: unbekannt

 
//! Contains various helpers to improve and expand on the bindings provided
//! by `mach2`

// Just exports all of the mach functions we use into a flat list
pub use mach2::{
    kern_return::{kern_return_t, KERN_SUCCESS},
    port::mach_port_name_t,
    task::{self, task_threads},
    task_info,
    thread_act::thread_get_state,
    traps::mach_task_self,
    vm::{mach_vm_deallocate, mach_vm_read, mach_vm_region_recurse},
    vm_region::vm_region_submap_info_64,
};

/// A Mach kernel error.
///
/// See <usr/include/mach/kern_return.h>.
#[derive(thiserror::Error, Debug)]
pub enum KernelError {
    #[error("specified address is not currently valid")]
    InvalidAddress = 1,
    #[error("specified memory is valid, but does not permit the required forms of access")]
    ProtectionFailure = 2,
    #[error("the address range specified is already in use, or no address range of the size specified could be found")]
    NoSpace = 3,
    #[error("the function requested was not applicable to this type of argument, or an argument is invalid")]
    InvalidArgument = 4,
    #[error("the function could not be performed")]
    Failure = 5,
    #[error("system resource could not be allocated to fulfill this request")]
    ResourceShortage = 6,
    #[error("the task in question does not hold receive rights for the port argument")]
    NotReceiver = 7,
    #[error("bogus access restriction")]
    NoAccess = 8,
    #[error(
        "during a page fault, the target address refers to a memory object that has been destroyed"
    )]
    MemoryFailure = 9,
    #[error(
        "during a page fault, the memory object indicated that the data could not be returned"
    )]
    MemoryError = 10,
    #[error("the receive right is already a member of the portset")]
    AlreadyInSet = 11,
    #[error("the receive right is not a member of a port set")]
    NotInSet = 12,
    #[error("the name already denotes a right in the task")]
    NameExists = 13,
    #[error("the operation was aborted")]
    Aborted = 14,
    #[error("the name doesn't denote a right in the task")]
    InvalidName = 15,
    #[error("target task isn't an active task")]
    InvalidTask = 16,
    #[error("the name denotes a right, but not an appropriate right")]
    InvalidRight = 17,
    #[error("a blatant range error")]
    InvalidValue = 18,
    #[error("operation would overflow limit on user-references")]
    UserRefsOverflow = 19,
    #[error("the supplied port capability is improper")]
    InvalidCapability = 20,
    #[error("the task already has send or receive rights for the port under another name")]
    RightExists = 21,
    #[error("target host isn't actually a host")]
    InvalidHost = 22,
    #[error("an attempt was made to supply 'precious' data for memory that is already present in a memory object")]
    MemoryPresent = 23,
    // These 2 are errors which should only ever be seen by the kernel itself
    //MemoryDataMoved = 24,
    //MemoryRestartCopy = 25,
    #[error("an argument applied to assert processor set privilege was not a processor set control port")]
    InvalidProcessorSet = 26,
    #[error("the specified scheduling attributes exceed the thread's limits")]
    PolicyLimit = 27,
    #[error("the specified scheduling policy is not currently enabled for the processor set")]
    InvalidPolicy = 28,
    #[error("the external memory manager failed to initialize the memory object")]
    InvalidObject = 29,
    #[error(
        "a thread is attempting to wait for an event for which there is already a waiting thread"
    )]
    AlreadyWaiting = 30,
    #[error("an attempt was made to destroy the default processor set")]
    DefaultSet = 31,
    #[error("an attempt was made to fetch an exception port that is protected, or to abort a thread while processing a protected exception")]
    ExceptionProtected = 32,
    #[error("a ledger was required but not supplied")]
    InvalidLedger = 33,
    #[error("the port was not a memory cache control port")]
    InvalidMemoryControl = 34,
    #[error("an argument supplied to assert security privilege was not a host security port")]
    InvalidSecurity = 35,
    #[error("thread_depress_abort was called on a thread which was not currently depressed")]
    NotDepressed = 36,
    #[error("object has been terminated and is no longer available")]
    Terminated = 37,
    #[error("lock set has been destroyed and is no longer available")]
    LockSetDestroyed = 38,
    #[error("the thread holding the lock terminated before releasing the lock")]
    LockUnstable = 39,
    #[error("the lock is already owned by another thread")]
    LockOwned = 40,
    #[error("the lock is already owned by the calling thread")]
    LockOwnedSelf = 41,
    #[error("semaphore has been destroyed and is no longer available")]
    SemaphoreDestroyed = 42,
    #[error("return from RPC indicating the target server was terminated before it successfully replied")]
    RpcServerTerminated = 43,
    #[error("terminate an orphaned activation")]
    RpcTerminateOrphan = 44,
    #[error("allow an orphaned activation to continue executing")]
    RpcContinueOrphan = 45,
    #[error("empty thread activation (No thread linked to it)")]
    NotSupported = 46,
    #[error("remote node down or inaccessible")]
    NodeDown = 47,
    #[error("a signalled thread was not actually waiting")]
    NotWaiting = 48,
    #[error("some thread-oriented operation (semaphore_wait) timed out")]
    OperationTimedOut = 49,
    #[error("during a page fault, indicates that the page was rejected as a result of a signature check")]
    CodesignError = 50,
    #[error("the requested property cannot be changed at this time")]
    PoicyStatic = 51,
    #[error("the provided buffer is of insufficient size for the requested data")]
    InsufficientBufferSize = 52,
    #[error("denied by security policy")]
    Denied = 53,
    #[error("the KC on which the function is operating is missing")]
    MissingKC = 54,
    #[error("the KC on which the function is operating is invalid")]
    InvalidKC = 55,
    #[error("a search or query operation did not return a result")]
    NotFound = 56,
}

impl From<mach2::kern_return::kern_return_t> for KernelError {
    fn from(kr: mach2::kern_return::kern_return_t) -> Self {
        use mach2::kern_return::*;

        match kr {
            KERN_INVALID_ADDRESS => Self::InvalidAddress,
            KERN_PROTECTION_FAILURE => Self::ProtectionFailure,
            KERN_NO_SPACE => Self::NoSpace,
            KERN_INVALID_ARGUMENT => Self::InvalidArgument,
            KERN_FAILURE => Self::Failure,
            KERN_RESOURCE_SHORTAGE => Self::ResourceShortage,
            KERN_NOT_RECEIVER => Self::NotReceiver,
            KERN_NO_ACCESS => Self::NoAccess,
            KERN_MEMORY_FAILURE => Self::MemoryFailure,
            KERN_MEMORY_ERROR => Self::MemoryError,
            KERN_ALREADY_IN_SET => Self::AlreadyInSet,
            KERN_NAME_EXISTS => Self::NameExists,
            KERN_INVALID_NAME => Self::InvalidName,
            KERN_INVALID_TASK => Self::InvalidTask,
            KERN_INVALID_RIGHT => Self::InvalidRight,
            KERN_INVALID_VALUE => Self::InvalidValue,
            KERN_UREFS_OVERFLOW => Self::UserRefsOverflow,
            KERN_INVALID_CAPABILITY => Self::InvalidCapability,
            KERN_RIGHT_EXISTS => Self::RightExists,
            KERN_INVALID_HOST => Self::InvalidHost,
            KERN_MEMORY_PRESENT => Self::MemoryPresent,
            KERN_INVALID_PROCESSOR_SET => Self::InvalidProcessorSet,
            KERN_POLICY_LIMIT => Self::PolicyLimit,
            KERN_INVALID_POLICY => Self::InvalidPolicy,
            KERN_INVALID_OBJECT => Self::InvalidObject,
            KERN_ALREADY_WAITING => Self::AlreadyWaiting,
            KERN_DEFAULT_SET => Self::DefaultSet,
            KERN_EXCEPTION_PROTECTED => Self::ExceptionProtected,
            KERN_INVALID_LEDGER => Self::InvalidLedger,
            KERN_INVALID_MEMORY_CONTROL => Self::InvalidMemoryControl,
            KERN_INVALID_SECURITY => Self::InvalidSecurity,
            KERN_NOT_DEPRESSED => Self::NotDepressed,
            KERN_TERMINATED => Self::Terminated,
            KERN_LOCK_SET_DESTROYED => Self::LockSetDestroyed,
            KERN_LOCK_UNSTABLE => Self::LockUnstable,
            KERN_LOCK_OWNED => Self::LockOwned,
            KERN_LOCK_OWNED_SELF => Self::LockOwnedSelf,
            KERN_SEMAPHORE_DESTROYED => Self::SemaphoreDestroyed,
            KERN_RPC_SERVER_TERMINATED => Self::RpcServerTerminated,
            KERN_RPC_TERMINATE_ORPHAN => Self::RpcTerminateOrphan,
            KERN_RPC_CONTINUE_ORPHAN => Self::RpcContinueOrphan,
            KERN_NOT_SUPPORTED => Self::NotSupported,
            KERN_NODE_DOWN => Self::NodeDown,
            KERN_NOT_WAITING => Self::NotWaiting,
            KERN_OPERATION_TIMED_OUT => Self::OperationTimedOut,
            KERN_CODESIGN_ERROR => Self::CodesignError,
            KERN_POLICY_STATIC => Self::PoicyStatic,
            52 => Self::InsufficientBufferSize,
            53 => Self::Denied,
            54 => Self::MissingKC,
            55 => Self::InvalidKC,
            56 => Self::NotFound,
            // This should never happen given a result from a mach call, but
            // in that case we just use `Failure` as the mach header itself
            // describes it as a catch all
            _ => Self::Failure,
        }
    }
}

// From /usr/include/mach/machine/thread_state.h
pub const THREAD_STATE_MAX: usize = 1296;

cfg_if::cfg_if! {
    if #[cfg(target_arch = "x86_64")] {
        /// x86_THREAD_STATE64 in /usr/include/mach/i386/thread_status.h
        pub const THREAD_STATE_FLAVOR: u32 = 4;

        pub type ArchThreadState = mach2::structs::x86_thread_state64_t;
    } else if #[cfg(target_arch = "aarch64")] {
        /// ARM_THREAD_STATE64 in /usr/include/mach/arm/thread_status.h
        pub const THREAD_STATE_FLAVOR: u32 = 6;

        // Missing from mach2 atm
        // _STRUCT_ARM_THREAD_STATE64 from /usr/include/mach/arm/_structs.h
        #[repr(C)]
        pub struct Arm64ThreadState {
            pub x: [u64; 29],
            pub fp: u64,
            pub lr: u64,
            pub sp: u64,
            pub pc: u64,
            pub cpsr: u32,
            __pad: u32,
        }

        pub type ArchThreadState = Arm64ThreadState;
    } else {
        compile_error!("unsupported target arch");
    }
}

#[repr(C, align(8))]
pub struct ThreadState {
    pub state: [u32; THREAD_STATE_MAX],
    pub state_size: u32,
}

impl Default for ThreadState {
    fn default() -> Self {
        Self {
            state: [0u32; THREAD_STATE_MAX],
            state_size: (THREAD_STATE_MAX * std::mem::size_of::<u32>()) as u32,
        }
    }
}

impl ThreadState {
    /// Gets the program counter
    #[inline]
    pub fn pc(&self) -> u64 {
        cfg_if::cfg_if! {
            if #[cfg(target_arch = "x86_64")] {
                self.arch_state().__rip
            } else if #[cfg(target_arch = "aarch64")] {
                self.arch_state().pc
            }
        }
    }

    /// Gets the stack pointer
    #[inline]
    pub fn sp(&self) -> u64 {
        cfg_if::cfg_if! {
            if #[cfg(target_arch = "x86_64")] {
                self.arch_state().__rsp
            } else if #[cfg(target_arch = "aarch64")] {
                self.arch_state().sp
            }
        }
    }

    /// Converts the raw binary blob into the architecture specific state
    #[inline]
    pub fn arch_state(&self) -> &ArchThreadState {
        // SAFETY: hoping the kernel isn't lying
        unsafe { &*(self.state.as_ptr().cast()) }
    }
}

/// Minimal trait that just pairs a structure that can be filled out by
/// [`mach2::task::task_info`] with the "flavor" that tells it the info we
/// actually want to retrieve
pub trait TaskInfo {
    /// One of the `MACH_*_TASK` integers. I assume it's very bad if you implement
    /// this trait and provide the wrong flavor for the struct
    const FLAVOR: u32;
}

/// Minimal trait that just pairs a structure that can be filled out by
/// [`thread_info`] with the "flavor" that tells it the info we
/// actually want to retrieve
pub trait ThreadInfo {
    /// One of the `THREAD_*` integers. I assume it's very bad if you implement
    /// this trait and provide the wrong flavor for the struct
    const FLAVOR: u32;
}

/// <usr/include/mach-o/loader.h>, the file type for the main executable image
pub const MH_EXECUTE: u32 = 0x2;
/// <usr/include/mach-o/loader.h>, the file type dyld, the dynamic loader
pub const MH_DYLINKER: u32 = 0x7;
// usr/include/mach-o/loader.h, magic number for MachHeader
pub const MH_MAGIC_64: u32 = 0xfeedfacf;

/// Load command constants from usr/include/mach-o/loader.h
#[repr(u32)]
#[derive(Debug)]
pub enum LoadCommandKind {
    /// Command to map a segment
    Segment = 0x19,
    /// Dynamically linked shared lib ident
    IdDylib = 0xd,
    /// Image uuid
    Uuid = 0x1b,
    /// Load a dynamic linker. Should only be on MH_EXECUTE (main executable)
    /// images when the dynamic linker is overriden
    LoadDylinker = 0xe,
    /// Dynamic linker identification
    IdDylinker = 0xf,
}

impl LoadCommandKind {
    #[inline]
    fn from_u32(kind: u32) -> Option<Self> {
        Some(if kind == Self::Segment as u32 {
            Self::Segment
        } else if kind == Self::IdDylib as u32 {
            Self::IdDylib
        } else if kind == Self::Uuid as u32 {
            Self::Uuid
        } else if kind == Self::LoadDylinker as u32 {
            Self::LoadDylinker
        } else if kind == Self::IdDylinker as u32 {
            Self::IdDylinker
        } else {
            return None;
        })
    }
}

/// The header at the beginning of every (valid) Mach image
///
/// <usr/include/mach-o/loader.h>
#[repr(C)]
#[derive(Clone)]
pub struct MachHeader {
    /// Mach magic number identifier, this is used to validate the header is valid
    pub magic: u32,
    /// `cpu_type_t` cpu specifier
    pub cpu_type: i32,
    /// `cpu_subtype_t` machine specifier
    pub cpu_sub_type: i32,
    /// Type of file, eg. [`MH_EXECUTE`] for the main executable
    pub file_type: u32,
    /// Number of load commands for the image
    pub num_commands: u32,
    /// Size in bytes of all of the load commands
    pub size_commands: u32,
    pub flags: u32,
    __reserved: u32,
}

/// Every load command is a variable sized struct depending on its type, but
/// they all include the fields in this struct at the beginning
///
/// <usr/include/mach-o/loader.h>
#[repr(C)]
pub struct LoadCommandBase {
    /// Type of load command `LC_*`
    pub cmd: u32,
    /// Total size of the command in bytes
    pub cmd_size: u32,
}

/// The 64-bit segment load command indicates that a part of this file is to be
/// mapped into a 64-bit task's address space.  If the 64-bit segment has
/// sections then section_64 structures directly follow the 64-bit segment
/// command and their size is reflected in `cmdsize`.
#[repr(C)]
pub struct SegmentCommand64 {
    cmd: u32,
    pub cmd_size: u32,
    /// String name of the section
    pub segment_name: [u8; 16],
    /// Memory address the segment is mapped to
    pub vm_addr: u64,
    /// Total size of the segment
    pub vm_size: u64,
    /// File offset of the segment
    pub file_off: u64,
    /// Amount mapped from the file
    pub file_size: u64,
    /// Maximum VM protection
    pub max_prot: i32,
    /// Initial VM protection
    pub init_prot: i32,
    /// Number of sections in the segment
    pub num_sections: u32,
    pub flags: u32,
}

/// Dynamically linked shared libraries are identified by two things.  The
/// pathname (the name of the library as found for execution), and the
/// compatibility version number.  The pathname must match and the compatibility
/// number in the user of the library must be greater than or equal to the
/// library being used.  The time stamp is used to record the time a library was
/// built and copied into user so it can be use to determined if the library used
/// at runtime is exactly the same as used to built the program.
#[repr(C)]
#[derive(Debug)]
pub struct Dylib {
    /// Offset from the load command start to the pathname
    pub name: u32,
    /// Library's build time stamp
    pub timestamp: u32,
    /// Library's current version number
    pub current_version: u32,
    /// Library's compatibility version number
    pub compatibility_version: u32,
}

/// A dynamically linked shared library (filetype == MH_DYLIB in the mach header)
/// contains a dylib_command (cmd == LC_ID_DYLIB) to identify the library.
/// An object that uses a dynamically linked shared library also contains a
/// dylib_command (cmd == LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, or
/// LC_REEXPORT_DYLIB) for each library it uses.
#[repr(C)]
pub struct DylibCommand {
    cmd: u32,
    /// Total size of the command in bytes, including pathname string
    pub cmd_size: u32,
    /// Library identification
    pub dylib: Dylib,
}

/// A program that uses a dynamic linker contains a dylinker_command to identify
/// the name of the dynamic linker (LC_LOAD_DYLINKER).  And a dynamic linker
/// contains a dylinker_command to identify the dynamic linker (LC_ID_DYLINKER).
/// A file can have at most one of these.
/// This struct is also used for the LC_DYLD_ENVIRONMENT load command and
/// contains string for dyld to treat like environment variable.
#[repr(C)]
struct DylinkerCommandRepr {
    /// LC_ID_DYLINKER, LC_LOAD_DYLINKER or LC_DYLD_ENVIRONMENT
    cmd: u32,
    /// includes pathname string
    cmd_size: u32,
    /// Dynamic linker's path name, an offset from the load command address
    name: u32,
}

pub struct DylinkerCommand<'buf> {
    /// LC_ID_DYLINKER, LC_LOAD_DYLINKER or LC_DYLD_ENVIRONMENT
    pub cmd: u32,
    /// includes pathname string
    pub cmd_size: u32,
    /// The offset from the load command where the path was read
    pub name_offset: u32,
    /// Dynamic linker's path name
    pub name: &'buf str,
}

/// The uuid load command contains a single 128-bit unique random number that
/// identifies an object produced by the static link editor.
#[repr(C)]
pub struct UuidCommand {
    cmd: u32,
    pub cmd_size: u32,
    /// The UUID. The components are in big-endian regardless of the host architecture
    pub uuid: [u8; 16],
}

/// A block of load commands for a particular image
pub struct LoadCommands {
    /// The block of memory containing all of the load commands
    pub buffer: Vec<u8>,
    /// The number of actual load commmands that _should_ be in the buffer
    pub count: u32,
}

impl LoadCommands {
    /// Retrieves an iterator over the load commands in the contained buffer
    #[inline]
    pub fn iter(&self) -> LoadCommandsIter<'_> {
        LoadCommandsIter {
            buffer: &self.buffer,
            count: self.count,
        }
    }
}

/// A single load command
pub enum LoadCommand<'buf> {
    Segment(&'buf SegmentCommand64),
    Dylib(&'buf DylibCommand),
    Uuid(&'buf UuidCommand),
    DylinkerCommand(DylinkerCommand<'buf>),
}

pub struct LoadCommandsIter<'buf> {
    buffer: &'buf [u8],
    count: u32,
}

impl<'buf> Iterator for LoadCommandsIter<'buf> {
    type Item = LoadCommand<'buf>;

    fn next(&mut self) -> Option<Self::Item> {
        // SAFETY: we're interpreting raw bytes as C structs, we try and be safe
        unsafe {
            loop {
                if self.count == 0 || self.buffer.len() < std::mem::size_of::<LoadCommandBase>() {
                    return None;
                }

                let header = &*(self.buffer.as_ptr().cast::<LoadCommandBase>());

                // This would mean we've been lied to by the MachHeader and either
                // the size_commands field was too small, or the num_command was
                // too large
                if header.cmd_size as usize > self.buffer.len() {
                    return None;
                }

                let cmd = LoadCommandKind::from_u32(header.cmd).and_then(|kind| {
                    Some(match kind {
                        LoadCommandKind::Segment => LoadCommand::Segment(
                            &*(self.buffer.as_ptr().cast::<SegmentCommand64>()),
                        ),
                        LoadCommandKind::IdDylib => {
                            LoadCommand::Dylib(&*(self.buffer.as_ptr().cast::<DylibCommand>()))
                        }
                        LoadCommandKind::Uuid => {
                            LoadCommand::Uuid(&*(self.buffer.as_ptr().cast::<UuidCommand>()))
                        }
                        LoadCommandKind::LoadDylinker | LoadCommandKind::IdDylinker => {
                            let dcr = &*(self.buffer.as_ptr().cast::<DylinkerCommandRepr>());

                            let nul = self.buffer[dcr.name as usize..header.cmd_size as usize]
                                .iter()
                                .position(|c| *c == 0)?;

                            LoadCommand::DylinkerCommand(DylinkerCommand {
                                cmd: dcr.cmd,
                                cmd_size: dcr.cmd_size,
                                name_offset: dcr.name,
                                name: std::str::from_utf8(
                                    &self.buffer[dcr.name as usize..dcr.name as usize + nul],
                                )
                                .ok()?,
                            })
                        }
                    })
                });

                self.count -= 1;
                self.buffer = &self.buffer[header.cmd_size as usize..];

                if let Some(cmd) = cmd {
                    return Some(cmd);
                }
            }
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let sz = self.count as usize;
        (sz, Some(sz))
    }
}

/// Retrieves an integer sysctl by name. Returns the default value if retrieval
/// fails.
pub fn sysctl_by_name<T: Sized + Default>(name: &[u8]) -> T {
    let mut out = T::default();
    let mut len = std::mem::size_of_val(&out);

    // SAFETY: syscall
    unsafe {
        if libc::sysctlbyname(
            name.as_ptr().cast(),
            (&mut out as *mut T).cast(),
            &mut len,
            std::ptr::null_mut(),
            0,
        ) != 0
        {
            // TODO convert to ascii characters when logging?
            log::warn!("failed to get sysctl for {name:?}");
            T::default()
        } else {
            out
        }
    }
}

/// Retrieves an `i32` sysctl by name and casts it to the specified integer type.
/// Returns the default value if retrieval fails or the value is out of bounds of
/// the specified integer type.
pub fn int_sysctl_by_name<T: TryFrom<i32> + Default>(name: &[u8]) -> T {
    let val = sysctl_by_name::<i32>(name);
    T::try_from(val).unwrap_or_default()
}

/// Retrieves a string sysctl by name. Returns an empty string if the retrieval
/// fails or the string can't be converted to utf-8.
pub fn sysctl_string(name: &[u8]) -> String {
    let mut buf_len = 0;

    // SAFETY: syscalls
    let string_buf = unsafe {
        // Retrieve the size of the string (including null terminator)
        if libc::sysctlbyname(
            name.as_ptr().cast(),
            std::ptr::null_mut(),
            &mut buf_len,
            std::ptr::null_mut(),
            0,
        ) != 0
            || buf_len <= 1
        {
            return String::new();
        }

        let mut buff = Vec::new();
        buff.resize(buf_len, 0);

        if libc::sysctlbyname(
            name.as_ptr().cast(),
            buff.as_mut_ptr().cast(),
            &mut buf_len,
            std::ptr::null_mut(),
            0,
        ) != 0
        {
            return String::new();
        }

        buff.pop(); // remove null terminator
        buff
    };

    String::from_utf8(string_buf).unwrap_or_default()
}

extern "C" {
    /// From <usr/include/mach/mach_traps.h>, this retrieves the normal PID for
    /// the specified task as the syscalls from BSD use PIDs, not mach ports.
    ///
    /// This seems to be marked as "obsolete" in the header, but of course being
    /// Apple, there is no mention of a replacement function or when/if it might
    /// eventually disappear.
    pub fn pid_for_task(task: mach_port_name_t, pid: *mut i32) -> kern_return_t;

    /// Fomr <user/include/mach/thread_act.h>, this retrieves thread info for the
    /// for the specified thread.
    ///
    /// Note that the info_size parameter is actually the size of the thread_info / 4
    /// as it is the number of words in the thread info
    pub fn thread_info(
        thread: u32,
        flavor: u32,
        thread_info: *mut i32,
        info_size: *mut u32,
    ) -> kern_return_t;
}

[ Dauer der Verarbeitung: 0.40 Sekunden  (vorverarbeitet)  ]