//
// Syd: rock-solid application kernel
// src/kernel/xattr.rs: xattr handlers
//
// Copyright (c) 2023, 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    borrow::Cow,
    mem::MaybeUninit,
    os::fd::{AsFd, AsRawFd},
};

use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, fcntl::AtFlags, NixPath};

use crate::{
    compat::{
        getxattrat, listxattrat, removexattrat, setxattrat, XattrArgs, XATTR_LIST_MAX,
        XATTR_SIZE_MAX,
    },
    config::ROOT_FILE,
    cookie::safe_fchdir,
    kernel::{syscall_path_handler, to_atflags},
    lookup::FsFlags,
    path::XPath,
    req::{PathArgs, SysArg, SysFlags, UNotifyEventRequest},
    sandbox::SandboxGuard,
    xattr::{denyxattr, filterxattr},
};

pub(crate) fn sys_getxattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because fgetxattr requires a read-only
    // fd but we may not have access to open the file! Note, getxattr is
    // a Stat access not Read access! Potential TOCTOU-vectors are
    // handled in syscall_getxattr_handler() where we no longer resolve
    // symlinks.
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::WANT_BASE,
        ..Default::default()
    }];
    syscall_path_handler(request, "getxattr", argv, |path_args, request, sandbox| {
        syscall_getxattr_handler(request, &sandbox, path_args)
    })
}

pub(crate) fn sys_lgetxattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because fgetxattr requires a read-only
    // fd but we may not have access to open the file! Note, getxattr is
    // a Stat access not Read access! Potential TOCTOU-vectors are
    // handled in syscall_getxattr_handler() where we no longer resolve
    // symlinks.
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST | FsFlags::WANT_BASE,
        ..Default::default()
    }];
    syscall_path_handler(request, "lgetxattr", argv, |path_args, request, sandbox| {
        syscall_getxattr_handler(request, &sandbox, path_args)
    })
}

pub(crate) fn sys_fgetxattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // fgetxattr does not work with O_PATH fds.
    // Hence, we have to use WANT_READ.
    let argv = &[SysArg {
        dirfd: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::WANT_READ,
        ..Default::default()
    }];
    syscall_path_handler(request, "fgetxattr", argv, |path_args, request, sandbox| {
        // SAFETY:
        // 1. SysArg has one element.
        // 2. SysArg.path is None asserting dir is Some.
        #[expect(clippy::disallowed_methods)]
        let fd = path_args.0.as_ref().unwrap().dir.as_ref().unwrap();

        let req = request.scmpreq;
        let name = if req.data.args[1] != 0 {
            Some(request.read_xattr(req.data.args[1])?)
        } else {
            None
        };
        let name = name
            .as_ref()
            .map(|cstr| cstr.as_ptr())
            .unwrap_or(std::ptr::null());

        if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
            // SAFETY: Deny user.syd* extended attributes. name is either
            // NULL or a valid nul-terminated C-String.
            // SAFETY: Deny with ENODATA for stealth.
            // SAFETY: Deny only if the Sandbox is locked for the process.
            unsafe { denyxattr(name) }?;
        }

        // SAFETY: The size argument to the getxattr call
        // must not be fully trusted, it can be overly large,
        // and allocating a Vector of that capacity may overflow.
        let len = to_len_cap(req.data.args[3], XATTR_SIZE_MAX)?;
        let mut buf = if len > 0 {
            let mut buf: Vec<u8> = Vec::new();
            buf.try_reserve(len).or(Err(Errno::ENOMEM))?;
            Some(buf)
        } else {
            None
        };
        let ptr = match buf.as_mut() {
            Some(b) => b.as_mut_ptr(),
            None => std::ptr::null_mut(),
        };

        #[expect(clippy::cast_sign_loss)]
        // SAFETY: In libc we trust.
        let mut n = match Errno::result(unsafe {
            libc::fgetxattr(fd.as_raw_fd(), name, ptr.cast(), len)
        }) {
            Ok(n) => n as usize,
            Err(Errno::ERANGE) if len == XATTR_SIZE_MAX => {
                // SAFETY: Avoid a well-behaving process from
                // repeating calls to potentially exhaust memory.
                // See tar's tests for an example.
                return Err(Errno::E2BIG);
            }
            Err(errno) => return Err(errno),
        };

        if let Some(mut buf) = buf {
            // SAFETY: n was returned by the host Linux kernel.
            unsafe { buf.set_len(n) };
            buf.shrink_to_fit();

            n = request.write_mem(&buf, req.data.args[2])?;
        }

        #[expect(clippy::cast_possible_wrap)]
        Ok(request.return_syscall(n as i64))
    })
}

