You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nuttx.apache.org by ac...@apache.org on 2026/08/02 21:48:47 UTC

(nuttx) branch master updated (758b9d98fc9 -> 4002e6af5f9)

This is an automated email from the ASF dual-hosted git repository.

acassis pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git


    from 758b9d98fc9 boards/xtensa/esp32s3/esp32s3-m5-cardputer: follow the unified lvglterm input
     new 08afc5f2cf7 fs: enforce path search and unify inode permission helpers
     new 4002e6af5f9 Documentation: describe FS permission interface and mount-crossing

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 Documentation/components/filesystem/littlefs.rst |   3 +
 Documentation/components/filesystem/pseudofs.rst |   4 +
 Documentation/components/filesystem/tmpfs.rst    |  10 ++
 Documentation/implementation/file_permission.rst | 137 +++++++++++++++++++++++
 Documentation/implementation/index.rst           |   1 +
 Documentation/implementation/user_identity.rst   |  13 ++-
 fs/inode/fs_inode.c                              | 131 +++++++++++++++-------
 fs/inode/fs_inoderemove.c                        |   6 +-
 fs/inode/fs_inodereserve.c                       |   8 +-
 fs/inode/inode.h                                 |  39 +++++--
 fs/mount/fs_mount.c                              |   9 ++
 fs/mount/fs_umount2.c                            |  11 +-
 fs/tmpfs/fs_tmpfs.c                              |  41 ++++++-
 fs/vfs/fs_chstat.c                               |   7 ++
 fs/vfs/fs_mkdir.c                                |   7 ++
 fs/vfs/fs_open.c                                 |  12 +-
 fs/vfs/fs_readlink.c                             |   7 ++
 fs/vfs/fs_rename.c                               |  14 ++-
 fs/vfs/fs_rmdir.c                                |   7 ++
 fs/vfs/fs_stat.c                                 |   7 ++
 fs/vfs/fs_statfs.c                               |   7 ++
 fs/vfs/fs_unlink.c                               |   6 +
 include/nuttx/fs/fs.h                            |  12 ++
 23 files changed, 436 insertions(+), 63 deletions(-)
 create mode 100644 Documentation/implementation/file_permission.rst


(nuttx) 02/02: Documentation: describe FS permission interface and mount-crossing

Posted by ac...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git

commit 4002e6af5f99ec7bf2fb91335f4c325c1ec95d9c
Author: Abhishek Mishra <mi...@gmail.com>
AuthorDate: Sun Aug 2 17:54:38 2026 +0000

    Documentation: describe FS permission interface and mount-crossing
    
    Document inode_checkperm / inode_checkpathperm, mountpoint traverse vs open
    semantics, and the optional mountpt_operations.permission hook in
    file_permission.rst.
    
    Signed-off-by: Abhishek Mishra <mi...@gmail.com>
---
 Documentation/components/filesystem/littlefs.rst |   3 +
 Documentation/components/filesystem/pseudofs.rst |   4 +
 Documentation/components/filesystem/tmpfs.rst    |  10 ++
 Documentation/implementation/file_permission.rst | 137 +++++++++++++++++++++++
 Documentation/implementation/index.rst           |   1 +
 Documentation/implementation/user_identity.rst   |  13 ++-
 6 files changed, 165 insertions(+), 3 deletions(-)

diff --git a/Documentation/components/filesystem/littlefs.rst b/Documentation/components/filesystem/littlefs.rst
index 35c042cc8f7..7eeb477012e 100644
--- a/Documentation/components/filesystem/littlefs.rst
+++ b/Documentation/components/filesystem/littlefs.rst
@@ -57,3 +57,6 @@ target directory.
 Files created before permission support was enabled, or without stored
 attributes, default to mode ``0777`` until ``chmod``/``chown`` sets explicit
 metadata.
+
+Path components above the littlefs mountpoint are enforced by the VFS; see
+:ref:`file-permission`.
diff --git a/Documentation/components/filesystem/pseudofs.rst b/Documentation/components/filesystem/pseudofs.rst
index c8e13cd4007..e8397094fac 100644
--- a/Documentation/components/filesystem/pseudofs.rst
+++ b/Documentation/components/filesystem/pseudofs.rst
@@ -211,3 +211,7 @@ will also fail. You cannot do this, for example:
 
 See also NxFileSystem in 
 `Porting Guide <https://cwiki.apache.org/confluence/display/NUTTX/Porting+Guide>`_
+
+When ``CONFIG_FS_PERMISSION`` is enabled, pseudoFS directory modes also gate
+access into mounted volumes beneath them.  See
+:ref:`file-permission`.
diff --git a/Documentation/components/filesystem/tmpfs.rst b/Documentation/components/filesystem/tmpfs.rst
index 1d0dacb4901..c6f146e9ace 100644
--- a/Documentation/components/filesystem/tmpfs.rst
+++ b/Documentation/components/filesystem/tmpfs.rst
@@ -11,3 +11,13 @@ At runtime, simply use ``mount -t tmpfs /tmp`` to have a ``/tmp`` folder backed
 Be aware that TMPFS is backed by kernel memory thus don't expect to store big files on it and its size is limited by free kernel memory.
 
 We can watch the size of TMPFS with ``df -h`` command, especially you can see the ``Size`` column of TMPFS changes when files are added or removed in the TMPFS folder. Changes in TMPFS size is always reflected by reverse changes of free kernel memory size.
