Commit e739a383d for imagemagick.org

commit e739a383df7c14bef90b32ae333b4c827ae92da8
Author: Cristy <urban-warrior@imagemagick.org>
Date:   Wed Feb 18 11:48:05 2026 -0500

    protect against deep glob recursion

diff --git a/MagickCore/token.c b/MagickCore/token.c
index 84259aea2..4f884b129 100644
--- a/MagickCore/token.c
+++ b/MagickCore/token.c
@@ -351,208 +351,302 @@ MagickExport magick_hot_spot size_t GetNextToken(
 %      an expression.
 %
 */
-MagickExport MagickBooleanType GlobExpression(
-  const char *magick_restrict expression,const char *magick_restrict pattern,
-  const MagickBooleanType case_insensitive)
-{
-  char
-    path[MagickPathExtent];
-
-  MagickBooleanType
-    done,
-    match;

+static MagickBooleanType GlobExpression_(const char *magick_restrict expression,
+  const char *magick_restrict pattern,const MagickBooleanType case_insensitive,
+  const size_t depth)
+{
+  if (depth > MagickMaxRecursionDepth)
+    {
+      errno=EOVERFLOW;
+      return(MagickFalse);
+    }
   /*
-    Return on empty pattern or '*'.
+    Empty pattern or single '*' always matches.
   */
-  if (pattern == (char *) NULL)
+  if (pattern == (const char *) NULL)
     return(MagickTrue);
   if (GetUTFCode(pattern) == 0)
     return(MagickTrue);
-  if (LocaleCompare(pattern,"*") == 0)
+  if ((GetUTFCode(pattern) == '*') &&
+      (GetUTFCode(pattern+GetUTFOctets(pattern)) == 0))
     return(MagickTrue);
-  GetPathComponent(pattern,SubimagePath,path);
-  if (*path != '\0')
-    return(MagickFalse);
-  /*
-    Evaluate glob expression.
-  */
-  done=MagickFalse;
-  while ((GetUTFCode(pattern) != 0) && (done == MagickFalse))
+  if ((strchr(pattern,'{') == NULL) &&
+      (strchr(pattern,'*') == NULL) &&
+      (strchr(pattern,'?') == NULL))
+    {
+      char
+        path[MagickPathExtent]= { 0 };
+
+      /*
+        If no glob characters exist, ensure no subimage specifier.
+      */
+      GetPathComponent(pattern,SubimagePath,path);
+      if (*path != '\0')
+        return(MagickFalse);
+    }
+  while (GetUTFCode(pattern) != 0)
   {
-    if (GetUTFCode(expression) == 0)
-      if ((GetUTFCode(pattern) != '{') && (GetUTFCode(pattern) != '*'))
-        break;
-    switch (GetUTFCode(pattern))
+    int
+      ecode = GetUTFCode(expression),
+      pcode = GetUTFCode(pattern);
+
+    if ((ecode == 0) && (pcode != '*') && (pcode != '{'))
+      break;
+    switch (pcode)
     {
       case '*':
       {
-        MagickBooleanType
-          status;
-
-        status=MagickFalse;
-        while (GetUTFCode(pattern) == '*')
-          pattern+=GetUTFOctets(pattern);
-        while ((GetUTFCode(expression) != 0) && (status == MagickFalse))
+        do
         {
-          status=GlobExpression(expression,pattern,case_insensitive);
-          expression+=GetUTFOctets(expression);
+          /*
+            Skip consecutive '*'.
+          */
+          pattern+=GetUTFOctets(pattern);
         }
-        if (status != MagickFalse)
-          {
-            while (GetUTFCode(expression) != 0)
-              expression+=GetUTFOctets(expression);
-            while (GetUTFCode(pattern) != 0)
-              pattern+=GetUTFOctets(pattern);
+        while (GetUTFCode(pattern) == '*');
+        while (1)
+        {
+          /*
+            Try to match at each position.
+          */
+          if (GlobExpression_(expression,pattern,case_insensitive,depth+1) != MagickFalse)
+            {
+              /*
+                Consume rest of expression and pattern.
+              */
+              while (GetUTFCode(expression) != 0)
+                expression+=GetUTFOctets(expression);
+              while (GetUTFCode(pattern) != 0)
+                pattern+=GetUTFOctets(pattern);
+              return(MagickTrue);
+            }
+            if (GetUTFCode(expression) == 0)
+              break;
+            expression+=GetUTFOctets(expression);
           }
+        return(MagickFalse);
+      }
+      case '?':
+      {
+        if (ecode == 0)
+          return(MagickFalse);
+        pattern+=GetUTFOctets(pattern);
+        expression+=GetUTFOctets(expression);
         break;
       }
       case '[':
       {
-        int
-          c;
+        const char
+          *p = pattern+GetUTFOctets(pattern),
+          *q = pattern+GetUTFOctets(pattern);

-        pattern+=GetUTFOctets(pattern);
-        for ( ; ; )
+        MagickBooleanType
+          matched = MagickFalse;
+
+        if (ecode == 0)
+          return(MagickFalse);
+        while ((GetUTFCode(q) != 0) && (GetUTFCode(q) != ']'))
+          q+=GetUTFOctets(q);
+        if (GetUTFCode(q) == 0)
+          return(MagickFalse);  /* malformed */
+        while (p < q)
         {
-          if ((GetUTFCode(pattern) == 0) || (GetUTFCode(pattern) == ']'))
+          const char
+            *next;
+
+          int
+            code = GetUTFCode(p);
+
+          size_t
+            octets = GetUTFOctets(p);
+
+          if (code == '\\')
             {
-              done=MagickTrue;
-              break;
+              p+=octets;
+              code=GetUTFCode(p);
+              octets=GetUTFOctets(p);
             }
-          if (GetUTFCode(pattern) == '\\')
+          next=p+octets;
+          if ((next < q) && (GetUTFCode(next) == '-'))
             {
-              pattern+=GetUTFOctets(pattern);
-              if (GetUTFCode(pattern) == 0)
-                {
-                  done=MagickTrue;
-                  break;
-                }
-             }
-          if (GetUTFCode(pattern+GetUTFOctets(pattern)) == '-')
-            {
-              c=GetUTFCode(pattern);
-              pattern+=GetUTFOctets(pattern);
-              pattern+=GetUTFOctets(pattern);
-              if (GetUTFCode(pattern) == ']')
-                {
-                  done=MagickTrue;
-                  break;
-                }
-              if (GetUTFCode(pattern) == '\\')
-                {
-                  pattern+=GetUTFOctets(pattern);
-                  if (GetUTFCode(pattern) == 0)
-                    {
-                      done=MagickTrue;
-                      break;
-                    }
-                }
-              if ((GetUTFCode(expression) < c) ||
-                  (GetUTFCode(expression) > GetUTFCode(pattern)))
+              int
+                ncode;
+
+              next+=GetUTFOctets(next);
+              ncode=GetUTFCode(next);
+              if (ncode == '\\')
                 {
-                  pattern+=GetUTFOctets(pattern);
-                  continue;
+                  next+=GetUTFOctets(next);
+                  ncode=GetUTFCode(next);
                 }
+              if ((ecode >= code) && (ecode <= ncode))
+                matched=MagickTrue;
+              p=next+GetUTFOctets(next);
             }
           else
-            if (GetUTFCode(pattern) != GetUTFCode(expression))
-              {
-                pattern+=GetUTFOctets(pattern);
-                continue;
-              }
-          pattern+=GetUTFOctets(pattern);
-          while ((GetUTFCode(pattern) != ']') && (GetUTFCode(pattern) != 0))
-          {
-            if ((GetUTFCode(pattern) == '\\') &&
-                (GetUTFCode(pattern+GetUTFOctets(pattern)) > 0))
-              pattern+=GetUTFOctets(pattern);
-            pattern+=GetUTFOctets(pattern);
-          }
-          if (GetUTFCode(pattern) != 0)
             {
-              pattern+=GetUTFOctets(pattern);
-              expression+=GetUTFOctets(expression);
+              if (ecode == code)
+                matched=MagickTrue;
+              p+=octets;
             }
-          break;
         }
-        break;
-      }
-      case '?':
-      {
-        pattern+=GetUTFOctets(pattern);
+        /*
+          Skip consecutive '*'.
+        */
+        if (matched == MagickFalse)
+          return(MagickFalse);
+        pattern=q+GetUTFOctets(q);  /* skip ']' */
         expression+=GetUTFOctets(expression);
         break;
       }
       case '{':
       {
         char
-          *target;
+          *a,
+          *alternative;

-        char
-          *p;
+        const char
+          *p,
+          *q;
+
+        size_t
+          remaining = MagickPathExtent;

-        target=AcquireString(pattern);
-        p=target;
-        pattern++;
-        while ((GetUTFCode(pattern) != '}') && (GetUTFCode(pattern) != 0))
+        pattern+=GetUTFOctets(pattern);  /* Skip '{' */
+        if (GetUTFCode(pattern) == 0)
+          return(MagickFalse);
+        /*
+          End of brace expression: append remaining pattern.
+        */
+        p=pattern;
+        while ((GetUTFCode(p) != 0) && (GetUTFCode(p) != '}'))
         {
-          *p++=(*pattern++);
-          if ((GetUTFCode(pattern) == ',') || (GetUTFCode(pattern) == '}'))
+#if !defined(MAGICKCORE_WINDOWS_SUPPORT) || defined(__CYGWIN__)
+          if (GetUTFCode(p) == '\\')
             {
-              *p='\0';
-              match=GlobExpression(expression,target,case_insensitive);
+              p+=GetUTFOctets(p);
+              if (GetUTFCode(p) == 0)
+                break;
+            }
+#endif
+          p+=GetUTFOctets(p);
+        }
+        if (GetUTFCode(p) != '}')
+          return(MagickFalse);  /* malformed */
+        q=p+GetUTFOctets(p);
+        alternative=AcquireString(pattern);
+        a=alternative;
+        while (1)
+        {
+          int
+            code = GetUTFCode(pattern);
+
+          size_t
+            octets;
+
+          if ((code == 0) || (code == ',') || (code == '}'))
+            {
+              char
+                *subpattern;
+
+              MagickBooleanType
+                match;
+
+              /*
+                Try alternative as a full sub-pattern.
+              */
+              *a='\0';
+              subpattern=AcquireString(alternative);
+              if (ConcatenateString(&subpattern,q) == MagickFalse)
+                {
+                  subpattern=DestroyString(subpattern);
+                  alternative=DestroyString(alternative);
+                  return(MagickFalse);
+                }
+              match=GlobExpression_(expression,subpattern,case_insensitive,
+                depth+1);
+              subpattern=DestroyString(subpattern);
               if (match != MagickFalse)
                 {
-                  expression+=MagickMin(strlen(expression),strlen(target));
-                  break;
+                  /*
+                    Consume rest of expression and pattern.
+                  */
+                  while (GetUTFCode(expression) != 0)
+                    expression+=GetUTFOctets(expression);
+                  pattern=q;
+                  while (GetUTFCode(pattern) != 0)
+                    pattern+=GetUTFOctets(pattern);
+                  alternative=DestroyString(alternative);
+                  return(MagickTrue);
                 }
-              p=target;
-              pattern+=GetUTFOctets(pattern);
+              /*
+                Reset buffer for next alternative.
+              */
+              a=alternative;
+              remaining=MagickPathExtent;
+              if (code == ',')
+                {
+                  pattern+=GetUTFOctets(pattern);  /* skip ',' */
+                  continue;
+                }
+              break;  /* '}' or end */
             }
+          /*
+            Copy UTF-8 sequence into alternative.
+          */
+          octets=GetUTFOctets(pattern);
+          if ((octets == 0) || (octets >= remaining))
+            break;
+          (void) memcpy(a,pattern,octets);
+          a+=octets;
+          remaining-=octets;
+          pattern+=octets;
         }
-        while ((GetUTFCode(pattern) != '}') && (GetUTFCode(pattern) != 0))
-          pattern+=GetUTFOctets(pattern);
-        if (GetUTFCode(pattern) != 0)
-          pattern+=GetUTFOctets(pattern);
-        target=DestroyString(target);
-        break;
+        alternative=DestroyString(alternative);
+        return(MagickFalse);
       }
 #if !defined(MAGICKCORE_WINDOWS_SUPPORT) || defined(__CYGWIN__)
       case '\\':
       {
         pattern+=GetUTFOctets(pattern);
         if (GetUTFCode(pattern) == 0)
-          break;
+          return(MagickFalse);
         magick_fallthrough;
       }
 #endif
       default:
       {
+        int
+          ec = ecode,
+          pc = pcode;
+
+        if (ecode == 0)
+          return(MagickFalse);
         if (case_insensitive != MagickFalse)
           {
-            if (LocaleToLowercase((int) GetUTFCode(expression)) != LocaleToLowercase((int) GetUTFCode(pattern)))
-              {
-                done=MagickTrue;
-                break;
-              }
+            pc=LocaleToLowercase(pc);
+            ec=LocaleToLowercase(ec);
           }
-        else
-          if (GetUTFCode(expression) != GetUTFCode(pattern))
-            {
-              done=MagickTrue;
-              break;
-            }
-        expression+=GetUTFOctets(expression);
+        if (pc != ec)
+          return(MagickFalse);
         pattern+=GetUTFOctets(pattern);
+        expression+=GetUTFOctets(expression);
+        break;
       }
     }
   }
   while (GetUTFCode(pattern) == '*')
     pattern+=GetUTFOctets(pattern);
-  match=(GetUTFCode(expression) == 0) && (GetUTFCode(pattern) == 0) ?
-    MagickTrue : MagickFalse;
-  return(match);
+  return(((GetUTFCode(expression) == 0) &&
+          (GetUTFCode(pattern) == 0)) ? MagickTrue : MagickFalse);
+}
+
+MagickExport MagickBooleanType GlobExpression(
+  const char *magick_restrict expression,const char *magick_restrict pattern,
+  const MagickBooleanType case_insensitive)
+{
+  return(GlobExpression_(expression,pattern,case_insensitive,0));
 }

 /*