From 3253cb8e80a334c79989412d6e71501d22e13acd Mon Sep 17 00:00:00 2001 From: "Robert Dickenson (Inspirati)" Date: Wed, 3 Jun 2026 18:43:56 +1000 Subject: [PATCH 1/4] file-manager: fix double-free and relative path resolution for gitdir pointers When a .git file contains a relative gitdir: path (as used by submodules and linked worktrees), get_git_branch() was calling g_free() on its own const char* parameter, then the caller would free the same pointer again. This double-free causes heap corruption that manifests as a crash when enumerating a parent directory containing such a submodule. Fix by introducing a separate resolved_gitdir variable to hold the heap-allocated resolved path, leaving the parameter untouched. Use realpath() to canonicalise ../ components in relative paths, and guard against a NULL return when the target path does not exist on disk. Co-Authored-By: Claude Sonnet 4.6 --- src/file-manager/fm-icon-container.c | 37 ++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/file-manager/fm-icon-container.c b/src/file-manager/fm-icon-container.c index 8054c2da9..ad02e2bae 100644 --- a/src/file-manager/fm-icon-container.c +++ b/src/file-manager/fm-icon-container.c @@ -22,6 +22,7 @@ Author: Michael Meeks */ #include +#include #include #include @@ -301,13 +302,15 @@ fm_icon_container_get_icon_text_attribute_names (CajaIconContainer *container, **/ static char * get_git_branch (const char *git_path) { + gchar *resolved_gitdir = NULL; gchar *head_content = NULL; gchar *branch_name = NULL; gchar *head_path = NULL; + const gchar *effective_gitdir; if (g_file_test (git_path, G_FILE_TEST_IS_REGULAR)) { - /* It's a file, thus we are probably dealing with a submodule, so read it to resolve the real git dir */ + /* It's a file — submodule or worktree. Read it to resolve the real git dir. */ gchar *contents = NULL; if (g_file_get_contents (git_path, &contents, NULL, NULL) && contents) { @@ -316,19 +319,38 @@ get_git_branch (const char *git_path) { { size_t git_dir_prefix_len = strlen (git_dir_prefix); gchar *relative = g_strstrip (contents + git_dir_prefix_len); - gchar *base = g_path_get_dirname (git_path); - g_free (git_path); - git_path = g_build_filename (base, relative, NULL); - g_free (base); + if (!g_path_is_absolute (relative)) + { + gchar *base = g_path_get_dirname (git_path); + gchar *joined = g_build_filename (base, relative, NULL); + g_free (base); + /* realpath resolves ../ components; returns NULL if path does not exist */ + char *rp = realpath (joined, NULL); + g_free (joined); + if (rp != NULL) + { + resolved_gitdir = g_strdup (rp); + free (rp); + } + } + else + { + resolved_gitdir = g_strdup (relative); + } } g_free (contents); } + effective_gitdir = resolved_gitdir; /* may be NULL if path absent on disk */ + } + else + { + effective_gitdir = git_path; } /* Extract the current git branch name of the repository from file HEAD within .git directory */ - if (g_file_test (git_path, G_FILE_TEST_IS_DIR)) + if (effective_gitdir != NULL && g_file_test (effective_gitdir, G_FILE_TEST_IS_DIR)) { - head_path = g_build_filename (git_path, "HEAD", NULL); + head_path = g_build_filename (effective_gitdir, "HEAD", NULL); if (g_file_get_contents (head_path, &head_content, NULL, NULL)) { g_strstrip (head_content); @@ -350,6 +372,7 @@ get_git_branch (const char *git_path) { } } + g_free (resolved_gitdir); g_free (head_content); g_free (head_path); From 0158eaf2ca942b645def391bf9d2075998bf01c0 Mon Sep 17 00:00:00 2001 From: "Robert Dickenson (Inspirati)" Date: Wed, 3 Jun 2026 19:24:20 +1000 Subject: [PATCH 2/4] file-manager: classify git worktrees and submodules with distinct icon labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend get_git_branch() to inspect the resolved gitdir path and classify each .git entry as a standard repository, a linked worktree, or a submodule. Append a Unicode suffix to the branch label for non-standard cases so users can distinguish directory types at a glance in icon view: Standard repo → [branch] Linked worktree → [branch ⎇] Submodule → [branch ⊂] Classification is performed by searching the resolved gitdir path for the well-known path components .git/worktrees/ and .git/modules/, which git always uses for these object types. A file-scoped GitdirType enum encodes the three cases. The suffix is appended to both normal branch names and detached HEAD labels. No changes are required outside fm-icon-container.c. Co-Authored-By: Claude Sonnet 4.6 --- src/file-manager/fm-icon-container.c | 29 ++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/file-manager/fm-icon-container.c b/src/file-manager/fm-icon-container.c index ad02e2bae..c92207a55 100644 --- a/src/file-manager/fm-icon-container.c +++ b/src/file-manager/fm-icon-container.c @@ -297,6 +297,12 @@ fm_icon_container_get_icon_text_attribute_names (CajaIconContainer *container, return attributes; } +typedef enum { + GITDIR_STANDARD, + GITDIR_WORKTREE, + GITDIR_SUBMODULE, +} GitdirType; + /** * Parse a .git directory/file to determine repository branch name **/ @@ -347,9 +353,20 @@ get_git_branch (const char *git_path) { effective_gitdir = git_path; } - /* Extract the current git branch name of the repository from file HEAD within .git directory */ + /* Classify the git context, then extract branch name from HEAD */ if (effective_gitdir != NULL && g_file_test (effective_gitdir, G_FILE_TEST_IS_DIR)) { + GitdirType gitdir_type; + if (strstr (effective_gitdir, ".git/worktrees/") != NULL) + gitdir_type = GITDIR_WORKTREE; + else if (strstr (effective_gitdir, ".git/modules/") != NULL) + gitdir_type = GITDIR_SUBMODULE; + else + gitdir_type = GITDIR_STANDARD; + + const char *suffix = (gitdir_type == GITDIR_WORKTREE) ? " ⎇" : + (gitdir_type == GITDIR_SUBMODULE) ? " ⊂" : ""; + head_path = g_build_filename (effective_gitdir, "HEAD", NULL); if (g_file_get_contents (head_path, &head_content, NULL, NULL)) { @@ -359,15 +376,19 @@ get_git_branch (const char *git_path) { gchar **parts = g_strsplit (head_content, "/", -1); if (parts != NULL) { - branch_name = g_strdup (parts[g_strv_length(parts) - 1]); - g_strchomp (branch_name); + gchar *raw = g_strdup (parts[g_strv_length(parts) - 1]); + g_strchomp (raw); + branch_name = g_strdup_printf ("%s%s", raw, suffix); + g_free (raw); g_strfreev (parts); } } else { /* Repository is in a detached HEAD state */ - branch_name = g_strdup_printf (_("detached: %.7s"), head_content); + gchar *base_name = g_strdup_printf (_("detached: %.7s"), head_content); + branch_name = g_strdup_printf ("%s%s", base_name, suffix); + g_free (base_name); } } } From 15ea4ee41243d20b2dbc255855631a99c5691bc9 Mon Sep 17 00:00:00 2001 From: "Robert Dickenson (Inspirati)" Date: Thu, 4 Jun 2026 15:35:20 +1000 Subject: [PATCH 3/4] Implemented feedback from review comments at: https://github.com/mate-desktop/caja/pull/1893/changes/BASE..0158eaf2ca942b645def391bf9d2075998bf01c0#r3348344987 And: https://github.com/mate-desktop/caja/pull/1893/changes/BASE..0158eaf2ca942b645def391bf9d2075998bf01c0#r3349564766 --- src/file-manager/fm-icon-container.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/file-manager/fm-icon-container.c b/src/file-manager/fm-icon-container.c index c92207a55..9b46554c0 100644 --- a/src/file-manager/fm-icon-container.c +++ b/src/file-manager/fm-icon-container.c @@ -316,7 +316,7 @@ get_git_branch (const char *git_path) { if (g_file_test (git_path, G_FILE_TEST_IS_REGULAR)) { - /* It's a file — submodule or worktree. Read it to resolve the real git dir. */ + /* It's a file, thus we are probably dealing with a submodule, so read it to resolve the real git dir */ gchar *contents = NULL; if (g_file_get_contents (git_path, &contents, NULL, NULL) && contents) { @@ -357,9 +357,9 @@ get_git_branch (const char *git_path) { if (effective_gitdir != NULL && g_file_test (effective_gitdir, G_FILE_TEST_IS_DIR)) { GitdirType gitdir_type; - if (strstr (effective_gitdir, ".git/worktrees/") != NULL) + if (strstr (effective_gitdir, "/.git/worktrees/") != NULL) gitdir_type = GITDIR_WORKTREE; - else if (strstr (effective_gitdir, ".git/modules/") != NULL) + else if (strstr (effective_gitdir, "/.git/modules/") != NULL) gitdir_type = GITDIR_SUBMODULE; else gitdir_type = GITDIR_STANDARD; From d5f20b0987c0ff964975f9e5132c666b0010636e Mon Sep 17 00:00:00 2001 From: "Robert Dickenson (Inspirati)" Date: Sat, 6 Jun 2026 18:32:17 +1000 Subject: [PATCH 4/4] Modified in accordance with review feedback at https://github.com/mate-desktop/caja/pull/1893/changes/BASE..0158eaf2ca942b645def391bf9d2075998bf01c0#r3348375888 --- src/file-manager/fm-icon-container.c | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/file-manager/fm-icon-container.c b/src/file-manager/fm-icon-container.c index 9b46554c0..441e5d245 100644 --- a/src/file-manager/fm-icon-container.c +++ b/src/file-manager/fm-icon-container.c @@ -22,7 +22,6 @@ Author: Michael Meeks */ #include -#include #include #include @@ -327,17 +326,9 @@ get_git_branch (const char *git_path) { gchar *relative = g_strstrip (contents + git_dir_prefix_len); if (!g_path_is_absolute (relative)) { - gchar *base = g_path_get_dirname (git_path); - gchar *joined = g_build_filename (base, relative, NULL); + gchar *base = g_path_get_dirname (git_path); + resolved_gitdir = g_canonicalize_filename (relative, base); g_free (base); - /* realpath resolves ../ components; returns NULL if path does not exist */ - char *rp = realpath (joined, NULL); - g_free (joined); - if (rp != NULL) - { - resolved_gitdir = g_strdup (rp); - free (rp); - } } else {