+
+Permissions
+===========
+
+When ``CONFIG_FS_PERMISSION`` is enabled, tmpfs stores per-object owner, group,
+and mode and enforces them on path operations inside the volume.  It also
+implements the optional ``mountpt_operations.permission`` hook.
+
+Access into the mount from the pseudoFS is gated by the VFS (parent and
+mountpoint ``X_OK``).  See :ref:`file-permission`.
diff --git a/Documentation/implementation/file_permission.rst b/Documentation/implementation/file_permission.rst
new file mode 100644
index 00000000000..6b6f50c5118
--- /dev/null
+++ b/Documentation/implementation/file_permission.rst
@@ -0,0 +1,137 @@
+.. _file-permission:
+
+========================================
+Filesystem Permission Interface
+========================================
+
+When ``CONFIG_FS_PERMISSION`` is enabled, the VFS applies POSIX-style
+discretionary access control (DAC) using the caller's effective credentials
+(``tg_euid`` / ``tg_egid``).  This page describes the common inode helpers,
+how mountpoints participate, and how access across a mount is gated by
+pseudoFS directory modes.
+
+Prerequisite reading: :ref:`user-identity`.
+
+Configuration
+=============
+
+=============================== =============================================
+Option                          Role
+=============================== =============================================
+``CONFIG_SCHED_USER_IDENTITY``  Per-task-group UID/GID credentials
+``CONFIG_PSEUDOFS_ATTRIBUTES``  Store ``i_mode`` / ``i_owner`` / ``i_group``
+                                on pseudoFS inodes
+``CONFIG_FS_PERMISSION``        Enable DAC helpers and VFS enforcement
+                                (depends on the two options above)
+=============================== =============================================
+
+Without ``CONFIG_FS_PERMISSION``, the helpers described here return success
+and no mode-based checks are performed.
+
+Helpers
+=======
+
+``inode_checkperm``
+  Check ``amode`` (``R_OK`` / ``W_OK`` / ``X_OK``) against an inode's
+  ``i_owner`` / ``i_group`` / ``i_mode``.  Empty macro returning ``0``
+  when ``CONFIG_FS_PERMISSION`` is disabled.
+
+``inode_checkpathperm``
+  Require ``X_OK`` on every ancestor of an inode, and on the inode itself
+  when it is a pseudo directory or a mountpoint (directory search /
+  traverse).  If ``amode`` is non-zero, also require that access on the
+  inode.  Takes the inode tree read lock unless ``INODE_CHECK_LOCKED`` is
+  set (caller already holds ``inode_lock`` / ``inode_rlock``).  Empty
+  macro returning ``0`` when ``CONFIG_FS_PERMISSION`` is disabled.
+
+``inode_checkopenperm``
+  Validate that the inode supports the requested open access, then apply
+  mode checks for non-mountpoint inodes.
+
+``fs_checkmode``
+  Core owner/group/other test used by the helpers above and by filesystems
+  such as tmpfs and littlefs.
+
+Optional mountpoint hook
+------------------------
+
+``struct mountpt_operations`` may provide a ``permission`` method for
+in-volume DAC.  The field is at the **end** of the structure so existing
+positional initialisers remain valid.
+
+* **tmpfs** implements ``tmpfs_permission``.
+* Filesystems without Unix ownership on disk (for example FAT and ROMFS)
+  leave the method ``NULL``.
+
+The VFS mount-crossing gate does **not** depend on this hook.  Entry into a
+volume is enforced with ``inode_checkpathperm`` against the mountpoint
+inode's stored ``i_mode``.  In-volume checks remain the filesystem's job
+(tmpfs and littlefs enforce DAC inside their own open/mkdir/path helpers.
+``mops->permission`` is an optional common entry point for the same policy;
+the VFS does not invoke it for mount-crossing).
+
+Open vs traverse
+================
+
+Mountpoint inodes are **not** open-mode-checked by ``inode_checkopenperm``.
+Applying the mount directory's mode bits as file open modes would require
+read/write on the mount directory merely to open a file beneath it.
+
+Traverse is separate: callers use ``inode_checkpathperm`` so parent
+directories and the mountpoint itself still require ``X_OK``.
+
+Typical order after a successful ``inode_find``:
+
+1. ``inode_checkpathperm(inode, 0, 0)`` — search permission on the path
+   prefix (and on the mountpoint when entering a volume).  Call sites that
+   already hold the tree lock pass ``INODE_CHECK_LOCKED``; mount and
+   pseudoFS create/remove may pass a non-zero ``amode`` (for example
+   ``W_OK``) to combine traverse and target checks in one call.
+2. Operation-specific checks — ``inode_checkopenperm``, or the
+   filesystem's own methods for paths inside a mount.
+
+Mount-crossing
+==============
+
+Path walk stops at a mountpoint and returns that inode plus a relative path
+into the volume.  Without traverse checks, a restrictive mode on a pseudoFS
+parent would not protect objects under a filesystem mounted beneath it.
+
+Example::
+
+  /secure_dir          # pseudoFS directory, mode 0700, owner root
+  /secure_dir/mnt      # mounted volume (tmpfs, FAT, ...)
+  /secure_dir/mnt/a    # object inside the volume
+
+``inode_checkpathperm`` requires ``X_OK`` on ``secure_dir`` and on the
+mountpoint ``mnt``.  A non-root open of ``/secure_dir/mnt/a`` therefore
+returns ``EACCES``, even if the mounted filesystem itself has no Unix DAC.
+
+Where the checks run
+--------------------
+
+* After ``inode_find`` in open, unlink, mkdir, rmdir, rename, stat, chstat,
+  statfs, readlink, mount, and umount.  ``inode_checkpathperm`` takes
+  ``inode_rlock`` unless the caller already holds the tree lock
+  (``INODE_CHECK_LOCKED``).
+* Inside ``inode_reserve`` / ``inode_remove`` for pseudoFS create and remove
+  (ancestor ``X_OK`` and parent ``W_OK`` in one call, under ``inode_lock``).
+
+Who enforces what
+-----------------
+
+* **PseudoFS parent dirs** — ``X_OK`` (and ``W_OK`` when creating/removing)
+* **Mountpoint inode** — ``X_OK`` to enter the volume (stored ``i_mode``)
+* **Inside the volume** — Filesystem methods; optional ``mops->permission``
+* **FAT / ROMFS** — No Unix ownership on disk; entry still gated by the
+  mountpoint ``i_mode``
+
+References
+==========
+
+* ``fs/inode/fs_inode.c`` — ``inode_checkperm``, ``inode_checkpathperm``
+* ``include/nuttx/fs/fs.h`` — ``struct mountpt_operations``
+* ``fs/tmpfs/fs_tmpfs.c`` — ``tmpfs_permission``
+* :ref:`user-identity` — credential model
+* :doc:`/components/filesystem/littlefs` — littlefs in-volume DAC
+* :doc:`/components/filesystem/tmpfs` — tmpfs overview
diff --git a/Documentation/implementation/index.rst b/Documentation/implementation/index.rst
index 5ea1eaa7224..4d53b205c6d 100644
--- a/Documentation/implementation/index.rst
+++ b/Documentation/implementation/index.rst
@@ -16,6 +16,7 @@ Implementation Details
    device_nodes.rst
    drivers_design.rst
    file_descriptors.rst