pub(crate) fn sys_getxattrat(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because fgetxattr requires a read-only
    // fd but we may not have access to open the file! Note, getxattrat is
    // a Stat access not Read access! Potential TOCTOU-vectors are
    // handled in syscall_getxattrat_handler() where we no longer resolve
    // symlinks.
    let req = request.scmpreq;

    // SAFETY: Reject undefined/invalid flags.
    let flags = match to_atflags(
        req.data.args[2],
        AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH,
    ) {
        Ok(flags) => flags,
        Err(errno) => return request.fail_syscall(errno),
    };

    let mut fsflags = FsFlags::MUST_PATH | FsFlags::WANT_BASE;
    if flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags.insert(FsFlags::NO_FOLLOW_LAST);
    }

    let empty_path = flags.contains(AtFlags::AT_EMPTY_PATH);
    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        flags: if empty_path {
            SysFlags::EMPTY_PATH
        } else {
            SysFlags::empty()
        },
        fsflags,
        ..Default::default()
    }];
    syscall_path_handler(
        request,
        "getxattrat",
        argv,
        |path_args, request, sandbox| syscall_getxattrat_handler(request, &sandbox, path_args),
    )
}

pub(crate) fn sys_setxattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // fsetxattr does not work with O_PATH fds.
    // Hence, we have to use WANT_READ.
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::WANT_READ,
        ..Default::default()
    }];
    syscall_path_handler(request, "setxattr", argv, |path_args, request, sandbox| {
        syscall_setxattr_handler(request, &sandbox, path_args)
    })
}

pub(crate) fn sys_fsetxattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // fsetxattr does not work with O_PATH fds.
    // Hence, we have to use WANT_READ.
    let argv = &[SysArg {
        dirfd: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::WANT_READ,
        ..Default::default()
    }];
    syscall_path_handler(request, "fsetxattr", argv, |path_args, request, sandbox| {
        syscall_setxattr_handler(request, &sandbox, path_args)
    })
}

pub(crate) fn sys_lsetxattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because fsetxattr requires a read-only
    // fd but we may not have access to open the file!
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST | FsFlags::WANT_BASE,
        ..Default::default()
    }];
    syscall_path_handler(request, "lsetxattr", argv, |path_args, request, sandbox| {
        // SAFETY: SysArg has one element.
        #[expect(clippy::disallowed_methods)]
        let path = path_args.0.as_ref().unwrap();

        let base = if path.base().is_empty() {
            XPath::from_bytes(b".")
        } else {
            path.base()
        };

        let req = request.scmpreq;

        let name = if req.data.args[1] != 0 {
            Some(request.read_xattr(req.data.args[1])?)
        } else {
            None
        };
        let name = name
            .as_ref()
            .map(|cstr| cstr.as_ptr())
            .unwrap_or(std::ptr::null());

        if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
            // SAFETY: Deny user.syd* extended attributes. name is either
            // NULL or a valid nul-terminated C-String.
            // SAFETY: Deny with ENODATA for stealth.
            // SAFETY: Deny only if the Sandbox is locked for the process.
            unsafe { denyxattr(name) }?;
        }

        // SAFETY: The size argument to the setxattr call
        // must not be fully trusted, it can be overly large,
        // and allocating a Vector of that capacity may overflow.
        let (buf, len) = if req.data.args[3] == 0 {
            (None, 0)
        } else {
            let len = to_len_val(req.data.args[3], XATTR_SIZE_MAX)?;
            let buf = request.read_vec(req.data.args[2], len)?;
            let len = buf.len();
            (Some(buf), len)
        };
        let buf = buf.as_ref().map_or(std::ptr::null(), |b| b.as_ptr()) as *const libc::c_void;

        #[expect(clippy::cast_possible_truncation)]
        let flags = req.data.args[4] as libc::c_int;

        match &path.dir {
            Some(fd) => {
                // SAFETY: We use fchdir which is TOCTOU-free!
                safe_fchdir(fd)?;
            }
            None => {
                // SAFETY: `/` is never a symlink!
                safe_fchdir(ROOT_FILE())?;
            }
        };

        // SAFETY: In libc we trust.
        let res = base.with_nix_path(|cstr| unsafe {
            libc::lsetxattr(cstr.as_ptr(), name, buf, len, flags)
        })?;
        Errno::result(res).map(|_| request.return_syscall(0))
    })
}

