Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package php-composer2 for openSUSE:Factory 
checked in at 2026-08-28 19:54:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/php-composer2 (Old)
 and      /work/SRC/openSUSE:Factory/.php-composer2.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "php-composer2"

Fri Aug 28 19:54:53 2026 rev:39 rq:1374350 version:2.10.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/php-composer2/php-composer2.changes      
2026-08-01 18:37:08.211955509 +0200
+++ /work/SRC/openSUSE:Factory/.php-composer2.new.1265/php-composer2.changes    
2026-08-28 19:57:52.511983767 +0200
@@ -1,0 +2,16 @@
+Fri Aug 28 11:02:52 UTC 2026 - Petr Gajdos <[email protected]>
+
+- version update to 2.10.3
+  * Validate package bin paths against path traversal using symlinks 
(GHSA-96h3-5x6v-m776, CVE-2026-59944)
+  * Fixed command injection via malicious Perforce url (GHSA-rvx4-ffvw-m9q3)
+  * Sanitize URL-embedded usernames/token in a few more places (#13044)
+  * Fixed matching of gitlab URLs to avoid possible credential leak to the 
wrong domain (#12988)
+  * Fixed PHP 8.6 deprecation warnings (#12967, #13028)
+  * Fixed error output when a policy blocks a package version to be clearer 
(#12993)
+  * Fixed the lock file's content-hash with a conflict marker ending up 
breaking at runtime (#13048)
+  * Fixed possible race condition while creating directories like the cache 
dir when running multiple
+      Composer processes in parallel (#12977)
+  * Fixed forgejo support to handle empty repositories better (#12968)
+  * Fixed FilterListApiClient not forwarding transport options (#13040)
+
+-------------------------------------------------------------------

Old:
----
  composer-2.10.2.tar.gz

New:
----
  composer-2.10.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ php-composer2.spec ++++++
--- /var/tmp/diff_new_pack.EetwlI/_old  2026-08-28 19:57:53.702025373 +0200
+++ /var/tmp/diff_new_pack.EetwlI/_new  2026-08-28 19:57:53.703025408 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           php-composer2
-Version:        2.10.2
+Version:        2.10.3
 Release:        0
 Summary:        Dependency Management for PHP
 License:        MIT

++++++ composer-2.10.2.tar.gz -> composer-2.10.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/composer-2.10.2/composer.json 
new/composer-2.10.3/composer.json
--- old/composer-2.10.2/composer.json   2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/composer.json   2026-08-27 13:34:23.000000000 +0200
@@ -23,6 +23,8 @@
     ],
     "require": {
         "php": "^7.2.5 || ^8.0",
+        "ext-filter": "*",
+        "ext-hash": "*",
         "ext-json": "*",
         "composer/ca-bundle": "^1.5",
         "composer/class-map-generator": "^1.4.0",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/composer-2.10.2/composer.lock 
new/composer-2.10.3/composer.lock
--- old/composer-2.10.2/composer.lock   2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/composer.lock   2026-08-27 13:34:23.000000000 +0200
@@ -4,7 +4,7 @@
         "Read more about it at 
https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies";,
         "This file is @generated automatically"
     ],
-    "content-hash": "449b675e25bbd33266e405fe9ba8fe9b",
+    "content-hash": "c476757337544dcb83275f61d8692ed0",
     "packages": [
         {
             "name": "composer/ca-bundle",
@@ -2564,6 +2564,8 @@
     "prefer-lowest": false,
     "platform": {
         "php": "^7.2.5 || ^8.0",
+        "ext-filter": "*",
+        "ext-hash": "*",
         "ext-json": "*"
     },
     "platform-dev": {},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/composer-2.10.2/src/Composer/Advisory/Auditor.php 
new/composer-2.10.3/src/Composer/Advisory/Auditor.php
--- old/composer-2.10.2/src/Composer/Advisory/Auditor.php       2026-07-01 
11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Advisory/Auditor.php       2026-08-27 
13:34:23.000000000 +0200
@@ -523,6 +523,9 @@
                     if ($entry->url !== null) {
                         $parts[] = 'URL: ' . $entry->url;
                     }
+                    if ($entry->source !== null) {
+                        $parts[] = 'Source: ' . $entry->source;
+                    }
                     $io->write(implode('. ', $parts) . '.');
                 }
             }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Autoload/AutoloadGenerator.php 
new/composer-2.10.3/src/Composer/Autoload/AutoloadGenerator.php
--- old/composer-2.10.2/src/Composer/Autoload/AutoloadGenerator.php     
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Autoload/AutoloadGenerator.php     
2026-08-27 13:34:23.000000000 +0200
@@ -426,7 +426,10 @@
             }
 
             if (null === $suffix) {
-                $suffix = $locker !== null && $locker->isLocked() ? 
$locker->getLockData()['content-hash'] : bin2hex(random_bytes(16));
+                // a lock file with an unresolved merge conflict has its 
content-hash replaced by a
+                // human readable message, which would end up inside the 
autoloader class names
+                $contentHash = $locker !== null && $locker->isLocked() ? 
$locker->getLockData()['content-hash'] : null;
+                $suffix = is_string($contentHash) && 
Preg::isMatch('{^[a-f0-9]+$}', $contentHash) ? $contentHash : 
bin2hex(random_bytes(16));
             }
         }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/composer-2.10.2/src/Composer/Composer.php 
new/composer-2.10.3/src/Composer/Composer.php
--- old/composer-2.10.2/src/Composer/Composer.php       2026-07-01 
11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Composer.php       2026-08-27 
13:34:23.000000000 +0200
@@ -51,9 +51,9 @@
      *
      * @see getVersion()
      */
-    public const VERSION = '2.10.2';
+    public const VERSION = '2.10.3';
     public const BRANCH_ALIAS_VERSION = '';
-    public const RELEASE_DATE = '2026-07-01 11:24:45';
+    public const RELEASE_DATE = '2026-08-27 13:34:23';
     public const SOURCE_VERSION = '';
 
     /**
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/DependencyResolver/LockTransaction.php 
new/composer-2.10.3/src/Composer/DependencyResolver/LockTransaction.php
--- old/composer-2.10.2/src/Composer/DependencyResolver/LockTransaction.php     
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/DependencyResolver/LockTransaction.php     
2026-08-27 13:34:23.000000000 +0200
@@ -27,9 +27,9 @@
     /**
      * packages in current lock file, platform repo or otherwise present
      *
-     * Indexed by spl_object_hash
+     * Indexed by spl_object_id
      *
-     * @var array<string, BasePackage>
+     * @var array<int, BasePackage>
      */
     protected $presentMap;
 
@@ -48,7 +48,7 @@
     protected $resultPackages;
 
     /**
-     * @param array<string, BasePackage> $presentMap
+     * @param array<int, BasePackage> $presentMap
      * @param array<int, BasePackage> $unlockableMap
      */
     public function __construct(Pool $pool, array $presentMap, array 
$unlockableMap, Decisions $decisions)
@@ -112,7 +112,7 @@
             }
 
             // if we're just updating mirrors we need to reset everything to 
the same as currently "present" packages' references to keep the lock file as-is
-            if ($updateMirrors === true && 
!array_key_exists(spl_object_hash($package), $this->presentMap)) {
+            if ($updateMirrors === true && 
!array_key_exists(spl_object_id($package), $this->presentMap)) {
                 $package = $this->updateMirrorAndUrls($package);
             }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/DependencyResolver/Pool.php 
new/composer-2.10.3/src/Composer/DependencyResolver/Pool.php
--- old/composer-2.10.2/src/Composer/DependencyResolver/Pool.php        
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/DependencyResolver/Pool.php        
2026-08-27 13:34:23.000000000 +0200
@@ -41,7 +41,7 @@
     protected $unacceptableFixedOrLockedPackages;
     /** @var array<string, array<string, string>> Map of package name => 
normalized version => pretty version */
     protected $removedVersions = [];
-    /** @var array<string, array<string, string>> Map of package object hash 
=> removed normalized versions => removed pretty version */
+    /** @var array<int, array<string, string>> Map of package object id => 
removed normalized versions => removed pretty version */
     protected $removedVersionsByPackage = [];
     /** @var array<string, array<string, 
array<SecurityAdvisory|PartialSecurityAdvisory>>> Map of package name => 
normalized version => security advisories */
     private $securityRemovedVersions = [];
@@ -54,7 +54,7 @@
      * @param BasePackage[] $packages
      * @param BasePackage[] $unacceptableFixedOrLockedPackages
      * @param array<string, array<string, string>> $removedVersions
-     * @param array<string, array<string, string>> $removedVersionsByPackage
+     * @param array<int, array<string, string>> $removedVersionsByPackage
      * @param array<string, array<string, 
array<SecurityAdvisory|PartialSecurityAdvisory>>> $securityRemovedVersions
      * @param array<string, array<string, string>> $abandonedRemovedVersions
      * @param array<string, array<string, list<FilterListEntry>>> 
$filterListRemovedVersions
@@ -101,17 +101,17 @@
     /**
      * @return array<string, string>
      */
-    public function getRemovedVersionsByPackage(string $objectHash): array
+    public function getRemovedVersionsByPackage(int $objectId): array
     {
-        if (!isset($this->removedVersionsByPackage[$objectHash])) {
+        if (!isset($this->removedVersionsByPackage[$objectId])) {
             return [];
         }
 
-        return $this->removedVersionsByPackage[$objectHash];
+        return $this->removedVersionsByPackage[$objectId];
     }
 
     /**
-     * @return array<string, array<string, string>>
+     * @return array<int, array<string, string>>
      */
     public function getAllRemovedVersionsByPackage(): array
     {
@@ -201,17 +201,18 @@
         foreach ($this->filterListRemovedVersions[$packageName] ?? [] as 
$version => $filterListEntries) {
             if ($constraint !== null && $constraint->matches(new 
Constraint('==', $version))) {
                 foreach ($filterListEntries as $entry) {
-                    $entryKey = spl_object_hash($entry);
+                    $entryKey = spl_object_id($entry);
                     if (isset($seen[$entryKey])) {
                         continue;
                     }
 
                     $seen[$entryKey] = true;
 
+                    $source = (bool) $entry->source ? ' reported by ' . 
$entry->source : '';
                     $url = (bool) $entry->url ? ' (see ' . $entry->url . ')' : 
'';
                     $reason = (bool) $entry->reason ? ' reason: ' . 
$entry->reason : '';
 
-                    $lists[$entry->listName][] =  $url . $reason;
+                    $lists[$entry->listName][] =  $source . $url . $reason;
                 }
 
             }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/DependencyResolver/PoolBuilder.php 
new/composer-2.10.3/src/Composer/DependencyResolver/PoolBuilder.php
--- old/composer-2.10.2/src/Composer/DependencyResolver/PoolBuilder.php 
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/DependencyResolver/PoolBuilder.php 
2026-08-27 13:34:23.000000000 +0200
@@ -78,7 +78,7 @@
     private $io;
     /**
      * @var array[]
-     * @phpstan-var array<string, AliasPackage[]>
+     * @phpstan-var array<int, AliasPackage[]>
      */
     private $aliasMap = [];
     /**
@@ -303,8 +303,8 @@
 
                     $constraint = $this->temporaryConstraints[$packageName];
                     $packageAndAliases = [$i => $package];
-                    if (isset($this->aliasMap[spl_object_hash($package)])) {
-                        $packageAndAliases += 
$this->aliasMap[spl_object_hash($package)];
+                    if (isset($this->aliasMap[spl_object_id($package)])) {
+                        $packageAndAliases += 
$this->aliasMap[spl_object_id($package)];
                     }
 
                     $found = false;
@@ -478,7 +478,7 @@
         $this->packages[$index] = $package;
 
         if ($package instanceof AliasPackage) {
-            $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = 
$package;
+            $this->aliasMap[spl_object_id($package->getAliasOf())][$index] = 
$package;
         }
 
         $name = $package->getName();
@@ -514,7 +514,7 @@
 
             $newIndex = $this->indexCounter++;
             $this->packages[$newIndex] = $aliasPackage;
-            
$this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$newIndex] = 
$aliasPackage;
+            
$this->aliasMap[spl_object_id($aliasPackage->getAliasOf())][$newIndex] = 
$aliasPackage;
         }
 
         foreach ($package->getRequires() as $link) {
@@ -780,12 +780,12 @@
 
         
unset($this->loadedPerRepo[$repoIndex][$package->getName()][$package->getVersion()]);
         unset($this->packages[$index]);
-        if (isset($this->aliasMap[spl_object_hash($package)])) {
-            foreach ($this->aliasMap[spl_object_hash($package)] as $aliasIndex 
=> $aliasPackage) {
+        if (isset($this->aliasMap[spl_object_id($package)])) {
+            foreach ($this->aliasMap[spl_object_id($package)] as $aliasIndex 
=> $aliasPackage) {
                 
unset($this->loadedPerRepo[$repoIndex][$aliasPackage->getName()][$aliasPackage->getVersion()]);
                 unset($this->packages[$aliasIndex]);
             }
-            unset($this->aliasMap[spl_object_hash($package)]);
+            unset($this->aliasMap[spl_object_id($package)]);
         }
     }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/DependencyResolver/PoolOptimizer.php 
new/composer-2.10.3/src/Composer/DependencyResolver/PoolOptimizer.php
--- old/composer-2.10.2/src/Composer/DependencyResolver/PoolOptimizer.php       
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/DependencyResolver/PoolOptimizer.php       
2026-08-27 13:34:23.000000000 +0200
@@ -59,7 +59,7 @@
     private $aliasesPerPackage = [];
 
     /**
-     * @var array<string, array<string, string>>
+     * @var array<int, array<string, string>>
      */
     private $removedVersionsByPackage = [];
 
@@ -364,7 +364,7 @@
         }
 
         foreach ($versions as $version => $prettyVersion) {
-            
$this->removedVersionsByPackage[spl_object_hash($package)][$version] = 
$prettyVersion;
+            $this->removedVersionsByPackage[spl_object_id($package)][$version] 
= $prettyVersion;
         }
     }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/DependencyResolver/Problem.php 
new/composer-2.10.3/src/Composer/DependencyResolver/Problem.php
--- old/composer-2.10.2/src/Composer/DependencyResolver/Problem.php     
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/DependencyResolver/Problem.php     
2026-08-27 13:34:23.000000000 +0200
@@ -38,7 +38,7 @@
 {
     /**
      * A map containing the id of each rule part of this problem as a key
-     * @var array<string, true>
+     * @var array<int, true>
      */
     protected $reasonSeen;
 
@@ -58,7 +58,7 @@
      */
     public function addRule(Rule $rule): void
     {
-        $this->addReason(spl_object_hash($rule), $rule);
+        $this->addReason(spl_object_id($rule), $rule);
     }
 
     /**
@@ -182,7 +182,7 @@
                 $messages[] = $template;
                 $templates[$template][$m[1]][$parser->normalize($m[2])] = 
$m[2];
                 $sourcePackage = $rule->getSourcePackage($pool);
-                foreach 
($pool->getRemovedVersionsByPackage(spl_object_hash($sourcePackage)) as 
$version => $prettyVersion) {
+                foreach 
($pool->getRemovedVersionsByPackage(spl_object_id($sourcePackage)) as $version 
=> $prettyVersion) {
                     $templates[$template][$m[1]][$version] = $prettyVersion;
                 }
             } elseif ($message !== '') {
@@ -230,10 +230,10 @@
     /**
      * Store a reason descriptor but ignore duplicates
      *
-     * @param string $id     A canonical identifier for the reason
-     * @param Rule   $reason The reason descriptor
+     * @param int  $id     A canonical identifier for the reason
+     * @param Rule $reason The reason descriptor
      */
-    protected function addReason(string $id, Rule $reason): void
+    protected function addReason(int $id, Rule $reason): void
     {
         // TODO: if a rule is part of a problem description in two sections, 
isn't this going to remove a message
         // that is important to understand the issue?
@@ -553,7 +553,7 @@
                 }
             }
             if ($pool !== null && $useRemovedVersionGroup) {
-                foreach 
($pool->getRemovedVersionsByPackage(spl_object_hash($package)) as $version => 
$prettyVersion) {
+                foreach 
($pool->getRemovedVersionsByPackage(spl_object_id($package)) as $version => 
$prettyVersion) {
                     $prepared[$package->getName()]['versions'][$version] = 
$prettyVersion;
                 }
             }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/DependencyResolver/Request.php 
new/composer-2.10.3/src/Composer/DependencyResolver/Request.php
--- old/composer-2.10.2/src/Composer/DependencyResolver/Request.php     
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/DependencyResolver/Request.php     
2026-08-27 13:34:23.000000000 +0200
@@ -44,11 +44,11 @@
     protected $lockedRepository;
     /** @var array<string, ConstraintInterface> */
     protected $requires = [];
-    /** @var array<string, BasePackage> */
+    /** @var array<int, BasePackage> */
     protected $fixedPackages = [];
-    /** @var array<string, BasePackage> */
+    /** @var array<int, BasePackage> */
     protected $lockedPackages = [];
-    /** @var array<string, BasePackage> */
+    /** @var array<int, BasePackage> */
     protected $fixedLockedPackages = [];
     /** @var array<string> */
     protected $updateAllowList = [];
@@ -83,7 +83,7 @@
      */
     public function fixPackage(BasePackage $package): void
     {
-        $this->fixedPackages[spl_object_hash($package)] = $package;
+        $this->fixedPackages[spl_object_id($package)] = $package;
     }
 
     /**
@@ -98,7 +98,7 @@
      */
     public function lockPackage(BasePackage $package): void
     {
-        $this->lockedPackages[spl_object_hash($package)] = $package;
+        $this->lockedPackages[spl_object_id($package)] = $package;
     }
 
     /**
@@ -110,13 +110,13 @@
      */
     public function fixLockedPackage(BasePackage $package): void
     {
-        $this->fixedPackages[spl_object_hash($package)] = $package;
-        $this->fixedLockedPackages[spl_object_hash($package)] = $package;
+        $this->fixedPackages[spl_object_id($package)] = $package;
+        $this->fixedLockedPackages[spl_object_id($package)] = $package;
     }
 
     public function unlockPackage(BasePackage $package): void
     {
-        unset($this->lockedPackages[spl_object_hash($package)]);
+        unset($this->lockedPackages[spl_object_id($package)]);
     }
 
     /**
@@ -156,7 +156,7 @@
     }
 
     /**
-     * @return array<string, BasePackage>
+     * @return array<int, BasePackage>
      */
     public function getFixedPackages(): array
     {
@@ -165,11 +165,11 @@
 
     public function isFixedPackage(BasePackage $package): bool
     {
-        return isset($this->fixedPackages[spl_object_hash($package)]);
+        return isset($this->fixedPackages[spl_object_id($package)]);
     }
 
     /**
-     * @return array<string, BasePackage>
+     * @return array<int, BasePackage>
      */
     public function getLockedPackages(): array
     {
@@ -178,11 +178,11 @@
 
     public function isLockedPackage(PackageInterface $package): bool
     {
-        return isset($this->lockedPackages[spl_object_hash($package)]) || 
isset($this->fixedLockedPackages[spl_object_hash($package)]);
+        return isset($this->lockedPackages[spl_object_id($package)]) || 
isset($this->fixedLockedPackages[spl_object_id($package)]);
     }
 
     /**
-     * @return array<string, BasePackage>
+     * @return array<int, BasePackage>
      */
     public function getFixedOrLockedPackages(): array
     {
@@ -190,7 +190,7 @@
     }
 
     /**
-     * @return ($packageIds is true ? array<int, BasePackage> : array<string, 
BasePackage>)
+     * @return array<int, BasePackage>
      *
      * @TODO look into removing the packageIds option, the only place true is 
used
      *       is for the installed map in the solver problems.
@@ -203,12 +203,12 @@
 
         if ($this->lockedRepository !== null) {
             foreach ($this->lockedRepository->getPackages() as $package) {
-                $presentMap[$packageIds ? $package->getId() : 
spl_object_hash($package)] = $package;
+                $presentMap[$packageIds ? $package->getId() : 
spl_object_id($package)] = $package;
             }
         }
 
         foreach ($this->fixedPackages as $package) {
-            $presentMap[$packageIds ? $package->getId() : 
spl_object_hash($package)] = $package;
+            $presentMap[$packageIds ? $package->getId() : 
spl_object_id($package)] = $package;
         }
 
         return $presentMap;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/DependencyResolver/Solver.php 
new/composer-2.10.3/src/Composer/DependencyResolver/Solver.php
--- old/composer-2.10.2/src/Composer/DependencyResolver/Solver.php      
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/DependencyResolver/Solver.php      
2026-08-27 13:34:23.000000000 +0200
@@ -50,7 +50,7 @@
     protected $problems = [];
     /** @var array<Rule[]> */
     protected $learnedPool = [];
-    /** @var array<string, int> */
+    /** @var array<int, int> */
     protected $learnedWhy = [];
 
     /** @var bool */
@@ -340,7 +340,7 @@
 
             $this->rules->add($newRule, RuleSet::TYPE_LEARNED);
 
-            $this->learnedWhy[spl_object_hash($newRule)] = $why;
+            $this->learnedWhy[spl_object_id($newRule)] = $why;
 
             $ruleNode = new RuleWatchNode($newRule);
             $ruleNode->watch2OnHighest($this->decisions);
@@ -518,11 +518,11 @@
     }
 
     /**
-     * @param array<string, true> $ruleSeen
+     * @param array<int, true> $ruleSeen
      */
     private function analyzeUnsolvableRule(Problem $problem, Rule 
$conflictRule, array &$ruleSeen): void
     {
-        $why = spl_object_hash($conflictRule);
+        $why = spl_object_id($conflictRule);
         $ruleSeen[$why] = true;
 
         if ($conflictRule->getType() === RuleSet::TYPE_LEARNED) {
@@ -530,7 +530,7 @@
             $problemRules = $this->learnedPool[$learnedWhy];
 
             foreach ($problemRules as $problemRule) {
-                if (!isset($ruleSeen[spl_object_hash($problemRule)])) {
+                if (!isset($ruleSeen[spl_object_id($problemRule)])) {
                     $this->analyzeUnsolvableRule($problem, $problemRule, 
$ruleSeen);
                 }
             }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/DependencyResolver/Transaction.php 
new/composer-2.10.3/src/Composer/DependencyResolver/Transaction.php
--- old/composer-2.10.2/src/Composer/DependencyResolver/Transaction.php 
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/DependencyResolver/Transaction.php 
2026-08-27 13:34:23.000000000 +0200
@@ -38,7 +38,7 @@
 
     /**
      * Package set resulting from this transaction
-     * @var array<string, PackageInterface>
+     * @var array<int, PackageInterface>
      */
     protected $resultPackageMap;
 
@@ -87,7 +87,7 @@
 
         $this->resultPackageMap = [];
         foreach ($resultPackages as $package) {
-            $this->resultPackageMap[spl_object_hash($package)] = $package;
+            $this->resultPackageMap[spl_object_id($package)] = $package;
             foreach ($package->getNames() as $name) {
                 $this->resultPackagesByName[$name][] = $package;
             }
@@ -128,12 +128,12 @@
         while (\count($stack) > 0) {
             $package = array_pop($stack);
 
-            if (isset($processed[spl_object_hash($package)])) {
+            if (isset($processed[spl_object_id($package)])) {
                 continue;
             }
 
-            if (!isset($visited[spl_object_hash($package)])) {
-                $visited[spl_object_hash($package)] = true;
+            if (!isset($visited[spl_object_id($package)])) {
+                $visited[spl_object_id($package)] = true;
 
                 $stack[] = $package;
                 if ($package instanceof AliasPackage) {
@@ -147,8 +147,8 @@
                         }
                     }
                 }
-            } elseif (!isset($processed[spl_object_hash($package)])) {
-                $processed[spl_object_hash($package)] = true;
+            } elseif (!isset($processed[spl_object_id($package)])) {
+                $processed[spl_object_id($package)] = true;
 
                 if ($package instanceof AliasPackage) {
                     $aliasKey = 
$package->getName().'::'.$package->getVersion();
@@ -225,7 +225,7 @@
      * These serve as a starting point to enumerate packages in a topological 
order despite potential cycles.
      * If there are packages with a cycle on the top level the package with 
the lowest name gets picked
      *
-     * @return array<string, PackageInterface>
+     * @return array<int, PackageInterface>
      */
     protected function getRootPackages(): array
     {
@@ -241,7 +241,7 @@
 
                 foreach ($possibleRequires as $require) {
                     if ($require !== $package) {
-                        unset($roots[spl_object_hash($require)]);
+                        unset($roots[spl_object_id($require)]);
                     }
                 }
             }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Downloader/FileDownloader.php 
new/composer-2.10.3/src/Composer/Downloader/FileDownloader.php
--- old/composer-2.10.2/src/Composer/Downloader/FileDownloader.php      
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Downloader/FileDownloader.php      
2026-08-27 13:34:23.000000000 +0200
@@ -26,6 +26,7 @@
 use Composer\Plugin\PostFileDownloadEvent;
 use Composer\Plugin\PreFileDownloadEvent;
 use Composer\EventDispatcher\EventDispatcher;
+use Composer\Installer\BinaryInstaller;
 use Composer\Util\Filesystem;
 use Composer\Util\Http\Response;
 use Composer\Util\Platform;
@@ -364,9 +365,16 @@
         // Single files can not have a mode set like files in archives
         // so we make sure if the file is a binary that it is executable
         foreach ($package->getBinaries() as $bin) {
-            if (file_exists($path . '/' . $bin) && !is_executable($path . '/' 
. $bin)) {
-                Silencer::call('chmod', $path . '/' . $bin, 0777 & ~umask());
+            $binPath = $path . '/' . $bin;
+            if (!file_exists($binPath) || is_executable($binPath)) {
+                continue;
+            }
+            // a bin resolving outside of the package would let it chmod an 
arbitrary host file, this
+            // is reported by BinaryInstaller later in the same install 
(GHSA-96h3-5x6v-m776)
+            if (!BinaryInstaller::isBinPathInsidePackage($path, $binPath)) {
+                continue;
             }
+            Silencer::call('chmod', $binPath, 0777 & ~umask());
         }
 
         return \React\Promise\resolve(null);
@@ -448,7 +456,7 @@
             $extension = $package->getDistType();
         }
 
-        return rtrim($this->config->get('vendor-dir') . '/composer/tmp-' . 
hash('md5', $package . spl_object_hash($package)) . '.' . $extension, '.');
+        return rtrim($this->config->get('vendor-dir') . '/composer/tmp-' . 
hash('md5', $package . spl_object_id($package)) . '.' . $extension, '.');
     }
 
     /**
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/EventDispatcher/EventDispatcher.php 
new/composer-2.10.3/src/Composer/EventDispatcher/EventDispatcher.php
--- old/composer-2.10.2/src/Composer/EventDispatcher/EventDispatcher.php        
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/EventDispatcher/EventDispatcher.php        
2026-08-27 13:34:23.000000000 +0200
@@ -707,10 +707,10 @@
             return 'fn:'.$cb;
         }
         if (is_object($cb)) {
-            return 'obj:'.spl_object_hash($cb);
+            return 'obj:'.spl_object_id($cb);
         }
         if (is_array($cb)) {
-            return 'array:'.(is_string($cb[0]) ? $cb[0] : get_class($cb[0]) 
.'#'.spl_object_hash($cb[0])).'::'.$cb[1];
+            return 'array:'.(is_string($cb[0]) ? $cb[0] : get_class($cb[0]) 
.'#'.spl_object_id($cb[0])).'::'.$cb[1];
         }
 
         // not great but also do not want to break everything here
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/FilterList/FilterListApiClient.php 
new/composer-2.10.3/src/Composer/FilterList/FilterListApiClient.php
--- old/composer-2.10.2/src/Composer/FilterList/FilterListApiClient.php 
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/FilterList/FilterListApiClient.php 
2026-08-27 13:34:23.000000000 +0200
@@ -24,10 +24,16 @@
 {
     /** @var HttpDownloader */
     private $httpDownloader;
+    /** @var mixed[] */
+    private $options;
 
-    public function __construct(HttpDownloader $httpDownloader)
+    /**
+     * @param mixed[] $options Stream context options e.g. 
https://www.php.net/manual/en/context.http.php
+     */
+    public function __construct(HttpDownloader $httpDownloader, array $options 
= [])
     {
         $this->httpDownloader = $httpDownloader;
+        $this->options = $options;
     }
 
     /**
@@ -47,8 +53,11 @@
             'lists' => $configuredLists,
         ];
 
-        $options = [];
+        $options = $this->options;
         $options['http']['method'] = 'POST';
+        if (isset($options['http']['header'])) {
+            $options['http']['header'] = (array) $options['http']['header'];
+        }
         $options['http']['header'][] = 'Content-type: application/json';
         $options['http']['timeout'] = 10;
         $options['http']['content'] = json_encode($body);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Installer/BinaryInstaller.php 
new/composer-2.10.3/src/Composer/Installer/BinaryInstaller.php
--- old/composer-2.10.2/src/Composer/Installer/BinaryInstaller.php      
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Installer/BinaryInstaller.php      
2026-08-27 13:34:23.000000000 +0200
@@ -68,6 +68,13 @@
                 $this->io->writeError('    <warning>Skipped installation of 
bin '.$bin.' for package '.$package->getName().': found a directory at that 
path</warning>');
                 continue;
             }
+            // A malicious package can pass the ".." bin metadata check yet 
ship the bin as a symlink
+            // pointing outside the package (e.g. to ../../../victim.sh), 
following it here would let
+            // the package chmod/proxy an arbitrary host file 
(GHSA-96h3-5x6v-m776).
+            if (!self::isBinPathInsidePackage($installPath, $binPath)) {
+                $this->io->writeError('    <warning>Skipped installation of 
bin '.$bin.' for package '.$package->getName().': the bin resolves to a path 
outside of the package directory</warning>');
+                continue;
+            }
             if (!$this->filesystem->isAbsolutePath($binPath)) {
                 // in case a custom installer returned a relative path for the
                 // $package, we can now safely turn it into a absolute path 
(as we
@@ -145,6 +152,26 @@
     }
 
     /**
+     * Checks that a bin file resolves to a path inside the package's own 
install directory
+     *
+     * A bin escaping the package, either via ".." metadata or by being a 
symlink pointing out of it,
+     * would let the package chmod/proxy an arbitrary host file, see 
GHSA-gjfg-22fp-rrxx and
+     * GHSA-96h3-5x6v-m776.
+     */
+    public static function isBinPathInsidePackage(string $installPath, string 
$binPath): bool
+    {
+        $realBinPath = realpath($binPath);
+        $realInstallPath = realpath($installPath);
+
+        // fail closed if either path cannot be resolved
+        if (false === $realBinPath || false === $realInstallPath) {
+            return false;
+        }
+
+        return strpos($realBinPath, $realInstallPath.DIRECTORY_SEPARATOR) === 
0;
+    }
+
+    /**
      * @return string[]
      */
     protected function getBinaries(PackageInterface $package): array
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Package/Loader/ValidatingArrayLoader.php 
new/composer-2.10.3/src/Composer/Package/Loader/ValidatingArrayLoader.php
--- old/composer-2.10.2/src/Composer/Package/Loader/ValidatingArrayLoader.php   
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Package/Loader/ValidatingArrayLoader.php   
2026-08-27 13:34:23.000000000 +0200
@@ -23,6 +23,7 @@
 use Composer\Semver\Constraint\MatchNoneConstraint;
 use Composer\Semver\Intervals;
 use Composer\Spdx\SpdxLicenses;
+use Composer\Util\Perforce;
 
 /**
  * @author Jordi Boggiano <[email protected]>
@@ -551,6 +552,14 @@
                 if (isset($this->config[$srcType]['url']) && 
Preg::isMatch('{^\s*-}', (string) $this->config[$srcType]['url'])) {
                     $this->errors[] = $srcType . '.url : must not start with a 
"-", "'.$this->config[$srcType]['url'].'" given';
                 }
+                // a perforce url is passed to the p4 client as P4PORT, where 
rsh:/jsh: endpoints
+                // mean "run this command locally" (GHSA-rvx4-ffvw-m9q3)
+                if ($srcType === 'source' && ($this->config[$srcType]['type'] 
?? null) === 'perforce'
+                    && isset($this->config[$srcType]['url']) && 
is_string($this->config[$srcType]['url'])
+                    && !Perforce::isValidPort($this->config[$srcType]['url'])
+                ) {
+                    $this->errors[] = $srcType . '.url : invalid Perforce port 
("'.$this->config[$srcType]['url'].'"), it must be of the form 
[tcp|ssl:][host:]port';
+                }
             }
         }
 
@@ -700,6 +709,14 @@
             }
         }
 
+        // A perforce source.url ends up as the p4 client's P4PORT, and a 
"rsh:"/"jsh:" endpoint
+        // there makes the client execute the rest of the value as a local 
command instead of
+        // connecting to a server (GHSA-rvx4-ffvw-m9q3), so only accept 
network endpoints.
+        $sourceUrl = $package->getSourceUrl();
+        if ($package->getSourceType() === 'perforce' && $sourceUrl !== null && 
!Perforce::isValidPort($sourceUrl)) {
+            throw new SecurityException($package->getName().' has an invalid 
source.url, it must be a Perforce port of the form [tcp|ssl:][host:]port: 
'.$sourceUrl);
+        }
+
         // Bin paths are resolved relative to the package install dir and then 
chmod'd (and
         // proxied) by BinaryInstaller. A ".." segment escapes that directory 
and lets a
         // dependency chmod/point at an arbitrary host file 
(GHSA-gjfg-22fp-rrxx), so reject it.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Repository/ArrayRepository.php 
new/composer-2.10.3/src/Composer/Repository/ArrayRepository.php
--- old/composer-2.10.2/src/Composer/Repository/ArrayRepository.php     
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Repository/ArrayRepository.php     
2026-08-27 13:34:23.000000000 +0200
@@ -71,10 +71,10 @@
                     && 
!isset($alreadyLoaded[$package->getName()][$package->getVersion()])
                 ) {
                     // add selected packages which match stability requirements
-                    $result[spl_object_hash($package)] = $package;
+                    $result[spl_object_id($package)] = $package;
                     // add the aliased package for packages where the alias 
matches
-                    if ($package instanceof AliasPackage && 
!isset($result[spl_object_hash($package->getAliasOf())])) {
-                        $result[spl_object_hash($package->getAliasOf())] = 
$package->getAliasOf();
+                    if ($package instanceof AliasPackage && 
!isset($result[spl_object_id($package->getAliasOf())])) {
+                        $result[spl_object_id($package->getAliasOf())] = 
$package->getAliasOf();
                     }
                 }
 
@@ -85,8 +85,8 @@
         // add aliases of packages that were selected, even if the aliases did 
not match
         foreach ($packages as $package) {
             if ($package instanceof AliasPackage) {
-                if (isset($result[spl_object_hash($package->getAliasOf())])) {
-                    $result[spl_object_hash($package)] = $package;
+                if (isset($result[spl_object_id($package->getAliasOf())])) {
+                    $result[spl_object_id($package)] = $package;
                 }
             }
         }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Repository/ComposerRepository.php 
new/composer-2.10.3/src/Composer/Repository/ComposerRepository.php
--- old/composer-2.10.2/src/Composer/Repository/ComposerRepository.php  
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Repository/ComposerRepository.php  
2026-08-27 13:34:23.000000000 +0200
@@ -573,9 +573,9 @@
                     $namesFound[$name] = true;
 
                     if (!$constraint || $constraint->matches(new 
Constraint('==', $candidate->getVersion()))) {
-                        $matches[spl_object_hash($candidate)] = $candidate;
-                        if ($candidate instanceof AliasPackage && 
!isset($matches[spl_object_hash($candidate->getAliasOf())])) {
-                            
$matches[spl_object_hash($candidate->getAliasOf())] = $candidate->getAliasOf();
+                        $matches[spl_object_id($candidate)] = $candidate;
+                        if ($candidate instanceof AliasPackage && 
!isset($matches[spl_object_id($candidate->getAliasOf())])) {
+                            $matches[spl_object_id($candidate->getAliasOf())] 
= $candidate->getAliasOf();
                         }
                     }
                 }
@@ -583,8 +583,8 @@
                 // add aliases of matched packages even if they did not match 
the constraint
                 foreach ($candidates as $candidate) {
                     if ($candidate instanceof AliasPackage) {
-                        if 
(isset($matches[spl_object_hash($candidate->getAliasOf())])) {
-                            $matches[spl_object_hash($candidate)] = $candidate;
+                        if 
(isset($matches[spl_object_id($candidate->getAliasOf())])) {
+                            $matches[spl_object_id($candidate)] = $candidate;
                         }
                     }
                 }
@@ -911,7 +911,7 @@
     private function getFilterApiClient(): FilterListApiClient
     {
         if ($this->filterApiClient === null) {
-            $this->filterApiClient = new 
FilterListApiClient($this->httpDownloader);
+            $this->filterApiClient = new 
FilterListApiClient($this->httpDownloader, $this->options);
         }
 
         return $this->filterApiClient;
@@ -1275,7 +1275,7 @@
      * @phpstan-param array<string, BasePackage::STABILITY_*>|null 
$stabilityFlags
      * @param array<string, array<string, PackageInterface>> $alreadyLoaded
      *
-     * @return array{namesFound: array<string, true>, packages: array<string, 
BasePackage>}
+     * @return array{namesFound: array<string, true>, packages: array<int, 
BasePackage>}
      */
     private function loadAsyncPackages(array $packageNames, ?array 
$acceptableStabilities = null, ?array $stabilityFlags = null, array 
$alreadyLoaded = []): array
     {
@@ -1345,11 +1345,11 @@
                     $loadedPackages = $this->createPackages($versionsToLoad, 
$packagesSource);
                     foreach ($loadedPackages as $package) {
                         $package->setRepository($this);
-                        $packages[spl_object_hash($package)] = $package;
+                        $packages[spl_object_id($package)] = $package;
 
-                        if ($package instanceof AliasPackage && 
!isset($packages[spl_object_hash($package->getAliasOf())])) {
+                        if ($package instanceof AliasPackage && 
!isset($packages[spl_object_id($package->getAliasOf())])) {
                             $package->getAliasOf()->setRepository($this);
-                            $packages[spl_object_hash($package->getAliasOf())] 
= $package->getAliasOf();
+                            $packages[spl_object_id($package->getAliasOf())] = 
$package->getAliasOf();
                         }
                     }
                 });
@@ -1775,7 +1775,7 @@
                     }
 
                     // TODO use scarier wording once we know for sure it 
doesn't do false positives anymore
-                    throw new RepositorySecurityException('The contents of 
'.$filename.' do not match its signature. This could indicate a 
man-in-the-middle attack or e.g. antivirus software corrupting files. Try 
running composer again and report this if you think it is a mistake.');
+                    throw new RepositorySecurityException('The contents of 
'.Url::sanitize($filename).' do not match its signature. This could indicate a 
man-in-the-middle attack or e.g. antivirus software corrupting files. Try 
running composer again and report this if you think it is a mistake.');
                 }
 
                 if ($this->eventDispatcher) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Repository/PlatformRepository.php 
new/composer-2.10.3/src/Composer/Repository/PlatformRepository.php
--- old/composer-2.10.2/src/Composer/Repository/PlatformRepository.php  
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Repository/PlatformRepository.php  
2026-08-27 13:34:23.000000000 +0200
@@ -234,7 +234,8 @@
                     $info = $this->runtime->getExtensionInfo($name);
 
                     // SSL Version => OpenSSL/1.0.1t
-                    if (Preg::isMatchStrictGroups('{^SSL Version => 
(?<library>[^/]+)/(?<version>.+)$}im', $info, $sslMatches)) {
+                    // unversioned backends (=> Schannel) are not reported, 
the library must not span lines (#12615)
+                    if (Preg::isMatchStrictGroups('{^SSL Version => 
(?<library>[^\r\n/]+)/(?<version>[^\r\n]+?)\r?$}im', $info, $sslMatches)) {
                         $library = strtolower($sslMatches['library']);
                         if ($library === 'openssl') {
                             $parsedVersion = 
Version::parseOpenssl($sslMatches['version'], $isFips);
@@ -252,7 +253,7 @@
                     }
 
                     // libSSH Version => libssh2/1.4.3
-                    if (Preg::isMatchStrictGroups('{^libSSH Version => 
(?<library>[^/]+)/(?<version>.+?)(?:/.*)?$}im', $info, $sshMatches)) {
+                    if (Preg::isMatchStrictGroups('{^libSSH Version => 
(?<library>[^\r\n/]+)/(?<version>.+?)(?:/.*)?$}im', $info, $sshMatches)) {
                         $this->addLibrary($libraries, 
$name.'-'.strtolower($sshMatches['library']), $sshMatches['version'], 'curl 
'.$sshMatches['library'].' version');
                     }
 
@@ -393,8 +394,8 @@
                         $this->addLibrary($libraries, $name.'-libmbfl', 
$libmbflMatches['version'], 'mbstring libmbfl version');
                     }
 
-                    if ($this->runtime->hasConstant('MB_ONIGURUMA_VERSION')) {
-                        $this->addLibrary($libraries, $name.'-oniguruma', 
$this->runtime->getConstant('MB_ONIGURUMA_VERSION'), 'mbstring oniguruma 
version');
+                    if (\PHP_VERSION_ID < 90000 && 
$this->runtime->hasConstant('MB_ONIGURUMA_VERSION')) {
+                        $this->addLibrary($libraries, $name.'-oniguruma', 
Silencer::call([$this->runtime, 'getConstant'], 'MB_ONIGURUMA_VERSION'), 
'mbstring oniguruma version');
 
                     // Multibyte regex (oniguruma) version => 5.9.5
                     // oniguruma version => 6.9.0
@@ -700,6 +701,12 @@
             return;
         }
 
+        // a parsing glitch in one of the extension info parsers above must 
not result in a bogus
+        // package name, as that would then be looked up as if it was a real 
package, see #12615
+        if (!self::isPlatformPackage('lib-'.$name)) {
+            return;
+        }
+
         // avoid adding the same lib twice even if two conflicting extensions 
provide the same lib
         // see https://github.com/composer/composer/issues/12082
         if (isset($libraries['lib-'.$name])) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Repository/Vcs/ForgejoDriver.php 
new/composer-2.10.3/src/Composer/Repository/Vcs/ForgejoDriver.php
--- old/composer-2.10.2/src/Composer/Repository/Vcs/ForgejoDriver.php   
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Repository/Vcs/ForgejoDriver.php   
2026-08-27 13:34:23.000000000 +0200
@@ -58,6 +58,10 @@
         $resource = $this->forgejoUrl->apiUrl.'/contents/' . $file . 
'?ref='.urlencode($identifier);
         $resource = $this->getContents($resource)->decodeJson();
 
+        if ($resource === []) {
+            return '[]';
+        }
+
         // The Forgejo contents API only returns files up to 1MB as base64 
encoded files
         // larger files either need be fetched with a raw accept header or by 
using the git blob endpoint
         if ((!isset($resource['content']) || $resource['content'] === '') && 
$resource['encoding'] === 'none' && isset($resource['git_url'])) {
@@ -105,6 +109,9 @@
             do {
                 $response = $this->getContents($resource);
                 $branchData = $response->decodeJson();
+                if ($branchData === null) {
+                    break;
+                }
                 foreach ($branchData as $branch) {
                     $branches[$branch['name']] = $branch['commit']['id'];
                 }
@@ -130,6 +137,9 @@
             do {
                 $response = $this->getContents($resource);
                 $tagsData = $response->decodeJson();
+                if ($tagsData === null) {
+                    break;
+                }
                 foreach ($tagsData as $tag) {
                     $tags[$tag['name']] = $tag['commit']['sha'];
                 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/Filesystem.php 
new/composer-2.10.3/src/Composer/Util/Filesystem.php
--- old/composer-2.10.2/src/Composer/Util/Filesystem.php        2026-07-01 
11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Util/Filesystem.php        2026-08-27 
13:34:23.000000000 +0200
@@ -258,6 +258,12 @@
             }
 
             if (!@mkdir($directory, 0777, true)) {
+                // maybe another process created it since we checked above?
+                clearstatcache();
+                if (is_dir($directory)) {
+                    return;
+                }
+
                 $e = new \RuntimeException($directory.' does not exist and 
could not be created: '.(error_get_last()['message'] ?? ''));
 
                 // in pathological cases with paths like 
path/to/broken-symlink/../foo is_dir will fail to detect path/to/foo
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/Git.php 
new/composer-2.10.3/src/Composer/Util/Git.php
--- old/composer-2.10.2/src/Composer/Util/Git.php       2026-07-01 
11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Util/Git.php       2026-08-27 
13:34:23.000000000 +0200
@@ -81,7 +81,7 @@
             $callables[] = static function (string $url) use ($cmd): array {
                 $map = [
                     '%url%' => $url,
-                    '%sanitizedUrl%' => Preg::replace('{://([^@]+?):(.+?)@}', 
'://', $url),
+                    '%sanitizedUrl%' => Url::stripCredentials($url),
                 ];
 
                 return array_map(static function ($value) use ($map): string {
@@ -573,10 +573,13 @@
                 $commands = [
                     ['git', 'remote', 'set-url', 'origin', '--', '%url%'],
                     ['git', 'remote', 'show', 'origin'],
-                    ['git', 'remote', 'set-url', 'origin', '--', 
'%sanitizedUrl%'],
                 ];
 
-                $this->runCommands($commands, $url, $dir, false, $output);
+                try {
+                    $this->runCommands($commands, $url, $dir, false, $output);
+                } finally {
+                    $this->runCommands([['git', 'remote', 'set-url', 'origin', 
'--', '%sanitizedUrl%']], $url, $dir);
+                }
             }
 
             $lines = $this->process->splitLines($output);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Util/Http/CurlDownloader.php 
new/composer-2.10.3/src/Composer/Util/Http/CurlDownloader.php
--- old/composer-2.10.2/src/Composer/Util/Http/CurlDownloader.php       
2026-07-01 11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Util/Http/CurlDownloader.php       
2026-08-27 13:34:23.000000000 +0200
@@ -432,7 +432,7 @@
                         // Gzipped responses with missing Content-Length 
header cannot be detected during the file download
                         // because $progress['size_download'] refers to the 
gzipped size downloaded, not the actual file size
                         if ($contents !== false && Platform::strlen($contents) 
>= $maxFileSize) {
-                            throw new MaxFileSizeExceededException('Maximum 
allowed download size reached. Downloaded ' . Platform::strlen($contents) . ' 
of allowed ' .  $maxFileSize . ' bytes');
+                            throw new MaxFileSizeExceededException('Maximum 
allowed download size reached. Downloaded ' . Platform::strlen($contents) . ' 
of allowed ' .  $maxFileSize . ' bytes for ' . Url::sanitize($job['url']));
                         }
                     } else {
                         $contents = stream_get_contents($job['bodyHandle']);
@@ -518,12 +518,12 @@
                 if (isset($this->jobs[$i]['options']['max_file_size'])) {
                     // Compare max_file_size with the content-length header 
this value will be -1 until the header is parsed
                     if ($this->jobs[$i]['options']['max_file_size'] < 
$progress['download_content_length']) {
-                        $this->rejectJob($this->jobs[$i], new 
MaxFileSizeExceededException('Maximum allowed download size reached. 
Content-length header indicates ' . $progress['download_content_length'] . ' 
bytes. Allowed ' .  $this->jobs[$i]['options']['max_file_size'] . ' bytes'));
+                        $this->rejectJob($this->jobs[$i], new 
MaxFileSizeExceededException('Maximum allowed download size reached. 
Content-length header indicates ' . $progress['download_content_length'] . ' 
bytes. Allowed ' .  $this->jobs[$i]['options']['max_file_size'] . ' bytes for ' 
. Url::sanitize($this->jobs[$i]['url'])));
                     }
 
                     // Compare max_file_size with the download size in bytes
                     if ($this->jobs[$i]['options']['max_file_size'] < 
$progress['size_download']) {
-                        $this->rejectJob($this->jobs[$i], new 
MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded 
' . $progress['size_download'] . ' of allowed ' .  
$this->jobs[$i]['options']['max_file_size'] . ' bytes'));
+                        $this->rejectJob($this->jobs[$i], new 
MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded 
' . $progress['size_download'] . ' of allowed ' .  
$this->jobs[$i]['options']['max_file_size'] . ' bytes for ' . 
Url::sanitize($this->jobs[$i]['url'])));
                     }
                 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/Perforce.php 
new/composer-2.10.3/src/Composer/Util/Perforce.php
--- old/composer-2.10.2/src/Composer/Util/Perforce.php  2026-07-01 
11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Util/Perforce.php  2026-08-27 
13:34:23.000000000 +0200
@@ -12,6 +12,7 @@
 
 namespace Composer\Util;
 
+use Composer\Exception\SecurityException;
 use Composer\IO\IOInterface;
 use Composer\Pcre\Preg;
 use Symfony\Component\Process\ExecutableFinder;
@@ -64,6 +65,10 @@
      */
     public function __construct($repoConfig, string $port, string $path, 
ProcessExecutor $process, bool $isWindows, IOInterface $io)
     {
+        if (!self::isValidPort($port)) {
+            throw new SecurityException('Invalid Perforce port ('.$port.'), it 
must be of the form [tcp|ssl:][host:]port');
+        }
+
         $this->windowsFlag = $isWindows;
         $this->p4Port = $port;
         $this->initializePath($path);
@@ -82,10 +87,34 @@
 
     public static function checkServerExists(string $url, ProcessExecutor 
$processExecutor): bool
     {
+        // this is a detection probe which runs against every VCS repository 
url when no driver
+        // matched yet, so an unusable port is simply "not a perforce server" 
and not an error
+        if (!self::isValidPort($url)) {
+            return false;
+        }
+
         return 0 === $processExecutor->execute(['p4', '-p', $url, 'info', 
'-s'], $ignoredOutput);
     }
 
     /**
+     * Checks that a P4PORT value is a network endpoint the p4 client can 
connect to.
+     *
+     * A `rsh:`/`jsh:` P4PORT makes the p4 client run the rest of the value as 
a local child process
+     * instead of connecting to a server, so a package-controlled source.url 
must never reach it
+     * (GHSA-rvx4-ffvw-m9q3). Only the documented `[transport:][host:]port` 
forms are accepted.
+     */
+    public static function isValidPort(string $url): bool
+    {
+        // rsh/jsh are transport keywords to p4, so "rsh:foo" never parses as 
host "rsh" port "foo"
+        // and has to be rejected before the shape check below would happily 
accept it
+        if (Preg::isMatch('{^\s*+(?:rsh|jsh)\s*+:}i', $url)) {
+            return false;
+        }
+
+        return 
Preg::isMatch('{^(?:(?:tcp|ssl)(?:4|6|46|64)?:)?(?:\[[0-9a-f:.]++\]|[a-z0-9._][a-z0-9._-]*+)(?::[a-z0-9._][a-z0-9._-]*+)?$}iD',
 $url);
+    }
+
+    /**
      * @phpstan-param RepoConfig $repoConfig
      */
     public function initialize($repoConfig): void
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Util/ProcessExecutor.php 
new/composer-2.10.3/src/Composer/Util/ProcessExecutor.php
--- old/composer-2.10.2/src/Composer/Util/ProcessExecutor.php   2026-07-01 
11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Util/ProcessExecutor.php   2026-08-27 
13:34:23.000000000 +0200
@@ -482,14 +482,7 @@
         }
 
         $commandString = is_string($command) ? $command : implode(' ', 
array_map(self::class.'::escape', $command));
-        $safeCommand = 
Preg::replaceCallback('{://(?P<user>[^:/\s@]+)(?::(?P<password>[^@\s/]+))?@}i', 
static function ($m): string {
-            $user = Url::sanitizeUsername($m['user']);
-            if (($m['password'] ?? '') !== '') {
-                return '://'.$user.':***@';
-            }
-
-            return '://'.$user.'@';
-        }, $commandString);
+        $safeCommand = Url::sanitize($commandString);
         $safeCommand = Preg::replace("{--password (.*[^\\\\]\') }", 
'--password \'***\' ', $safeCommand);
         $this->io->writeError('Executing'.($async ? ' async' : '').' command 
('.($cwd ?: 'CWD').'): '.$safeCommand);
     }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/composer-2.10.2/src/Composer/Util/RemoteFilesystem.php 
new/composer-2.10.3/src/Composer/Util/RemoteFilesystem.php
--- old/composer-2.10.2/src/Composer/Util/RemoteFilesystem.php  2026-07-01 
11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Util/RemoteFilesystem.php  2026-08-27 
13:34:23.000000000 +0200
@@ -320,7 +320,7 @@
             $contentLength = !empty($http_response_header[0]) ? 
Response::findHeaderValue($http_response_header, 'content-length') : null;
             if ($contentLength && Platform::strlen($result) < $contentLength) {
                 // alas, this is not possible via the stream callback because 
STREAM_NOTIFY_COMPLETED is documented, but not implemented anywhere in PHP
-                $e = new TransportException('Content-Length mismatch, received 
'.Platform::strlen($result).' bytes out of the expected '.$contentLength);
+                $e = new TransportException('Content-Length mismatch, received 
'.Platform::strlen($result).' bytes out of the expected '.$contentLength.' for 
'.Url::sanitize($fileUrl));
                 $e->setHeaders($http_response_header);
                 $e->setStatusCode(self::findStatusCode($http_response_header));
                 try {
@@ -537,7 +537,7 @@
         }
 
         if ($result !== false && $maxFileSize !== null && 
Platform::strlen($result) >= $maxFileSize) {
-            throw new MaxFileSizeExceededException('Maximum allowed download 
size reached. Downloaded ' . Platform::strlen($result) . ' of allowed ' .  
$maxFileSize . ' bytes');
+            throw new MaxFileSizeExceededException('Maximum allowed download 
size reached. Downloaded ' . Platform::strlen($result) . ' of allowed ' .  
$maxFileSize . ' bytes for ' . Url::sanitize($fileUrl));
         }
 
         // 
https://www.php.net/manual/en/reserved.variables.httpresponseheader.php
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/Url.php 
new/composer-2.10.3/src/Composer/Util/Url.php
--- old/composer-2.10.2/src/Composer/Util/Url.php       2026-07-01 
11:24:45.000000000 +0200
+++ new/composer-2.10.3/src/Composer/Util/Url.php       2026-08-27 
13:34:23.000000000 +0200
@@ -103,7 +103,9 @@
             && !in_array($origin, $config->get('gitlab-domains'), true)
         ) {
             foreach ($config->get('gitlab-domains') as $gitlabDomain) {
-                if ($gitlabDomain !== '' && str_starts_with($gitlabDomain, 
$origin)) {
+                // configured domains may spell out a port the URL omits, see 
GitLab::authorizeOAuth
+                $bcDomain = Preg::replace('{^([^/]+):\d+}', '$1', 
$gitlabDomain);
+                if ($gitlabDomain !== '' && ($bcDomain === $origin || 
str_starts_with($bcDomain, $origin.'/'))) {
                     return $gitlabDomain;
                 }
             }
@@ -131,7 +133,9 @@
         // e.g. 
https://api.github.com/repositories/9999999999?access_token=github_token
         $url = Preg::replace('{([&?]access_token=)[^&]+}', '$1***', $url);
 
-        $url = 
Preg::replaceCallback('{^(?P<prefix>[a-z0-9]+://)?(?P<user>[^:/\s@]+)(?::(?P<password>[^@\s/]+))?@}i',
 static function ($m): string {
+        // matches every scheme://user:pass@ in the string as URLs are usually 
embedded in a longer
+        // message, plus a scheme-less one at the very start for URLs passed 
in on their own
+        $url = 
Preg::replaceCallback('{(?:(?P<prefix>[a-z0-9][a-z0-9+.-]*://)|\A)(?P<user>[^:/\s?#]*)(?::(?P<password>[^\s/?#]+))?@}i',
 static function ($m): string {
             $user = self::sanitizeUsername($m['user']);
             if (($m['password'] ?? '') !== '') {
                 return $m['prefix'].$user.':***@';
@@ -144,6 +148,16 @@
     }
 
     /**
+     * Removes the credentials from a URL entirely, for URLs which get 
persisted somewhere (e.g. in a git remote)
+     *
+     * Unlike sanitize() this also drops the username, as a bare token in the 
user slot is a credential too.
+     */
+    public static function stripCredentials(string $url): string
+    {
+        return Preg::replace('{://[^/\s?#]+@}', '://', $url);
+    }
+
+    /**
      * Returns a display-safe version of the user/token part of a URL's 
userinfo.
      *
      * Modern GitHub tokens (e.g. ghp_xxx, github_pat_xxx) and any other long 
(12char+) value keep

++++++ composer.phar ++++++
Binary files /var/tmp/diff_new_pack.EetwlI/_old and 
/var/tmp/diff_new_pack.EetwlI/_new differ

++++++ composer.phar.asc ++++++
Binary files /var/tmp/diff_new_pack.EetwlI/_old and 
/var/tmp/diff_new_pack.EetwlI/_new differ

Reply via email to