+   file_permission.rst
    hardfaults.rst
    interrupt_controls.rst
    ioctl.rst
diff --git a/Documentation/implementation/user_identity.rst b/Documentation/implementation/user_identity.rst
index 5c9486901bd..a831a1f2353 100644
--- a/Documentation/implementation/user_identity.rst
+++ b/Documentation/implementation/user_identity.rst
@@ -86,7 +86,9 @@ Configuration
 
 ``CONFIG_FS_PERMISSION``
   Enables filesystem ownership and permission enforcement. Requires
-  ``CONFIG_SCHED_USER_IDENTITY``.
+  ``CONFIG_SCHED_USER_IDENTITY`` and ``CONFIG_PSEUDOFS_ATTRIBUTES``.
+  See :ref:`file-permission` for the VFS helpers, mount-crossing
+  traverse rules, and testing notes.
 
 Pseudo-Filesystem Ownership
 ===========================
@@ -96,5 +98,10 @@ enabled, ``inode_alloc()`` assigns ``i_owner`` and ``i_group`` from the
 caller's effective credentials. This covers
 message queues (``mq_open()``), named semaphores (``sem_open()``), shared
 memory objects (``shm_open()``), FIFOs (``mkfifo()``), and pseudo-files
-created through the same inode reservation path. Open-time permission checks
-use ``inode_checkopenperm()`` (or ``inode_checkperm()`` for message queues).
+created through the same inode reservation path.
+
+Path resolution requires directory search permission (``X_OK``) on ancestors
+via ``inode_checkpathperm()``.  Open-time checks on the final node use
+``inode_checkopenperm()`` (or ``inode_checkperm()`` for named IPC
+objects).  Full details, including mounts under private pseudoFS parents,
+are in :ref:`file-permission`.


(nuttx) 01/02: fs: enforce path search and unify inode permission helpers

Posted by ac...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git