pub(crate) fn sys_setxattrat(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because fsetxattr requires a read-only
    // fd but we may not have access to open the file! Note, setxattrat is
    // a Chattr access not Read access! Potential TOCTOU-vectors are
    // handled in syscall_setxattrat_handler() where we no longer resolve
    // symlinks.
    let req = request.scmpreq;

    // SAFETY: Reject undefined/invalid flags.
    let flags = match to_atflags(
        req.data.args[2],
        AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH,
    ) {
        Ok(flags) => flags,
        Err(errno) => return request.fail_syscall(errno),
    };

    let mut fsflags = FsFlags::MUST_PATH | FsFlags::WANT_BASE;
    if flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags.insert(FsFlags::NO_FOLLOW_LAST);
    }

    let empty_path = flags.contains(AtFlags::AT_EMPTY_PATH);
    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        flags: if empty_path {
            SysFlags::EMPTY_PATH
        } else {
            SysFlags::empty()
        },
        fsflags,
        ..Default::default()
    }];
    syscall_path_handler(
        request,
        "setxattrat",
        argv,
        |path_args, request, sandbox| syscall_setxattrat_handler(request, &sandbox, path_args),
    )
}

pub(crate) fn sys_flistxattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // flistxattr does not work with O_PATH fds.
    // Hence, we have to use WANT_READ.
    let argv = &[SysArg {
        dirfd: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::WANT_READ,
        ..Default::default()
    }];
    syscall_path_handler(
        request,
        "flistxattr",
        argv,
        |path_args, request, sandbox| {
            // SAFETY:
            // 1. SysArg has one element.
            // 2. SysArg.path is None asserting dir is Some.
            #[expect(clippy::disallowed_methods)]
            let fd = path_args.0.as_ref().unwrap().dir.as_ref().unwrap();

            let req = request.scmpreq;

            // SAFETY: The size argument to the flistxattr call
            // must not be fully trusted, it can be overly large,
            // and allocating a Vector of that capacity may overflow.
            let len = to_len_cap(req.data.args[2], XATTR_LIST_MAX)?;
            let mut buf = if len > 0 {
                let mut buf = Vec::new();
                buf.try_reserve(len).or(Err(Errno::ENOMEM))?;
                Some(buf)
            } else {
                None
            };
            let ptr = buf
                .as_mut()
                .map_or(std::ptr::null_mut(), |b| b.as_mut_ptr())
                as *mut libc::c_char;

            #[expect(clippy::cast_sign_loss)]
            // SAFETY: In libc we trust.
            let mut n = match Errno::result(unsafe { libc::flistxattr(fd.as_raw_fd(), ptr, len) }) {
                Ok(n) => n as usize,
                Err(Errno::ERANGE) if len == XATTR_LIST_MAX => {
                    // SAFETY: Avoid a well-behaving process from
                    // repeating calls to potentially exhaust memory.
                    // See tar's tests for an example.
                    return Err(Errno::E2BIG);
                }
                Err(errno) => return Err(errno),
            };

            if let Some(mut buf) = buf {
                // SAFETY: n was returned by the host Linux kernel.
                unsafe { buf.set_len(n) };
                buf.shrink_to_fit();

                // SAFETY:
                // Filter out attributes that start with "user.syd".
                // Deny only if the Sandbox is locked for the process.
                let buf = if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
                    Cow::Owned(filterxattr(&buf, n)?)
                } else {
                    Cow::Borrowed(&buf)
                };

                n = request.write_mem(&buf, req.data.args[1])?;
            }

            #[expect(clippy::cast_possible_wrap)]
            Ok(request.return_syscall(n as i64))
        },
    )
}

pub(crate) fn sys_listxattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because flistxattr requires a read-only
    // fd but we may not have access to open the file! Note, listxattr
    // is a Stat access not Read access! Potential TOCTOU-vectors are
    // handled in syscall_listxattr_handler() where we no longer resolve
    // symlinks.
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::WANT_BASE,
        ..Default::default()
    }];
    syscall_path_handler(request, "listxattr", argv, |path_args, request, sandbox| {
        syscall_listxattr_handler(request, &sandbox, path_args)
    })
}

pub(crate) fn sys_llistxattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because flistxattr requires a read-only
    // fd but we may not have access to open the file! Note, listxattr
    // is a Stat access not Read access! Potential TOCTOU-vectors are
    // handled in syscall_listxattr_handler() where we no longer resolve
    // symlinks.
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST | FsFlags::WANT_BASE,
        ..Default::default()
    }];
    syscall_path_handler(
        request,
        "llistxattr",
        argv,
        |path_args, request, sandbox| syscall_listxattr_handler(request, &sandbox, path_args),
    )
}

pub(crate) fn sys_removexattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // fremovexattr does not work with O_PATH fds.
    // Hence, we have to use WANT_READ.
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::WANT_READ,
        ..Default::default()
    }];
    syscall_path_handler(
        request,
        "removexattr",
        argv,
        |path_args, request, sandbox| syscall_removexattr_handler(request, &sandbox, path_args),
    )
}

