[PATCH] elf: Open the normalized $ORIGIN rpath in AT_SECURE programs (BZ 34360)

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Fri Aug 14 16:35:14 GMT 2026



On 14/08/26 10:33, Florian Weimer wrote:
> * Adhemerval Zanella:
> 
>> +/* Lexically normalize the NUL-terminated PATH in place, collapsing "//",
>> +   "/./" and "/../" segments (a leading "/../" collapses to "/").  The
>> +   normalized string is a rearrangement of a prefix of PATH: the write
>> +   cursor never runs ahead of the read cursor and no trailing character is
>> +   appended, so this only ever touches bytes within the original
>> +   strlen (PATH) + 1 storage and can never access memory out of bounds.
>> +   Returns the length of the normalized path (excluding the terminating
>> +   NUL).  */
>> +static size_t
>> +dst_normalize_path (char *path)
>>  {
>> +  char *wnp = path;
>> +  const char *rnp = path;
>> +  while (*rnp != '\0')
>>      {
>> +      if (rnp[0] == '/')
>>  	{
>> +	  /* Collapse a run of '/' to a single one before interpreting "/."
>> +	     or "/..", so that the "." or ".." is applied to the real
>> +	     preceding component rather than to an empty "//" segment: skip
>> +	     this '/' whenever it is immediately followed by another one.  */
>> +	  if (rnp[1] == '/')
>>  	    {
>> +	      ++rnp;
>> +	      continue;
>>  	    }
>>  
>> +	  if (rnp[1] == '.')
>>  	    {
>> +	      if (rnp[2] == '.' && (rnp[3] == '/' || rnp[3] == '\0'))
>> +		{
>> +		  while (wnp > path && *--wnp != '/')
>> +		    ;
>> +		  rnp += 3;
>> +		  continue;
>> +		}
>> +	      else if (rnp[2] == '/' || rnp[2] == '\0')
>> +		{
>> +		  rnp += 2;
>> +		  continue;
>> +		}
>>  	    }
>>  	}
>>  
>> +      *wnp++ = *rnp++;
>>      }
>>  
>> +  *wnp = '\0';
>> +  return wnp - path;
>> +}
> 
> This turns "/a/.." into "", which doesn't look right to me.  "a/../"
> ends up as "/", and "a/.." as "".  And of course "" remains "".  The ""
> special case probably needs to be called out in the function comment.
> The comment should also clarify the expected behavior regarding trailing
> slashes (the caller cannot assume their presence or absence).
> 
> I suggest putting this function into a separate (header) file, so that
> we can test it directly.

Indeed this approach is not fully correct, and I will move it to a different
header and add an internal testcase.

> 
> The integration test looks okay to me.
> 
>>  /* Given a substring starting at INPUT, just after the DST '$' start
>> @@ -327,6 +342,8 @@ _dl_dst_substitute (struct link_map *l, const char *input, char *result)
>>      }
>>    while (*input != '\0');
>>  
>> +  *wp = '\0';
>> +
>>    /* In SUID/SGID programs, after $ORIGIN expansion the normalized
>>       path must be rooted in one of the trusted directories.  The $LIB
>>       and $PLATFORM DST cannot in any way be manipulated by the caller
>> @@ -335,15 +352,21 @@ _dl_dst_substitute (struct link_map *l, const char *input, char *result)
>>       checked for trust, the authors of the binaries themselves are
>>       trusted to have designed this correctly.  Only $ORIGIN is tested in
>>       this way because it may be manipulated in some ways with hard
>> -     links.  */
>> -  if (__glibc_unlikely (check_for_trusted)
>> -      && !is_trusted_path_normalize (result, wp - result))
>> -    {
>> -      *result = '\0';
>> -      return result;
>> -    }
>> +     links.
>>  
>> -  *wp = '\0';
>> +     Checking the normalized path but opening the raw one is not enough:
>> +     "a/b/../c" only names "a/c" when "b" is not a symbolic link, so an
>> +     attacker who controls a component of $ORIGIN (for example by
>> +     hard-linking the program into an attacker-owned directory) could
>> +     otherwise redirect the lookup outside the trusted directory.  Replace
>> +     the expansion with its normalized, "../"-free form, so that the path
>> +     that is opened is exactly the path that was validated.  */
>> +  if (__glibc_unlikely (check_for_trusted))
>> +    {
>> +      size_t nlen = dst_normalize_path (result);
>> +      if (!path_is_trusted (result, nlen))
>> +	*result = '\0';
>> +    }
> 
> It turns out this fixes a potential buffer overflow as well.
> Previously, we called is_trusted_path_normalize on a potential
> non-null-terminated byte array.  The old loop iteration did not stop at
> the specified length, but at the first null byte.  So this could end up
> writing beyond the end of the stack-allocated array.
> 
> I've attached the full report below.
> 
> I've been instructed to mention: Found by AISLE in partnership with Red Hat
> 
> 
> I think under the current rules, these two bugs do not need separate in
> CVE assignment even though they are very different in nature.  They were
> introduced in the same commit, and as your patch shows, it's not really
> possible to fix them separately.
Indeed expand_dynamic_string_token uses buffer without a NULL terminator,
and it does seems a security issue. I will reserve a CVE for this one and 
user for a v2.


More information about the Libc-alpha mailing list