commit 08afc5f2cf7c37d74c65347dc1aa281942722330
Author: Abhishek Mishra <mi...@gmail.com>
AuthorDate: Sun Aug 2 17:54:38 2026 +0000

    fs: enforce path search and unify inode permission helpers
    
    Require X_OK on path ancestors and mountpoint gates via
    inode_checkpathperm(), and use inode_checkperm() for inode mode checks.
    inode_checkpathperm() takes the inode tree read lock (or INODE_CHECK_LOCKED
    when the caller already holds it) and can fold a non-zero amode check onto
    the target inode in the same call.
    
    When CONFIG_FS_PERMISSION is disabled, inode_checkperm/inode_checkpathperm
    are empty macros returning 0 so there is no stub .text cost.
    
    Skip mountpoint R/W open-mode checks in inode_checkopenperm(); keep
    traversal separate from open.  Add an optional mountpt_operations.permission
    hook (tmpfs when CONFIG_FS_PERMISSION) for in-volume DAC without making the
    VFS mount-crossing path depend on it.
    
    Signed-off-by: Abhishek Mishra <mi...@gmail.com>
---
 fs/inode/fs_inode.c        | 131 +++++++++++++++++++++++++++++++--------------
 fs/inode/fs_inoderemove.c  |   6 +--
 fs/inode/fs_inodereserve.c |   8 +--
 fs/inode/inode.h           |  39 ++++++++++----
 fs/mount/fs_mount.c        |   9 ++++
 fs/mount/fs_umount2.c      |  11 +++-
 fs/tmpfs/fs_tmpfs.c        |  41 +++++++++++++-
 fs/vfs/fs_chstat.c         |   7 +++
 fs/vfs/fs_mkdir.c          |   7 +++
 fs/vfs/fs_open.c           |  12 ++++-
 fs/vfs/fs_readlink.c       |   7 +++
 fs/vfs/fs_rename.c         |  14 ++++-
 fs/vfs/fs_rmdir.c          |   7 +++
 fs/vfs/fs_stat.c           |   7 +++
 fs/vfs/fs_statfs.c         |   7 +++
 fs/vfs/fs_unlink.c         |   6 +++
 include/nuttx/fs/fs.h      |  12 +++++
 17 files changed, 271 insertions(+), 60 deletions(-)

diff --git a/fs/inode/fs_inode.c b/fs/inode/fs_inode.c
index e89eb337489..cc9b4115356 100644
--- a/fs/inode/fs_inode.c
+++ b/fs/inode/fs_inode.c
@@ -213,61 +213,107 @@ void inode_runlock(void)
   up_read(&g_inode_lock);
 }
 
+#ifdef CONFIG_FS_PERMISSION
 /****************************************************************************
  * Name: inode_checkperm
  *
  * Description:
- *   Check 'inode' for 'amode' access on pseudo-filesystem inodes.
- *   NULL 'inode' (root) and mountpoints are exempt.
- *
- * Input Parameters:
- *   inode - Inode to check, or NULL for a root-level path
- *   amode - Access mode bitmask (R_OK / W_OK / X_OK)
- *
- * Returned Value:
- *   Zero (OK) on success, or -EACCES if permission is denied.
+ *   Check 'inode' for 'amode' access against stored owner/group/mode.
+ *   Applies to pseudoFS nodes and to mountpoint inodes (whose i_mode gates
+ *   traversal into the mounted filesystem).  Optional mountpt_operations
+ *   .permission may expose the same policy for in-volume paths; the VFS
+ *   does not call that hook for mount-crossing.
  *
  ****************************************************************************/
 
+int inode_checkperm(FAR struct inode *inode, int amode)
+{
+  if (inode == NULL)
+    {
+      return OK;
+    }
+
+  return fs_checkmode(inode->i_owner, inode->i_group, inode->i_mode, amode);
+}
+
 /****************************************************************************
- * Name: inode_checkperm
+ * Name: inode_checkpathperm
+ *
+ * Description:
+ *   Require X_OK on every ancestor of 'inode', and on 'inode' itself when
+ *   it is a directory or mountpoint that must be traversed.  If 'amode' is
+ *   non-zero, also require that access on 'inode'.  Takes the inode tree
+ *   read lock unless INODE_CHECK_LOCKED is set in 'flags'.
+ *
  ****************************************************************************/
 
-int inode_checkperm(FAR struct inode *inode, int amode)
+int inode_checkpathperm(FAR struct inode *inode, int amode, int flags)
 {
-#ifdef CONFIG_FS_PERMISSION
+  FAR struct inode *node;
+  int locked = (flags & INODE_CHECK_LOCKED) != 0;
+  int ret;
 
   if (inode == NULL)
     {
       return OK;
     }
 
-  if (INODE_IS_MOUNTPT(inode))
+  if (!locked)
     {
-      return OK;
+      inode_rlock();
     }
 
-  return fs_checkmode(inode->i_owner, inode->i_group, inode->i_mode, amode);
+  if (INODE_IS_PSEUDODIR(inode)
+#ifndef CONFIG_DISABLE_MOUNTPOINT
+      || INODE_IS_MOUNTPT(inode)
+#endif
+     )
+    {
+      ret = inode_checkperm(inode, X_OK);
+      if (ret < 0)
+        {
+          goto errout;
+        }
+    }
 
-#else
-  return OK;
-#endif /* CONFIG_FS_PERMISSION */
+  for (node = inode->i_parent; node != NULL; node = node->i_parent)
+    {
+      ret = inode_checkperm(node, X_OK);
+      if (ret < 0)
+        {
+          goto errout;
+        }
+    }
+
+  if (amode != 0)
+    {
+      ret = inode_checkperm(inode, amode);
+      if (ret < 0)
+        {
+          goto errout;
+        }
+    }
+
+  ret = OK;
+
+errout:
+  if (!locked)
+    {
+      inode_runlock();
+    }
+
+  return ret;
 }
