diff mbox series

[pseudo] unix: realpath fails if the resolved path doesn't exist.

Message ID 20251219140600.212527-1-ghaderer@wyplay.com
State New
Headers show
Series [pseudo] unix: realpath fails if the resolved path doesn't exist. | expand

Commit Message

Gauthier HADERER Dec. 19, 2025, 2:06 p.m. UTC
The pseudo implementation of `realpath()' may return a path which doesn't
exist. This is not POSIX compliant and causes troubles with uutils (coreutils
in Rust).

For example, the tail commands tries to determine the file path of its standard
input file descriptor by calling `realpath("/dev/fd/0")'. When the input is a
pipe, the GNU C library returns NULL but pseudo returns
`/proc/<pid>/fd/pipe:[xxxxxx]'. As it got a path, the tail command tries to
open it and fails.

Upstream-Status: Pending
---
 ports/unix/guts/realpath.c | 8 ++++++++
 1 file changed, 8 insertions(+)

Comments

Mark Hatle Dec. 22, 2025, 3:37 p.m. UTC | #1
I'm going to investigate this a bit more, but I think generally speaking this 
may be right.

Looking at the POSIX documentation for realpath, we need to set errno as well. 
I've just got to figure out what the errno needs to be.  It looks like there are 
two different errno settings, one is based on the dirname, the other the 
basename.  But I need to figure out what real glibc does in this case so we can 
mimic.

I plan to work on this today and provide more feedback as I get it.

--Mark

On 12/19/25 8:06 AM, Gauthier HADERER via lists.yoctoproject.org wrote:
> The pseudo implementation of `realpath()' may return a path which doesn't
> exist. This is not POSIX compliant and causes troubles with uutils (coreutils
> in Rust).
> 
> For example, the tail commands tries to determine the file path of its standard
> input file descriptor by calling `realpath("/dev/fd/0")'. When the input is a
> pipe, the GNU C library returns NULL but pseudo returns
> `/proc/<pid>/fd/pipe:[xxxxxx]'. As it got a path, the tail command tries to
> open it and fails.
> 
> Upstream-Status: Pending
> ---
>   ports/unix/guts/realpath.c | 8 ++++++++
>   1 file changed, 8 insertions(+)
> 
> diff --git a/ports/unix/guts/realpath.c b/ports/unix/guts/realpath.c
> index 8d8118b..ee7d409 100644
> --- a/ports/unix/guts/realpath.c
> +++ b/ports/unix/guts/realpath.c
> @@ -14,6 +14,14 @@
>   		errno = ENAMETOOLONG;
>   		return NULL;
>   	}
> +
> +	/* We must fail if the target path doesn't exist. */
> +	struct stat buf;
> +
> +	if (base_lstat(rname, &buf) == -1) {
> +		return NULL;
> +	}
> +
>   		len = strlen(rname);
>   		char *ep = rname + len - 1;
>   		while (ep > rname && *ep == '/') {
Mark Hatle Dec. 22, 2025, 5:28 p.m. UTC | #2
On 12/22/25 9:37 AM, Mark Hatle wrote:
> I'm going to investigate this a bit more, but I think generally speaking this
> may be right.
> 
> Looking at the POSIX documentation for realpath, we need to set errno as well.
> I've just got to figure out what the errno needs to be.  It looks like there are
> two different errno settings, one is based on the dirname, the other the
> basename.  But I need to figure out what real glibc does in this case so we can
> mimic.
> 
> I plan to work on this today and provide more feedback as I get it.
> 
> --Mark
> 
> On 12/19/25 8:06 AM, Gauthier HADERER via lists.yoctoproject.org wrote:
>> The pseudo implementation of `realpath()' may return a path which doesn't
>> exist. This is not POSIX compliant and causes troubles with uutils (coreutils
>> in Rust).
>>
>> For example, the tail commands tries to determine the file path of its standard
>> input file descriptor by calling `realpath("/dev/fd/0")'. When the input is a
>> pipe, the GNU C library returns NULL but pseudo returns
>> `/proc/<pid>/fd/pipe:[xxxxxx]'. As it got a path, the tail command tries to
>> open it and fails.

(without pseudo)

echo | realpath /dev/fd/0

results in

/proc/1399637/fd/pipe:[1176962683]

So the result of the pipe isn't actually wrong according to what I'm seeing.

What I am finding is realpath needs to:

Resolve <dirname>, if it doesn't exist it's NULL and an errno.

TRY to resolve the <basename>, if this doesn't exist, then we just return it as 
it is, otherwise it's resolved (if a symlink converted to the target) otherwise 
just returned 'as-is'.

So I think something else is causing an issue here.  The result from pseudo may 
still be incorrect, but it's not as simple as just checking if the file exists 
or not.  Not existing is allowed, as long as the <dirname> exists, if the file 
is a symlink that the target of the symlink itself exists, otherwise it's 
returned "as-is".

--Mark

>> Upstream-Status: Pending
>> ---
>>    ports/unix/guts/realpath.c | 8 ++++++++
>>    1 file changed, 8 insertions(+)
>>
>> diff --git a/ports/unix/guts/realpath.c b/ports/unix/guts/realpath.c
>> index 8d8118b..ee7d409 100644
>> --- a/ports/unix/guts/realpath.c
>> +++ b/ports/unix/guts/realpath.c
>> @@ -14,6 +14,14 @@
>>    		errno = ENAMETOOLONG;
>>    		return NULL;
>>    	}
>> +
>> +	/* We must fail if the target path doesn't exist. */
>> +	struct stat buf;
>> +
>> +	if (base_lstat(rname, &buf) == -1) {
>> +		return NULL;
>> +	}
>> +
>>    		len = strlen(rname);
>>    		char *ep = rname + len - 1;
>>    		while (ep > rname && *ep == '/') {
> 
> 
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#2845): https://lists.yoctoproject.org/g/yocto-patches/message/2845
> Mute This Topic: https://lists.yoctoproject.org/mt/116859482/3616948
> Group Owner: yocto-patches+owner@lists.yoctoproject.org
> Unsubscribe: https://lists.yoctoproject.org/g/yocto-patches/leave/13201099/3616948/947757854/xyzzy [mark.hatle@kernel.crashing.org]
> -=-=-=-=-=-=-=-=-=-=-=-
> 
>
Mark Hatle Dec. 22, 2025, 10:58 p.m. UTC | #3
I created a different implementation that I THINK will resolve this particular 
issue.

My concern below is that all of the test cases that I ran in Ubuntu 24.04 LTS 
showed that this changes realpath behavior.  That may include errno in some 
cases, or just no longer returning a result when the realpath on 24.04 _DOES_ 
return the full /proc/<pid>/fd/pipe:[xxxx] path.

Instead the patch that I just sent to the list for review modifies the 
psseudo_fix_path code by attempting to detect that the resolution has moved into 
the '/proc' filesystem and then stops doing further symlink resolution.  This 
appears to resolve the issue by converting /dev/fd/0 to /proc/self/fd/0 and then 
passing that into open or similar, which does work since we never try to 
directly access the pipe:[....] filename.

If you can test this/provide feedback that would be helpful!

Thank you!
--Mark

On 12/19/25 8:06 AM, Gauthier HADERER via lists.yoctoproject.org wrote:
> The pseudo implementation of `realpath()' may return a path which doesn't
> exist. This is not POSIX compliant and causes troubles with uutils (coreutils
> in Rust).
> 
> For example, the tail commands tries to determine the file path of its standard
> input file descriptor by calling `realpath("/dev/fd/0")'. When the input is a
> pipe, the GNU C library returns NULL but pseudo returns
> `/proc/<pid>/fd/pipe:[xxxxxx]'. As it got a path, the tail command tries to
> open it and fails.
> 
> Upstream-Status: Pending
> ---
>   ports/unix/guts/realpath.c | 8 ++++++++
>   1 file changed, 8 insertions(+)
> 
> diff --git a/ports/unix/guts/realpath.c b/ports/unix/guts/realpath.c
> index 8d8118b..ee7d409 100644
> --- a/ports/unix/guts/realpath.c
> +++ b/ports/unix/guts/realpath.c
> @@ -14,6 +14,14 @@
>   		errno = ENAMETOOLONG;
>   		return NULL;
>   	}
> +
> +	/* We must fail if the target path doesn't exist. */
> +	struct stat buf;
> +
> +	if (base_lstat(rname, &buf) == -1) {
> +		return NULL;
> +	}
> +
>   		len = strlen(rname);
>   		char *ep = rname + len - 1;
>   		while (ep > rname && *ep == '/') {
Gauthier HADERER Jan. 5, 2026, 1:34 p.m. UTC | #4
The `realpath()` libc function and the realpath command have a different behavior. The `realpath()` libc function will fail if the target file doesn't exist.
By default, the realpath command will happily return a path if the final component is missing. To get the same behavior as the libc function, you must pass the --canonicalize-existing option. Or you can use this simple program.

> 
> #include <limits.h>
> #include <stdlib.h>
> #include <stdio.h>
> int main(int argc, char **argv) {
> if (argc != 2)
> return 2;
> char *rpath = realpath(argv[1], NULL);
> if (!rpath) {
> perror("realpath");
> return 1;
> }
> printf("%s\n", rpath);
> free(rpath);
> return 0;
> }
>
Gauthier HADERER Jan. 8, 2026, 8:36 a.m. UTC | #5
Here's an updated version of my patch with a fix regarding the type of the stat structure.
Mark Hatle Jan. 13, 2026, 3:52 a.m. UTC | #6
Thank you for the patch.

I've merged this in a temporary branch 'fray/master', along with a test case and 
Richard's openat2 syscall work.

d3c58c2 test-syscall: Add a syscall test
ad9f32c ports/linux/pseudo_wrappers: Avoid openat2 usage via syscall
3225656 unix: realpath fails if the resolved path doesn't exist.
4e4f97a test-realpath: Verify the realpath behavior matches glibc
786d146 pseudo_util.c: Skip realpath like expansion for /proc
0922771 test: Cleanup test output

--Mark

On 1/8/26 2:36 AM, Gauthier HADERER via lists.yoctoproject.org wrote:
> Here's an updated version of my patch with a fix regarding the type of the stat 
> structure.
> _._,_._,_
> --------------------------------------------------------------------------------
> Links:
> 
> You receive all messages sent to this group.
> 
> View/Reply Online (#2913) 
> <https://lists.yoctoproject.org/g/yocto-patches/message/2913> | Reply to Group 
> <mailto:yocto-patches@lists.yoctoproject.org?subject=Re:%20Re%3A%20%5Byocto-patches%5D%20%5Bpseudo%5D%20%5BPATCH%5D%20unix%3A%20realpath%20fails%20if%20the%20resolved%20path%20doesn%27t%20exist.> | Reply to Sender <mailto:ghaderer@wyplay.com?subject=Private:%20Re:%20Re%3A%20%5Byocto-patches%5D%20%5Bpseudo%5D%20%5BPATCH%5D%20unix%3A%20realpath%20fails%20if%20the%20resolved%20path%20doesn%27t%20exist.> | Mute This Topic <https://lists.yoctoproject.org/mt/116859482/3616948> | New Topic <https://lists.yoctoproject.org/g/yocto-patches/post>
> Your Subscription 
> <https://lists.yoctoproject.org/g/yocto-patches/editsub/3616948> | Contact Group 
> Owner <mailto:yocto-patches+owner@lists.yoctoproject.org> | Unsubscribe 
> <https://lists.yoctoproject.org/g/yocto-patches/leave/13201099/3616948/947757854/xyzzy> [mark.hatle@kernel.crashing.org]
> 
> _._,_._,_
diff mbox series

Patch

diff --git a/ports/unix/guts/realpath.c b/ports/unix/guts/realpath.c
index 8d8118b..ee7d409 100644
--- a/ports/unix/guts/realpath.c
+++ b/ports/unix/guts/realpath.c
@@ -14,6 +14,14 @@ 
 		errno = ENAMETOOLONG;
 		return NULL;
 	}
+
+	/* We must fail if the target path doesn't exist. */
+	struct stat buf;
+
+	if (base_lstat(rname, &buf) == -1) {
+		return NULL;
+	}
+
 		len = strlen(rname);
 		char *ep = rname + len - 1;
 		while (ep > rname && *ep == '/') {