pub(crate) fn sys_listxattrat(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because flistxattr requires a read-only
    // fd but we may not have access to open the file! Note, listxattr
    // is a Stat access not Read access! Potential TOCTOU-vectors are
    // handled in syscall_listxattrat_handler() where we no longer resolve
    // symlinks.
    let req = request.scmpreq;

    // SAFETY: Reject undefined/invalid flags.
    let flags = match to_atflags(
        req.data.args[2],
        AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH,
    ) {
        Ok(flags) => flags,
        Err(errno) => return request.fail_syscall(errno),
    };

    let mut fsflags = FsFlags::MUST_PATH | FsFlags::WANT_BASE;
    if flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags.insert(FsFlags::NO_FOLLOW_LAST);
    }

    let empty_path = flags.contains(AtFlags::AT_EMPTY_PATH);
    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        flags: if empty_path {
            SysFlags::EMPTY_PATH
        } else {
            SysFlags::empty()
        },
        fsflags,
        ..Default::default()
    }];

    syscall_path_handler(
        request,
        "listxattrat",
        argv,
        |path_args, request, sandbox| syscall_listxattrat_handler(request, &sandbox, path_args),
    )
}

pub(crate) fn sys_fremovexattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // fremovexattr does not work with O_PATH fds.
    // Hence, we have to use WANT_READ.
    let argv = &[SysArg {
        dirfd: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::WANT_READ,
        ..Default::default()
    }];
    syscall_path_handler(
        request,
        "fremovexattr",
        argv,
        |path_args, request, sandbox| syscall_removexattr_handler(request, &sandbox, path_args),
    )
}

pub(crate) fn sys_lremovexattr(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because fremovexattr requires a read-only
    // fd but we may not have access to open the file!
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST | FsFlags::WANT_BASE,
        ..Default::default()
    }];
    syscall_path_handler(
        request,
        "lremovexattr",
        argv,
        |path_args, request, sandbox| {
            // SAFETY: SysArg has one element.
            #[expect(clippy::disallowed_methods)]
            let path = path_args.0.as_ref().unwrap();

            let base = if path.base().is_empty() {
                XPath::from_bytes(b".")
            } else {
                path.base()
            };

            let req = request.scmpreq;

            let name = if req.data.args[1] != 0 {
                Some(request.read_xattr(req.data.args[1])?)
            } else {
                None
            };
            let name = name
                .as_ref()
                .map(|cstr| cstr.as_ptr())
                .unwrap_or(std::ptr::null());

            if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
                // SAFETY: Deny user.syd* extended attributes.
                // name is either NULL or a valid nul-terminated C-String.
                // SAFETY: Deny with ENODATA for stealth.
                // SAFETY: Deny only if the Sandbox is locked for the process.
                unsafe { denyxattr(name) }?;
            }

            match &path.dir {
                Some(fd) => {
                    // SAFETY: We use fchdir which is TOCTOU-free!
                    safe_fchdir(fd)?
                }
                None => {
                    // SAFETY: `/` is never a symlink!
                    safe_fchdir(ROOT_FILE())?;
                }
            };

            let res = base
                // SAFETY: In libc we trust.
                .with_nix_path(|cstr| unsafe { libc::lremovexattr(cstr.as_ptr(), name) })?;
            Errno::result(res).map(|_| request.return_syscall(0))
        },
    )
}

pub(crate) fn sys_removexattrat(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY: We set WANT_BASE because fsetxattr requires a read-only
    // fd but we may not have access to open the file! Note, setxattrat is
    // a Chattr access not Read access! Potential TOCTOU-vectors are
    // handled in syscall_removexattrat_handler() where we no longer resolve
    // symlinks.
    let req = request.scmpreq;

    // SAFETY: Reject undefined/invalid flags.
    let flags = match to_atflags(
        req.data.args[2],
        AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH,
    ) {
        Ok(flags) => flags,
        Err(errno) => return request.fail_syscall(errno),
    };

    let mut fsflags = FsFlags::MUST_PATH | FsFlags::WANT_BASE;
    if flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags.insert(FsFlags::NO_FOLLOW_LAST);
    }

    let empty_path = flags.contains(AtFlags::AT_EMPTY_PATH);
    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        flags: if empty_path {
            SysFlags::EMPTY_PATH
        } else {
            SysFlags::empty()
        },
        fsflags,
        ..Default::default()
    }];
    syscall_path_handler(
        request,
        "removexattrat",
        argv,
        |path_args, request, sandbox| syscall_removexattrat_handler(request, &sandbox, path_args),
    )
}