+#endif /* CONFIG_FS_PERMISSION */
 
 /****************************************************************************
  * Name: inode_checkopenperm
  *
  * Description:
  *   Validate open access to 'inode' for 'oflags'.  Checks driver operation
- *   support, then pseudo-filesystem mode bits when enabled.  Mountpoints
- *   are exempt from mode checks.
- *
- * Input Parameters:
- *   inode  - The inode to check
- *   oflags - Open flags (O_RDONLY / O_WRONLY / O_RDWR)
- *
- * Returned Value:
- *   Zero (OK) on success, or a negated errno on failure.
+ *   support, then mode bits for non-mountpoint inodes.  Mountpoints are not
+ *   mode-checked here for R/W (that would confuse directory bits with file
+ *   open modes); callers use inode_checkpathperm() for traversal.
  *
  ****************************************************************************/
 
@@ -275,22 +321,31 @@ int inode_checkopenperm(FAR struct inode *inode, int oflags)
 {
   FAR const struct file_operations *ops;
 
-  if (INODE_IS_NAMEDSEM(inode))
+#ifndef CONFIG_DISABLE_MOUNTPOINT
+  /* Mountpoints: only verify that open exists.  Path search / DAC for the
+   * mount directory is handled by inode_checkpathperm(); per-file DAC is
+   * the filesystem's responsibility.
+   */
+
+  if (INODE_IS_MOUNTPT(inode))
     {
-#ifdef CONFIG_FS_PERMISSION
-      return inode_checkperm(inode, R_OK | W_OK);
-#else
+      if (inode->u.i_mops == NULL || inode->u.i_mops->open == NULL)
+        {
+          return -ENXIO;
+        }
+
       return OK;
+    }
 #endif
+
+  if (INODE_IS_NAMEDSEM(inode))
+    {
+      return inode_checkperm(inode, R_OK | W_OK);
     }
 
   if (INODE_IS_MQUEUE(inode) || INODE_IS_PSEUDODIR(inode))
     {
-#ifdef CONFIG_FS_PERMISSION
       return inode_checkperm(inode, fs_open_amode(oflags));
-#else
-      return OK;
-#endif
     }
 
   ops = inode->u.i_ops;
@@ -307,11 +362,5 @@ int inode_checkopenperm(FAR struct inode *inode, int oflags)
       return -EACCES;
     }
 
-#ifdef CONFIG_FS_PERMISSION
-
   return inode_checkperm(inode, fs_open_amode(oflags));
-
-#else
-  return OK;
-#endif /* CONFIG_FS_PERMISSION */
 }
diff --git a/fs/inode/fs_inoderemove.c b/fs/inode/fs_inoderemove.c
index 5c41948e896..7696bf46184 100644
--- a/fs/inode/fs_inoderemove.c
+++ b/fs/inode/fs_inoderemove.c
@@ -79,17 +79,17 @@ static FAR struct inode *inode_unlink(FAR const char *path)
       inode = desc.node;
       DEBUGASSERT(inode != NULL);
 
-#ifdef CONFIG_FS_PERMISSION
       if (desc.parent != NULL)
         {
-          ret = inode_checkperm(desc.parent, W_OK);
+          /* Caller holds the inode tree lock. */
+
+          ret = inode_checkpathperm(desc.parent, W_OK, INODE_CHECK_LOCKED);
           if (ret < 0)
             {
               inode = NULL;
               goto errout;
             }
         }
-#endif
 
       /* If peer is non-null, then remove the node from the right of
        * of that peer node.
diff --git a/fs/inode/fs_inodereserve.c b/fs/inode/fs_inodereserve.c
index f3b3e3b7f12..3082a523b97 100644
--- a/fs/inode/fs_inodereserve.c
+++ b/fs/inode/fs_inodereserve.c
@@ -226,16 +226,18 @@ int inode_reserve(FAR const char *path,
   left   = desc.peer;
   parent = desc.parent;
 
-#ifdef CONFIG_FS_PERMISSION
   if (parent != NULL)
     {
-      ret = inode_checkperm(parent, W_OK | X_OK);
+      /* Traverse ancestors (X_OK), then require write on the parent.
+       * Caller holds the inode tree lock.
+       */
+
+      ret = inode_checkpathperm(parent, W_OK, INODE_CHECK_LOCKED);
       if (ret < 0)
         {
           goto errout_with_search;
         }
     }
