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

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

commit 67cd42677b4e91f0acdd36a36a5ecbe036554561
Author: yukangzhi <[email protected]>
AuthorDate: Fri Jul 3 11:12:22 2026 +0800

    fs/inode: canonicalize path before inode search to fix .. resolution
    
    Add _inode_canonicalize() to remove '.' and '..' segments from the
    absolute path before the inode tree traversal begins. This fixes the
    case where paths containing '..' that resolve back to a mountpoint
    root (e.g., /tmp/subdir/..) were not being handed to the filesystem.
    
    Previously, _compute_path_depth() returned 0 for such paths, causing
    the VFS to skip the mountpoint and attempt to find 'subdir' in the
    pseudo filesystem -- which fails with ENOTDIR.
    
    With canonicalization, /tmp/subdir/.. becomes /tmp before the search,
    so the mountpoint is correctly matched. This fixes chdir('..'),
    stat('../..'), opendir('../..'), and similar operations from within
    mountpoint subdirectories.
    
    The implementation uses an in-place two-pointer algorithm with no
    additional stack allocation, safe for NuttX's small kernel stacks.
    
    Signed-off-by: yukangzhi <[email protected]>
---
 fs/inode/fs_inodesearch.c | 129 ++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 126 insertions(+), 3 deletions(-)

diff --git a/fs/inode/fs_inodesearch.c b/fs/inode/fs_inodesearch.c
index 809714ffef5..e769f5bde16 100644
--- a/fs/inode/fs_inodesearch.c
+++ b/fs/inode/fs_inodesearch.c
@@ -48,6 +48,7 @@ static int _inode_linktarget(FAR struct inode *inode,
 #endif
 static int _inode_search(FAR struct inode_search_s *desc);
 static FAR const char *_inode_getcwd(void);
+static int _inode_canonicalize(FAR char *path);
 
 /****************************************************************************
  * Public Data
@@ -240,6 +241,103 @@ static int _compute_path_depth(FAR const char *path)
   return depth;
 }
 
+/****************************************************************************
+ * Name: _inode_canonicalize
+ *
+ * Description:
+ *   Remove "." and ".." segments from an absolute path in-place.
+ *   The path MUST start with '/'.  Returns -EINVAL if ".." attempts
+ *   to ascend beyond the root directory, or -ENAMETOOLONG if the
+ *   canonicalized result is >= PATH_MAX bytes.
+ *
+ ****************************************************************************/
+
+static int _inode_canonicalize(FAR char *path)
+{
+  /* Skip the initial '/' -- caller guarantees absolute path */
+
+  FAR char *src = path + 1;
+  FAR char *dst = path + 1;
+
+  while (*src != '\0')
+    {
+      /* Skip duplicate slashes */
+
+      if (*src == '/')
+        {
+          src++;
+          continue;
+        }
+
+      /* Check for "." (current directory) */
+
+      if (src[0] == '.' && (src[1] == '/' || src[1] == '\0'))
+        {
+          src += (src[1] == '/') ? 2 : 1;
+          continue;
+        }
+
+      /* Check for ".." (parent directory) */
+
+      if (src[0] == '.' && src[1] == '.' &&
+          (src[2] == '/' || src[2] == '\0'))
+        {
+          /* Cannot go above root */
+
+          if (dst <= path + 1)
+            {
+              return -EINVAL;
+            }
+
+          /* Remove trailing slash first */
+
+          dst--;
+
+          /* Scan backward to find the previous '/' */
+
+          while (dst > path + 1 && *(dst - 1) != '/')
+            {
+              dst--;
+            }
+
+          src += (src[2] == '/') ? 3 : 2;
+          continue;
+        }
+
+      /* Regular path component: copy until end of segment (including '/') */
+
+      do
+        {
+          if (dst != src)
+            {
+              *dst = *src;
+            }
+
+          dst++;
+          src++;
+        }
+      while (*src != '\0' && *(src - 1) != '/');
+    }
+
+  /* Remove trailing slash (unless root "/") */
+
+  if (dst > path + 1 && *(dst - 1) == '/')
+    {
+      dst--;
+    }
+
+  *dst = '\0';
+
+  /* After canonicalization, check if the resolved path exceeds PATH_MAX */
+
+  if ((dst - path) >= PATH_MAX)
+    {
+      return -ENAMETOOLONG;
+    }
+
+  return 0;
+}
+
 /****************************************************************************
  * Name: _inode_checkpath
  ****************************************************************************/
@@ -313,9 +411,9 @@ static int _inode_search(FAR struct inode_search_s *desc)
       return ret;
     }
 
-  /* Convert the relative path to the absolute path */
+  /* Ensure we have a writable buffer for path manipulation */
 
-  if (*desc->path != '/')
+  if (desc->buffer == NULL)
     {
       desc->buffer = lib_get_tempbuffer(PATH_MAX);
       if (desc->buffer == NULL)
@@ -323,10 +421,35 @@ static int _inode_search(FAR struct inode_search_s *desc)
           return -ENOMEM;
         }
 
-      snprintf(desc->buffer, PATH_MAX, "%s/%s", _inode_getcwd(), desc->path);
+      /* Convert relative path to absolute, or just copy absolute path.
+       * Use desc->path directly (points to caller's string) as source
+       * to avoid overlap with desc->buffer.
+       */
+
+      if (*desc->path != '/')
+        {
+          snprintf(desc->buffer, PATH_MAX, "%s/%s",
+                   _inode_getcwd(), desc->path);
+        }
+      else
+        {
+          strlcpy(desc->buffer, desc->path, PATH_MAX);
+        }
+
       desc->path = desc->buffer;
     }
 
+  /* Canonicalize the path to remove "." and ".." segments.  This ensures
+   * that mountpoint relpath never contains ".." which most filesystems
+   * (tmpfs, romfs, etc.) cannot resolve.
+   */
+
+  ret = _inode_canonicalize(desc->buffer);
+  if (ret < 0)
+    {
+      return ret;
+    }
+
   name = desc->path;
   ret = -ENOENT;
 

Reply via email to