/// A helper function to handle getxattr-family syscalls.
fn syscall_getxattr_handler(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    args: PathArgs,
) -> Result<ScmpNotifResp, Errno> {
    let req = request.scmpreq;

    // SAFETY: SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();

    let base = if path.base().is_empty() {
        XPath::from_bytes(b".")
    } else {
        path.base()
    };

    let name = if req.data.args[1] != 0 {
        Some(request.read_xattr(req.data.args[1])?)
    } else {
        None
    };
    let name = name
        .as_ref()
        .map(|cstr| cstr.as_ptr())
        .unwrap_or(std::ptr::null());

    if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
        // SAFETY: Deny user.syd* extended attributes. name is either
        // NULL or a valid nul-terminated C-String.
        // SAFETY: Deny with ENODATA for stealth.
        // SAFETY: Deny only if the Sandbox is locked for the process.
        unsafe { denyxattr(name) }?;
    }

    // SAFETY: The size argument to the getxattr call
    // must not be fully trusted, it can be overly large,
    // and allocating a Vector of that capacity may overflow.
    let len = to_len_cap(req.data.args[3], XATTR_SIZE_MAX)?;
    let mut buf = if len > 0 {
        let mut buf: Vec<u8> = Vec::new();
        buf.try_reserve(len).or(Err(Errno::ENOMEM))?;
        Some(buf)
    } else {
        None
    };

    let ptr = match buf.as_mut() {
        Some(b) => b.as_mut_ptr(),
        None => std::ptr::null_mut(),
    };

    match &path.dir {
        Some(fd) => {
            // SAFETY: We use fchdir which is TOCTOU-free!
            safe_fchdir(fd)?;
        }
        None => safe_fchdir(ROOT_FILE())?,
    };

    let res = base
        // SAFETY: We do not resolve symbolic links here!
        .with_nix_path(|cstr| unsafe { libc::lgetxattr(cstr.as_ptr(), name, ptr.cast(), len) })?;

    #[expect(clippy::cast_sign_loss)]
    let mut n = match Errno::result(res) {
        Ok(n) => n as usize,
        Err(Errno::ERANGE) if len == XATTR_SIZE_MAX => {
            // SAFETY: Avoid a well-behaving process from
            // repeating calls to potentially exhaust memory.
            // See tar's tests for an example.
            return Err(Errno::E2BIG);
        }
        Err(errno) => return Err(errno),
    };

    if let Some(mut buf) = buf {
        // SAFETY: n was returned by the host Linux kernel.
        unsafe { buf.set_len(n) };
        buf.shrink_to_fit();

        n = request.write_mem(&buf, req.data.args[2])?;
    }

    #[expect(clippy::cast_possible_wrap)]
    Ok(request.return_syscall(n as i64))
}

/// A helper function to handle getxattrat syscall.
fn syscall_getxattrat_handler(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    args: PathArgs,
) -> Result<ScmpNotifResp, Errno> {
    let req = request.scmpreq;

    // SAFETY: SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();

    let base = if path.base().is_empty() {
        XPath::from_bytes(b".")
    } else {
        path.base()
    };

    // Read struct xattr_args which holds the return pointer, buffer size and flags.
    let mut args = MaybeUninit::<XattrArgs>::zeroed();

    // SAFETY: Ensure size of XattrArgs matches with user argument.
    if req.data.args[5] != size_of::<XattrArgs>() as u64 {
        return Err(Errno::EINVAL);
    }

    // SAFETY: `args` is sized for XattrArgs, and we're just writing bytes to it.
    // We don't read uninitialized memory, and after `read_mem` fills it,
    // we're good to assume it's valid.
    let buf = unsafe {
        std::slice::from_raw_parts_mut(args.as_mut_ptr().cast::<u8>(), size_of::<XattrArgs>())
    };

    // Read the remote data structure.
    request.read_mem(buf, req.data.args[4], buf.len())?;

    // SAFETY: read_mem() has initialized `args` if it succeeded.
    let args = unsafe { args.assume_init() };

    // SAFETY: For getxattrat `flags` member must be zero!
    if args.flags != 0 {
        return Err(Errno::EINVAL);
    }

    let name = if req.data.args[3] != 0 {
        Some(request.read_xattr(req.data.args[3])?)
    } else {
        None
    };
    let name = name
        .as_ref()
        .map(|cstr| cstr.as_ptr())
        .unwrap_or(std::ptr::null());

    if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
        // SAFETY: Deny user.syd* extended attributes. name is either
        // NULL or a valid nul-terminated C-String.
        // SAFETY: Deny with ENODATA for stealth.
        // SAFETY: Deny only if the Sandbox is locked for the process.
        unsafe { denyxattr(name) }?;
    }

    // SAFETY: The size element of the struct xattr_args
    // must not be fully trusted, it can be overly large,
    // and allocating a Vector of that capacity may overflow.
    let len = to_len_cap(args.size.into(), XATTR_SIZE_MAX)?;
    let mut buf = if len > 0 {
        let mut buf = Vec::new();
        buf.try_reserve(len).or(Err(Errno::ENOMEM))?;
        Some(buf)
    } else {
        None
    };

    let fd = match &path.dir {
        Some(fd) => fd.as_fd(),
        None => return Err(Errno::EBADF),
    };

    #[expect(clippy::cast_possible_truncation)]
    let mut my_args = XattrArgs {
        value: match buf.as_mut() {
            Some(b) => b.as_mut_ptr() as *mut libc::c_void as u64,
            None => 0,
        },
        size: len as u32,
        flags: 0,
    };

    // SAFETY:
    // 1. `name` is a valid raw pointer (may be NULL)!
    // 2. We do not resolve symbolic links here!
    let mut n =
        match unsafe { getxattrat(fd, base, name, &mut my_args, AtFlags::AT_SYMLINK_NOFOLLOW) } {
            Ok(n) => n,
            Err(Errno::ERANGE) if len == XATTR_SIZE_MAX => {
                // SAFETY: Avoid a well-behaving process from
                // repeating calls to potentially exhaust memory.
                // See tar's tests for an example.
                return Err(Errno::E2BIG);
            }
            Err(errno) => return Err(errno),
        };

    if let Some(mut buf) = buf {
        // SAFETY: n was returned by the host Linux kernel.
        unsafe { buf.set_len(n) };
        buf.shrink_to_fit();

        n = request.write_mem(&buf, args.value)?;
    }

    #[expect(clippy::cast_possible_wrap)]
    Ok(request.return_syscall(n as i64))
}