-#endif
 
   for (; ; )
     {
diff --git a/fs/inode/inode.h b/fs/inode/inode.h
index 74d9ef89c00..2f938ac81a3 100644
--- a/fs/inode/inode.h
+++ b/fs/inode/inode.h
@@ -423,31 +423,52 @@ void inode_addref(FAR struct inode *inode);
 
 void inode_release(FAR struct inode *inode);
 
+/* Caller already holds the inode tree lock (inode_lock/inode_rlock). */
+
+#define INODE_CHECK_LOCKED  (1 << 0)
+
 /****************************************************************************
  * Name: inode_checkperm
  *
  * Description:
- *   Check 'inode' for 'amode' access on pseudo-filesystem inodes.
- *   NULL 'inode' (root) and mountpoints are exempt.
+ *   Check 'inode' for 'amode' access against stored owner/group/mode
+ *   (pseudoFS nodes and mountpoint inodes used as traverse gates).
+ *   Empty macros when CONFIG_FS_PERMISSION is disabled (zero cost).
  *
- * Input Parameters:
- *   inode - Inode to check, or NULL for a root-level path
- *   amode - Access mode bitmask (R_OK / W_OK / X_OK)
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: inode_checkpathperm
  *
- * Returned Value:
- *   Zero (OK) on success, or -EACCES if permission is denied.
+ * Description:
+ *   Enforce directory search (X_OK) on the path leading to 'inode', and
+ *   optionally check 'amode' on 'inode' itself.  Requires X_OK on every
+ *   ancestor, and on 'inode' when it is a pseudo directory or mountpoint.
+ *   If 'amode' is non-zero, also requires that access on 'inode'.
+ *
+ *   Takes the inode tree read lock unless INODE_CHECK_LOCKED is set in
+ *   'flags' (caller already holds inode_lock/inode_rlock).
+ *   Empty macros when CONFIG_FS_PERMISSION is disabled (zero cost).
  *
  ****************************************************************************/
 
+#ifdef CONFIG_FS_PERMISSION
 int inode_checkperm(FAR struct inode *inode, int amode);
+int inode_checkpathperm(FAR struct inode *inode, int amode, int flags);
+#else
+#  define inode_checkperm(inode, amode)             0
+#  define inode_checkpathperm(inode, amode, flags)  0
+#  define fs_open_amode(oflags)                     0
+#endif
 
 /****************************************************************************
  * Name: inode_checkopenperm
  *
  * Description:
  *   Validate open access to 'inode' for 'oflags'.  Checks driver operation
- *   support, then pseudo-filesystem mode bits when enabled.  Mountpoints
- *   are exempt from mode checks.
+ *   support, then mode bits for non-mountpoint inodes.  Mountpoints skip
+ *   open-mode checks here; callers must use inode_checkpathperm() so
+ *   parent / mountgates still require X_OK to traverse.
  *
  * Input Parameters:
  *   inode  - The inode to check
diff --git a/fs/mount/fs_mount.c b/fs/mount/fs_mount.c
index 0f4758c43a9..d46d6ee8c20 100644
--- a/fs/mount/fs_mount.c
+++ b/fs/mount/fs_mount.c
@@ -403,6 +403,15 @@ int nx_mount(FAR const char *source, FAR const char *target,
           inode_release(mountpt_inode);
           goto errout_with_lock;
         }
+
+      /* Require search on ancestors and write on the mount target. */
+
+      ret = inode_checkpathperm(mountpt_inode, W_OK, INODE_CHECK_LOCKED);
+      if (ret < 0)
+        {
+          inode_release(mountpt_inode);
+          goto errout_with_lock;
+        }
     }
 #endif
 
diff --git a/fs/mount/fs_umount2.c b/fs/mount/fs_umount2.c
index b030a316e0f..0de3dfb16d8 100644
--- a/fs/mount/fs_umount2.c
+++ b/fs/mount/fs_umount2.c
@@ -110,9 +110,18 @@ int nx_umount2(FAR const char *target, unsigned int flags)
    * performed, or a negated error code on a failure.
    */
 
-  /* Hold the semaphore through the unbind logic */
+  /* Hold the inode tree lock across permission checks and unbind,
+   * matching mount()'s check-under-lock pattern.
+   */
 
   inode_lock();
+
+  ret = inode_checkpathperm(mountpt_inode, W_OK, INODE_CHECK_LOCKED);
+  if (ret < 0)
+    {
+      goto errout_with_lock;
+    }
+
   ret = mountpt_inode->u.i_mops->unbind(mountpt_inode->i_private,
                                        &blkdrvr_inode, flags);
   if (ret < 0)
diff --git a/fs/tmpfs/fs_tmpfs.c b/fs/tmpfs/fs_tmpfs.c
index ed25f2a50f0..28c76b17d47 100644
--- a/fs/tmpfs/fs_tmpfs.c
+++ b/fs/tmpfs/fs_tmpfs.c
@@ -180,6 +180,10 @@ static int  tmpfs_stat(FAR struct inode *mountpt, FAR const char *relpath,
               FAR struct stat *buf);
 static int  tmpfs_chstat(FAR struct inode *mountpt, FAR const char *relpath,
               FAR const struct stat *buf, int flags);
