For some reason, lib/canonicalize.c counted the number of links it saw
but never used the value beyond ensuring it never went over 20. This
causes the infinite loop mentioned in an Ubuntu bug report [1]:
ln -s foo/bar foo
realpath foo
It will take quite a bit of time (as in more than an hour), but if you
leave it running, your system will eventually run out of memory due to
the allocations used to combine the path components below the modified
section.
This patch fixes it, by using the condition from
lib/canonicalize-lgpl.c.
Collin
[1] https://bugs.launchpad.net/ubuntu/+source/coreutils/+bug/2161155
>From 694224a9928becbf3ee16f7a96e75e401d73ffd8 Mon Sep 17 00:00:00 2001
Message-ID: <694224a9928becbf3ee16f7a96e75e401d73ffd8.1790323316.git.collin.fu...@gmail.com>
From: Collin Funk <[email protected]>
Date: Fri, 25 Sep 2026 00:51:05 -0700
Subject: [PATCH] canonicalize: Fix a missing check for symlink loops.
Reported by Brian Foster in:
<https://bugs.launchpad.net/ubuntu/+source/coreutils/+bug/2161155>
* lib/canonicalize.c: Include min-eloop-threshold.h.
(canonicalize_filename_mode_stk): Set errno to ELOOP and exit if we see
MIN_ELOOP_THRESHOLD links.
* modules/canonicalize (Depends-on): Add eloop-threshold.
* tests/test-canonicalize.c (main): Add a test case that would
previously loop until the system ran out of memory. Clean up the created
sylink.
---
ChangeLog | 13 +++++++++++++
lib/canonicalize.c | 9 ++++++---
modules/canonicalize | 1 +
tests/test-canonicalize.c | 15 +++++++++++++++
4 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/ChangeLog b/ChangeLog
index a440b6d3f6..b1f347efe4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,16 @@
+2026-09-25 Collin Funk <[email protected]>
+
+ canonicalize: Fix a missing check for symlink loops.
+ Reported by Brian Foster in:
+ <https://bugs.launchpad.net/ubuntu/+source/coreutils/+bug/2161155>
+ * lib/canonicalize.c: Include min-eloop-threshold.h.
+ (canonicalize_filename_mode_stk): Set errno to ELOOP and exit if we see
+ MIN_ELOOP_THRESHOLD links.
+ * modules/canonicalize (Depends-on): Add eloop-threshold.
+ * tests/test-canonicalize.c (main): Add a test case that would
+ previously loop until the system ran out of memory. Clean up the created
+ sylink.
+
2026-09-23 Paul Eggert <[email protected]>
tests: prefer GCC sanitization macros
diff --git a/lib/canonicalize.c b/lib/canonicalize.c
index 7da95d920c..b86011284a 100644
--- a/lib/canonicalize.c
+++ b/lib/canonicalize.c
@@ -32,6 +32,7 @@
#include "attribute.h"
#include "file-set.h"
#include "hashcode-file.h"
+#include "min-eloop-threshold.h"
#include "xalloc.h"
#ifndef DOUBLE_SLASH_IS_DISTINCT_ROOT
@@ -337,9 +338,11 @@ canonicalize_filename_mode_stk (const char *name, canonicalize_mode_t can_mode,
if (0 <= n)
{
/* A physical traversal and RNAME is a symbolic link. */
-
- if (num_links < 20)
- num_links++;
+ if (++num_links > MIN_ELOOP_THRESHOLD)
+ {
+ errno = ELOOP;
+ goto error;
+ }
else if (*start)
{
/* Enough symlinks have been seen that it is time to
diff --git a/modules/canonicalize b/modules/canonicalize
index 99a5b7edd9..bcfac2ff33 100644
--- a/modules/canonicalize
+++ b/modules/canonicalize
@@ -11,6 +11,7 @@ Depends-on:
attribute
bool
double-slash-root
+eloop-threshold
errno-h
extensions
fcntl-h
diff --git a/tests/test-canonicalize.c b/tests/test-canonicalize.c
index af82ff6c0a..65d2669258 100644
--- a/tests/test-canonicalize.c
+++ b/tests/test-canonicalize.c
@@ -314,6 +314,20 @@ main (void)
ASSERT (errno == ELOOP);
}
+ /* Test that this loop is detected. Previously, it would run
+ until the system ran out of memory. */
+ ASSERT (symlink ("loop/a", BASE "/loop") == 0);
+ {
+ errno = 0;
+ char *result1 = canonicalize_filename_mode (BASE "/loop", CAN_ALL_BUT_LAST);
+ ASSERT (result1 == NULL);
+ ASSERT (errno == ELOOP);
+ errno = 0;
+ char *result2 = canonicalize_filename_mode (BASE "/loop", CAN_MISSING);
+ ASSERT (result2 == NULL);
+ ASSERT (errno == ELOOP);
+ }
+
/* Check that alternate modes can resolve missing basenames. */
{
char *result1 = canonicalize_filename_mode (BASE "/zzz", CAN_ALL_BUT_LAST);
@@ -441,6 +455,7 @@ main (void)
ASSERT (remove (BASE "/ket") == 0);
ASSERT (remove (BASE "/lum") == 0);
ASSERT (remove (BASE "/tra") == 0);
+ ASSERT (remove (BASE "/loop") == 0);
ASSERT (remove (BASE) == 0);
ASSERT (remove ("ise") == 0);
--
2.55.0