/// A helper function to handle setxattr-family syscalls.
fn syscall_setxattr_handler(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    args: PathArgs,
) -> Result<ScmpNotifResp, Errno> {
    // SAFETY:
    // 1. SysArg has one element.
    // 2. `/` is not permitted -> EACCES.
    #[expect(clippy::disallowed_methods)]
    let fd = args.0.as_ref().unwrap().dir.as_ref().ok_or(Errno::EACCES)?;

    let req = request.scmpreq;
    let name = if req.data.args[1] != 0 {
        Some(request.read_xattr(req.data.args[1])?)
    } else {
        None
    };
    let name = name
        .as_ref()
        .map(|cstr| cstr.as_ptr())
        .unwrap_or(std::ptr::null());

    if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
        // SAFETY: Deny user.syd* extended attributes. name is either
        // NULL or a valid nul-terminated C-String.
        // SAFETY: Deny with EACCES to denote access violation.
        // SAFETY: Deny only if the Sandbox is locked for the process.
        unsafe { denyxattr(name) }.map_err(|_| Errno::EACCES)?;
    }

    // SAFETY: The size argument to the setxattr call
    // must not be fully trusted, it can be overly large,
    // and allocating a Vector of that capacity may overflow.
    let (buf, len) = if req.data.args[3] == 0 {
        (None, 0)
    } else {
        let len = to_len_val(req.data.args[3], XATTR_SIZE_MAX)?;
        let buf = request.read_vec(req.data.args[2], len)?;
        let len = buf.len();
        (Some(buf), len)
    };
    let buf = buf.as_ref().map_or(std::ptr::null(), |b| b.as_ptr()) as *const libc::c_void;

    #[expect(clippy::cast_possible_truncation)]
    let flags = req.data.args[4] as libc::c_int;

    // SAFETY: In libc we trust.
    Errno::result(unsafe { libc::fsetxattr(fd.as_raw_fd(), name, buf, len, flags) })
        .map(|_| request.return_syscall(0))
}