+#ifdef CONFIG_FS_PERMISSION
+static int  tmpfs_permission(FAR struct inode *mountpt,
+              FAR const char *relpath, int amode);
+#endif
 
 /****************************************************************************
  * Public Data
@@ -218,7 +222,14 @@ const struct mountpt_operations g_tmpfs_operations =
   tmpfs_rmdir,      /* rmdir */
   tmpfs_rename,     /* rename */
   tmpfs_stat,       /* stat */
-  tmpfs_chstat      /* chstat */
+  tmpfs_chstat,     /* chstat */
+  NULL,             /* syncfs */
+  NULL,             /* ioctldir */
+#ifdef CONFIG_FS_PERMISSION
+  tmpfs_permission  /* permission */
+#else
+  NULL              /* permission */
+#endif
 };
 
 /****************************************************************************
@@ -3132,6 +3143,34 @@ errout_with_fslock:
 #endif
 }
 
+/****************************************************************************
+ * Name: tmpfs_permission
+ ****************************************************************************/
+
+#ifdef CONFIG_FS_PERMISSION
+static int tmpfs_permission(FAR struct inode *mountpt,
+                            FAR const char *relpath, int amode)
+{
+  FAR struct tmpfs_s *fs;
+  int ret;
+
+  DEBUGASSERT(mountpt != NULL && relpath != NULL);
+
+  fs = mountpt->i_private;
+  DEBUGASSERT(fs != NULL);
+
+  ret = tmpfs_lock(fs);
+  if (ret < 0)
+    {
+      return ret;
+    }
+
+  ret = tmpfs_check_pathperm(fs, relpath, strlen(relpath), amode);
+  tmpfs_unlock(fs);
+  return ret;
+}
+#endif
+
 /****************************************************************************
  * Public Functions
  ****************************************************************************/
diff --git a/fs/vfs/fs_chstat.c b/fs/vfs/fs_chstat.c
index 1d1828992f5..16b61b257e9 100644
--- a/fs/vfs/fs_chstat.c
+++ b/fs/vfs/fs_chstat.c
@@ -71,6 +71,13 @@ static int chstat_recursive(FAR const char *path,
   inode = desc.node;
   DEBUGASSERT(inode != NULL);
 
+  ret = inode_checkpathperm(inode, 0, 0);
+  if (ret < 0)
+    {
+      inode_release(inode);
+      goto errout_with_search;
+    }
+
   /* The way we handle the chstat depends on the type of inode that we
    * are dealing with.
    */
diff --git a/fs/vfs/fs_mkdir.c b/fs/vfs/fs_mkdir.c
index 253be08f27a..6efc790ed11 100644
--- a/fs/vfs/fs_mkdir.c
+++ b/fs/vfs/fs_mkdir.c
@@ -100,6 +100,13 @@ int mkdir(const char *pathname, mode_t mode)
           goto errout_with_inode;
         }
 
+      ret = inode_checkpathperm(inode, 0, 0);
+      if (ret < 0)
+        {
+          errcode = -ret;
+          goto errout_with_inode;
+        }
+
       /* Perform the mkdir operation using the relative path
        * at the mountpoint.
        */
diff --git a/fs/vfs/fs_open.c b/fs/vfs/fs_open.c
index 0959527defa..bc2024b8a23 100644
--- a/fs/vfs/fs_open.c
+++ b/fs/vfs/fs_open.c
@@ -170,7 +170,17 @@ static int file_vopen(FAR struct file *filep, FAR const char *path,
     }
 #endif
 
-  /* Validate operation support and pseudo-filesystem permissions */
+  /* Enforce directory search (X_OK) on ancestors / mount gates, then
+   * validate open modes.  inode_checkpathperm() takes the tree read lock
+   * for the path walk; for non-mountpoints, hold it again around openperm
+   * so i_mode cannot race with concurrent chmod.
+   */
+
+  ret = inode_checkpathperm(inode, 0, 0);
+  if (ret < 0)
+    {
+      goto errout_with_inode;
+    }
 
 #ifndef CONFIG_DISABLE_MOUNTPOINT
   if (INODE_IS_MOUNTPT(inode))
diff --git a/fs/vfs/fs_readlink.c b/fs/vfs/fs_readlink.c
index 6cdf067d434..3a057ad895d 100644
--- a/fs/vfs/fs_readlink.c
+++ b/fs/vfs/fs_readlink.c
@@ -94,6 +94,13 @@ ssize_t readlink(FAR const char *path, FAR char *buf, size_t bufsize)
   node = desc.node;
   DEBUGASSERT(node != NULL);
 