/// A helper function to handle setxattrat syscall.
fn syscall_setxattrat_handler(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    args: PathArgs,
) -> Result<ScmpNotifResp, Errno> {
    let req = request.scmpreq;

    // SAFETY: SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();

    let base = if path.base().is_empty() {
        XPath::from_bytes(b".")
    } else {
        path.base()
    };

    // Read struct xattr_args which holds the extension name, buffer size and flags.
    let mut args = MaybeUninit::<XattrArgs>::zeroed();

    // SAFETY: Ensure size of XattrArgs matches with user argument.
    if req.data.args[5] != size_of::<XattrArgs>() as u64 {
        return Err(Errno::EINVAL);
    }

    // SAFETY: `args` is sized for XattrArgs, and we're just writing bytes to it.
    // We don't read uninitialized memory, and after `read_mem` fills it,
    // we're good to assume it's valid.
    let buf = unsafe {
        std::slice::from_raw_parts_mut(args.as_mut_ptr().cast::<u8>(), size_of::<XattrArgs>())
    };

    // Read the remote data structure.
    request.read_mem(buf, req.data.args[4], buf.len())?;

    // SAFETY: read_mem() has initialized `args` if it succeeded.
    let args = unsafe { args.assume_init() };

    let name = if req.data.args[3] != 0 {
        Some(request.read_xattr(req.data.args[3])?)
    } else {
        None
    };
    let name = name
        .as_ref()
        .map(|cstr| cstr.as_ptr())
        .unwrap_or(std::ptr::null());

    if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
        // SAFETY: Deny user.syd* extended attributes. name is either
        // NULL or a valid nul-terminated C-String.
        // SAFETY: Deny with EACCES to denote access violation.
        // SAFETY: Deny only if the Sandbox is locked for the process.
        unsafe { denyxattr(name) }.map_err(|_| Errno::EACCES)?;
    }

    // SAFETY: The size argument to the setxattr call
    // must not be fully trusted, it can be overly large,
    // and allocating a Vector of that capacity may overflow.
    let (buf, len) = if args.size == 0 {
        (None, 0)
    } else {
        let len = to_len_val(args.size.into(), XATTR_SIZE_MAX)?;
        let buf = request.read_vec(args.value, len)?;
        let len = buf.len();
        (Some(buf), len)
    };
    let buf = buf.as_ref().map_or(std::ptr::null(), |b| b.as_ptr()) as *const libc::c_void;

    let fd = match &path.dir {
        Some(fd) => fd.as_fd(),
        None => return Err(Errno::EBADF),
    };

    #[expect(clippy::cast_possible_truncation)]
    let my_args = XattrArgs {
        value: buf as u64,
        size: len as u32,
        flags: args.flags,
    };

    // SAFETY:
    // 1. `name` is a valid raw pointer (may be NULL)!
    // 2. We do not resolve symbolic links here!
    unsafe { setxattrat(fd, base, name, &my_args, AtFlags::AT_SYMLINK_NOFOLLOW) }
        .map(|_| request.return_syscall(0))
}

/// A helper function to handle listxattr-family syscalls.
fn syscall_listxattr_handler(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    args: PathArgs,
) -> Result<ScmpNotifResp, Errno> {
    let req = request.scmpreq;

    // SAFETY: SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();

    let base = if path.base().is_empty() {
        XPath::from_bytes(b".")
    } else {
        path.base()
    };

    // SAFETY: The size argument to the llistxattr call
    // must not be fully trusted, it can be overly large,
    // and allocating a Vector of that capacity may overflow.
    let len = to_len_cap(req.data.args[2], XATTR_LIST_MAX)?;
    let mut buf = if len > 0 {
        let mut buf = Vec::new();
        buf.try_reserve(len).or(Err(Errno::ENOMEM))?;
        Some(buf)
    } else {
        None
    };

    let ptr = buf
        .as_mut()
        .map_or(std::ptr::null_mut(), |b| b.as_mut_ptr()) as *mut libc::c_char;

    match &path.dir {
        Some(fd) => {
            // SAFETY: We use fchdir which is TOCTOU-free!
            safe_fchdir(fd)?;
        }
        None => safe_fchdir(ROOT_FILE())?,
    };

    let res = base
        // SAFETY: We do not resolve symbolic links here!
        .with_nix_path(|cstr| unsafe { libc::llistxattr(cstr.as_ptr(), ptr, len) })?;

    #[expect(clippy::cast_sign_loss)]
    let mut n = match Errno::result(res) {
        Ok(n) => n as usize,
        Err(Errno::ERANGE) if len == XATTR_LIST_MAX => {
            // SAFETY: Avoid a well-behaving process from
            // repeating calls to potentially exhaust memory.
            // See tar's tests for an example.
            return Err(Errno::E2BIG);
        }
        Err(errno) => return Err(errno),
    };

    if let Some(mut buf) = buf {
        // SAFETY: n was returned by the host Linux kernel.
        unsafe { buf.set_len(n) };
        buf.shrink_to_fit();

        // SAFETY:
        // Filter out attributes that start with "user.syd".
        // Deny only if the Sandbox is locked for the process.
        let buf = if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
            Cow::Owned(filterxattr(&buf, n)?)
        } else {
            Cow::Borrowed(&buf)
        };

        n = request.write_mem(&buf, req.data.args[1])?;
    }

    #[expect(clippy::cast_possible_wrap)]
    Ok(request.return_syscall(n as i64))
}

/// A helper function to handle listxattrat syscall.
fn syscall_listxattrat_handler(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    args: PathArgs,
) -> Result<ScmpNotifResp, Errno> {
    let req = request.scmpreq;

    // SAFETY: SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();

    let base = if path.base().is_empty() {
        XPath::from_bytes(b".")
    } else {
        path.base()
    };

    // SAFETY: The size argument to the llistxattr call
    // must not be fully trusted, it can be overly large,
    // and allocating a Vector of that capacity may overflow.
    let len = to_len_cap(req.data.args[4], XATTR_LIST_MAX)?;
    let mut buf = if len > 0 {
        let mut buf = Vec::new();
        buf.try_reserve(len).or(Err(Errno::ENOMEM))?;
        Some(buf)
    } else {
        None
    };

    let ptr = buf
        .as_mut()
        .map_or(std::ptr::null_mut(), |b| b.as_mut_ptr()) as *mut libc::c_char;

    let fd = match &path.dir {
        Some(fd) => fd.as_fd(),
        None => return Err(Errno::EBADF),
    };

    // SAFETY:
    // 1. `ptr` is a valid raw pointer (may be NULL)!
    // 2. We do not resolve symbolic links here!
    let mut n = match unsafe { listxattrat(fd, base, AtFlags::AT_SYMLINK_NOFOLLOW, ptr, len) } {
        Ok(n) => n,
        Err(Errno::ERANGE) if len == XATTR_LIST_MAX => {
            // SAFETY: Avoid a well-behaving process from
            // repeating calls to potentially exhaust memory.
            // See tar's tests for an example.
            return Err(Errno::E2BIG);
        }
        Err(errno) => return Err(errno),
    };

    if let Some(mut buf) = buf {
        // SAFETY: n was returned by the host Linux kernel.
        unsafe { buf.set_len(n) };
        buf.shrink_to_fit();

        // SAFETY:
        // Filter out attributes that start with "user.syd".
        // Deny only if the Sandbox is locked for the process.
        let buf = if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
            Cow::Owned(filterxattr(&buf, n)?)
        } else {
            Cow::Borrowed(&buf)
        };

        n = request.write_mem(&buf, req.data.args[3])?;
    }

    #[expect(clippy::cast_possible_wrap)]
    Ok(request.return_syscall(n as i64))
}

/// A helper function to handle removexattr-family syscalls.
fn syscall_removexattr_handler(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    args: PathArgs,
) -> Result<ScmpNotifResp, Errno> {
    // SAFETY:
    // 1. SysArg has one element.
    // 2. `/` is not permitted -> EACCES.
    #[expect(clippy::disallowed_methods)]
    let fd = args.0.as_ref().unwrap().dir.as_ref().ok_or(Errno::EACCES)?;

    let req = request.scmpreq;

    let name = if req.data.args[1] != 0 {
        Some(request.read_xattr(req.data.args[1])?)
    } else {
        None
    };
    let name = name
        .as_ref()
        .map(|cstr| cstr.as_ptr())
        .unwrap_or(std::ptr::null());

    if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
        // SAFETY: Deny user.syd* extended attributes.
        // name is either NULL or a valid nul-terminated C-String.
        // SAFETY: Deny with ENODATA for stealth.
        // SAFETY: Deny only if the Sandbox is locked for the process.
        unsafe { denyxattr(name) }?;
    }

    // SAFETY: In libc we trust.
    Errno::result(unsafe { libc::fremovexattr(fd.as_raw_fd(), name) })
        .map(|_| request.return_syscall(0))
}

/// A helper function to handle removexattrat syscall.
fn syscall_removexattrat_handler(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    args: PathArgs,
) -> Result<ScmpNotifResp, Errno> {
    let req = request.scmpreq;

    // SAFETY: SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();

    let base = if path.base().is_empty() {
        XPath::from_bytes(b".")
    } else {
        path.base()
    };

    let name = if req.data.args[3] != 0 {
        Some(request.read_xattr(req.data.args[3])?)
    } else {
        None
    };
    let name = name
        .as_ref()
        .map(|cstr| cstr.as_ptr())
        .unwrap_or(std::ptr::null());

    if !sandbox.flags.allow_unsafe_xattr() && sandbox.locked_for(req.pid()) {
        // SAFETY: Deny user.syd* extended attributes.
        // name is either NULL or a valid nul-terminated C-String.
        // SAFETY: Deny with ENODATA for stealth.
        // SAFETY: Deny only if the Sandbox is locked for the process.
        unsafe { denyxattr(name) }?;
    }

    let fd = match &path.dir {
        Some(fd) => fd.as_fd(),
        None => return Err(Errno::EBADF),
    };

    // SAFETY:
    // 1. `name` is a valid raw pointer (may be NULL)!
    // 2. We do not resolve symbolic links here!
    unsafe { removexattrat(fd, base, name, AtFlags::AT_SYMLINK_NOFOLLOW) }
        .map(|_| request.return_syscall(0))
}

// Capping length converter, used by *{get,list}xattr*
#[inline]
fn to_len_cap(arg: u64, max: usize) -> Result<usize, Errno> {
    Ok(usize::try_from(arg).or(Err(Errno::E2BIG))?.min(max))
}

// Validating length converter, used by *setxattr*
#[inline]
fn to_len_val(arg: u64, max: usize) -> Result<usize, Errno> {
    match usize::try_from(arg).or(Err(Errno::ERANGE)) {
        Ok(len) if len <= max => Ok(len),
        _ => Err(Errno::ERANGE),
    }
}