+  ret = inode_checkpathperm(node, 0, 0);
+  if (ret < 0)
+    {
+      errcode = -ret;
+      goto errout_with_inode;
+    }
+
   /* An inode was found that includes this path and possibly refers to a
    * symbolic link.
    *
diff --git a/fs/vfs/fs_rename.c b/fs/vfs/fs_rename.c
index 99c66279e77..28afc700089 100644
--- a/fs/vfs/fs_rename.c
+++ b/fs/vfs/fs_rename.c
@@ -88,6 +88,11 @@ static int pseudorename(FAR const char *oldpath, FAR struct inode *oldinode,
 
   SETUP_SEARCH(&newdesc, newpath, true);
 
+  /* Ancestor X_OK was already checked by rename() via
+   * inode_checkpathperm(oldinode, ...).  Still require parent W_OK here
+   * under the tree lock before mutating.
+   */
+
   ret = inode_checkperm(oldparent, W_OK);
   if (ret < 0)
     {
@@ -305,7 +310,7 @@ static int mountptrename(FAR const char *oldpath, FAR struct inode *oldinode,
     }
 
   /* Get an inode for the new relpath -- it should lie on the same
-   * mountpoint
+   * mountpoint.  Path search on oldinode was already enforced by rename().
    */
 
   SETUP_SEARCH(&newdesc, newpath, true);
@@ -516,6 +521,13 @@ int rename(FAR const char *oldpath, FAR const char *newpath)
   oldinode = olddesc.node;
   DEBUGASSERT(oldinode != NULL);
 
+  ret = inode_checkpathperm(oldinode, 0, 0);
+  if (ret < 0)
+    {
+      inode_release(oldinode);
+      goto errout_with_oldsearch;
+    }
+
 #ifndef CONFIG_DISABLE_MOUNTPOINT
   /* Verify that the old inode is a valid mountpoint. */
 
diff --git a/fs/vfs/fs_rmdir.c b/fs/vfs/fs_rmdir.c
index f2b53284ddb..f2911085d53 100644
--- a/fs/vfs/fs_rmdir.c
+++ b/fs/vfs/fs_rmdir.c
@@ -91,6 +91,13 @@ int rmdir(FAR const char *pathname)
 
   if (INODE_IS_MOUNTPT(inode) && inode->u.i_mops)
     {
+      ret = inode_checkpathperm(inode, 0, 0);
+      if (ret < 0)
+        {
+          errcode = -ret;
+          goto errout_with_inode;
+        }
+
       /* Perform the rmdir operation using the relative path
        * from the mountpoint.
        */
diff --git a/fs/vfs/fs_stat.c b/fs/vfs/fs_stat.c
index 7e4a29ac79c..27f1b040f08 100644
--- a/fs/vfs/fs_stat.c
+++ b/fs/vfs/fs_stat.c
@@ -105,6 +105,13 @@ static int stat_recursive(FAR const char *path,
   inode = desc.node;
   DEBUGASSERT(inode != NULL);
 
+  ret = inode_checkpathperm(inode, 0, 0);
+  if (ret < 0)
+    {
+      inode_release(inode);
+      goto errout_with_search;
+    }
+
   /* The way we handle the stat depends on the type of inode that we
    * are dealing with.
    */
diff --git a/fs/vfs/fs_statfs.c b/fs/vfs/fs_statfs.c
index 842780d35ad..70d796c3b5f 100644
--- a/fs/vfs/fs_statfs.c
+++ b/fs/vfs/fs_statfs.c
@@ -110,6 +110,13 @@ int statfs(FAR const char *path, FAR struct statfs *buf)
   inode = desc.node;
   DEBUGASSERT(inode != NULL);
 
+  ret = inode_checkpathperm(inode, 0, 0);
+  if (ret < 0)
+    {
+      inode_release(inode);
+      goto errout_with_search;
+    }
+
   /* The way we handle the statfs depends on the type of inode that we
    * are dealing with.
    */
diff --git a/fs/vfs/fs_unlink.c b/fs/vfs/fs_unlink.c
index 8726610a260..e92a9ec99ec 100644
--- a/fs/vfs/fs_unlink.c
+++ b/fs/vfs/fs_unlink.c
@@ -88,6 +88,12 @@ int nx_unlink(FAR const char *pathname)
 
   if (INODE_IS_MOUNTPT(inode) && inode->u.i_mops)
     {
+      ret = inode_checkpathperm(inode, 0, 0);
+      if (ret < 0)
+        {
+          goto errout_with_inode;
+        }
+
       /* Perform the unlink operation using the relative path at the
        * mountpoint.
        */
diff --git a/include/nuttx/fs/fs.h b/include/nuttx/fs/fs.h
index 818f734e5a8..a13e56b04ab 100644
--- a/include/nuttx/fs/fs.h
+++ b/include/nuttx/fs/fs.h
@@ -387,6 +387,18 @@ struct mountpt_operations
   CODE int     (*ioctldir)(FAR struct inode *mountpt,
                            FAR struct fs_dirent_s *dir,
                            int cmd, unsigned long arg);
+
+  /* Optional DAC check for a path relative to this mountpoint.
+   * Filesystems may implement this for a common in-volume permission
+   * entry point.  The VFS mount-crossing gate does not call it; entry
+   * into a volume uses inode_checkpathperm() on the mountpoint inode.
+   * Filesystems without Unix permissions leave it NULL.
+   *
+   * Placed at the end so existing positional initialisers stay valid.
+   */
+
+  CODE int     (*permission)(FAR struct inode *mountpt,
+                             FAR const char *relpath, int amode);
 };
 #endif /* CONFIG_DISABLE_MOUNTPOINT */