Bug 13288

Summary: pseudo should not follow symlinks in /proc
Product: [Yocto Project Subprojects] Pseudo Reporter: Matt Cowell <matt.cowell>
Component: pseudoAssignee: Unassigned <unassigned>
Status: RESOLVED FIXED QA Contact:
Severity: normal    
Priority: Medium+ CC: david.nystrom, paul, randy.macleod, sam, seebs, tgamblin, yp.pseudo.watcher, yp.watcher
Version: master   
Target Milestone: 5.99   
Hardware: x86   
OS: Multiple   
Whiteboard:
OS type for building Yocto: --- Type of Regression: ---
Verified: Documentation change: No (bug/feature does not impact docs)
Attachments:
Description Flags
patch to fix issue none

Description Matt Cowell 2019-04-17 23:28:53 UTC
Created attachment 4480 [details]
patch to fix issue

Some symlinks in /proc, such as those under /proc/[pid]/fd,
/proc/[pid]/cwd, and /proc/[pid]/exe that are not real and should not
have readlink called on them.  These look like symlinks, but behave like
hardlinks.  Readlink does not return actual paths.  Previously
pseudo_fix_path would expand files such as /dev/stdin to paths such as
/proc/6680/fd/pipe:[1270830076] which do not exist.

This issue affects:
- deleted files
- deleted directories
- fifos
- sockets
- anon_inodes (epoll, eventfd, inotify, signalfd, timerfd, etc)

The proposed fix is to check the filesystem type of symlinks before calling readlink(), and skip symlink expansion for that element of the path if the filesystem type is procfs.


Reproduction information:

Preconditions/Environment
-------------------------
run a shell under pseudo

Triggering Action/Cause
-----------------------
-> echo foo | cat /dev/stdin

Expectation
-----------
"foo" prints

Actual Result
-------------
"cat: /dev/stdin: No such file or directory" prints

Reproducibility
---------------
100%


Same test case with debug enabled:

Before fix:
-> echo foo | PSEUDO_DEBUG=pV cat /dev/stdin
paes: newpath , element list </dev/stdin>
element to add: ''
pae: '', + '', is_dir 1
element to add: 'dev'
pae: '', + 'dev', is_dir 1
element to add: 'stdin'
pae: '/dev', + 'stdin', is_dir 1
paes: newpath , element list </proc/self/fd/0>
element to add: ''
pae: '', + '', is_dir 1
element to add: 'proc'
pae: '', + 'proc', is_dir 1
element to add: 'self'
pae: '/proc', + 'self', is_dir 1
paes: newpath /proc, element list <6680>
element to add: '6680'
pae: '/proc', + '6680', is_dir 1
element to add: 'fd'
pae: '/proc/6680', + 'fd', is_dir 1
element to add: '0'
pae: '/proc/6680/fd', + '0', is_dir 1
paes: newpath /proc/6680/fd, element list <pipe:[1270830076]>
element to add: 'pipe:[1270830076]'
pae: '/proc/6680/fd', + 'pipe:[1270830076]', is_dir 1
<nil> + /dev/stdin => </proc/6680/fd/pipe:[1270830076]>
base_path[follow]: <nil></>/dev/stdin => /proc/6680/fd/pipe:[1270830076]
cat: /dev/stdin: No such file or directory


After fix:
-> echo foo | PSEUDO_DEBUG=pV cat /dev/stdin
paes: newpath , element list </dev/stdin>
element to add: ''
pae: '', + '', is_dir 1
element to add: 'dev'
pae: '', + 'dev', is_dir 1
element to add: 'stdin'
pae: '/dev', + 'stdin', is_dir 1
paes: newpath , element list </proc/self/fd/0>
element to add: ''
pae: '', + '', is_dir 1
element to add: 'proc'
pae: '', + 'proc', is_dir 1
element to add: 'self'
pae: '/proc', + 'self', is_dir 1
pae: '/proc/self' is procfs symlink, not expanding
element to add: 'fd'
pae: '/proc/self', + 'fd', is_dir 0
element to add: '0'
pae: '/proc/self/fd', + '0', is_dir 1
pae: '/proc/self/fd/0' is procfs symlink, not expanding
<nil> + /dev/stdin => </proc/self/fd/0>
base_path[follow]: <nil></>/dev/stdin => /proc/self/fd/0
foo


Other test cases that can reproduce:
- Using bash process substitution: cat <(echo foo)
- Accessing a deleted file under /proc/[pid]/fd/* , /proc/[pid]/exe, or similar
- possibly glibc APIs which use symlinks under /proc (there are several including fexecve).
Comment 1 Seebs 2019-08-15 15:44:20 UTC
*** Bug 13483 has been marked as a duplicate of this bug. ***
Comment 2 Sam Liddicott 2019-08-15 17:22:40 UTC
I like this solution and patch.

I don't know everything intended to achieved by pseudo following symlinks, or how much of that is relevant to /proc, but if there is a compulsion to follow the symlinks for proc, then maybe:

instead of never following a symlink on procfs, we could just avoid following the symlink whose lstat failed if it is on a procfs 

so:

lstat("/proc/6724/fd/63", {st_mode=S_IFLNK|0500, st_size=64, ...}) = 0
readlink("/proc/6724/fd/63", "pipe:[21346885]", 4096) = 15
lstat("/proc/6724/fd/pipe:[21346885]", 0x7ffe0f16e040) = -1 ENOENT (No such file or directory)

As the final lstat failed, use the pre-readlink value for open, the pre-readlink file was on a procfs
Comment 3 Matt Cowell 2019-08-15 17:51:57 UTC
> we could just avoid following the symlink whose lstat failed

I had considered this, but there are cases where the lstat in procfs can improperly succeed and dereference the wrong path, such as when namespaces are in use or if a file with the suffix " (deleted)" exists (in the case of deleted files / directories).

Mount namespace example (two processes in different namespaces):
sh-5.0# readlink /proc/27570/cwd /proc/27574/cwd
/root/foo
/root/foo
sh-5.0# tail /proc/27570/cwd/asdf /proc/27574/cwd/asdf
==> /proc/27570/cwd/asdf <==
ns1

==> /proc/27574/cwd/asdf <==
ns2


Deleted file example:
sh-5.0# echo right file > bar
sh-5.0# echo wrong file > 'bar (deleted)'
sh-5.0# exec 200<>bar
sh-5.0# rm bar
sh-5.0# realpath /proc/self/fd/200
/root/bar (deleted)
sh-5.0# cat /proc/self/fd/200
right file
sh-5.0# cat "$(realpath /proc/self/fd/200)"
wrong file
Comment 4 Randy MacLeod 2020-01-16 16:13:39 UTC
Moved to M3 but the earlier we get pseudo patches in the better.
Comment 5 Randy MacLeod 2020-08-13 10:11:28 UTC
Sakib,
Peter thinks that the patch is okay.
He's a bit concerned about the cost of checking that for every single symlink path element, but he said that he can't think of a good way to avoid it.

Please prepare a patch and on an isolated system that only you use, time builds before and after to see if there is a measureable difference. If there isn't submit the patch and CC seebs. Thanks.
Comment 6 Randy MacLeod 2021-03-24 00:23:28 UTC
ping. 3.3-M4 or has this been merged?
Comment 7 Randy MacLeod 2021-03-31 00:10:48 UTC
ping. 3.3-M4 or has this been merged?
Comment 8 Randy MacLeod 2021-04-01 16:27:34 UTC
Medium+ needs a milestone.
Comment 9 Sam Liddicott 2021-09-18 09:27:54 UTC
As it's nearly 30 months since a patch has been supplied.

Instead of thinking about merging for years on end, please could you merge and then spend years thinking about reverting the merge for the rest of time?
Comment 10 Sakib Sajal 2021-09-20 13:42:58 UTC
If its so important for you, do it yourself. Cheers! :)
Comment 11 Sam Liddicott 2021-09-20 18:18:26 UTC
What kind of response is that?

I'm running the fix I posted in duplicate https://bugzilla.yoctoproject.org/show_bug.cgi?id=13483 so don't need the fix merging.

I'm thinking of all your other users for whom /dev/fd/2 etc hasn't been working for *years*

You've time for inane disrespect for people who take the time to report bugs *and* post fixes but no time to help users when you have the fix on a plate.

As far as I can tell this bug languishes because correct behaviour might take longer than the incorrect behaviour that causes total failure for simple code that ought to work.

You think it's not that important!
Comment 12 Sakib Sajal 2021-09-20 18:45:00 UTC
My apologies if my comment seemed inappropriate/disrespectful.

I have been trying to find time to work on upstream yocto bugs but have not been able to do so. I will get on them early October.

Thank you!
Comment 13 Randy MacLeod 2022-03-24 15:56:18 UTC
We added the patch and it seemed to mostly work okay but there is at least one use case, mentioned in the revert log, that failed so the patch was reverted:
   https://git.yoctoproject.org/pseudo/commit/?id=df1d1321fb093283485c387e3c933d2d264e509c

Early in the next release, we need to understand why this happened and decide if we want to add one or more special exceptions to the patch.
Comment 14 Sam Liddicott 2022-04-01 08:00:48 UTC
I suppose it failed because /proc/self/fd/3 is obviously reported by the kernel as a symlink to /home/pokybuild/yocto-worker/beaglebone/build/build/tmp/work/beaglebone_yocto-poky-linux-gnueabi/core-image-minimal/1.0-r0/sdk-ext/image/usr/local/oe-sdk-hardcoded-buildpath/layers/build/meta/

and that resolved path was needed by pseudo in order to perform more file system mappings, and therefore we cannot simply refuse to resolve /proc symlinks

However, the basis of the reverted patch is that we can't always safely resolve /proc symlinks. 

But maybe we can...

Certainly under /proc, opening a symlink is not the same as opening readlink(symlink) because for diagnostic reasons, the kernel emits bogus symlink target which the kernel itself does not follow when opening.

Apparently, only the kernel can properly resolve symlinks under proc -- it knows when the final stage symlink is bogus. (This problem is not restricted to pseudo; pre-use readlink resolution simply cannot be trusted in /proc by any process).

But can we tell after all? And in a simple way? 

Just as surely as we need to not fail when we fail to resolve /dev/*/fd/* to pipe:[...], 
we also need a way to resolve /proc symlinks that do direct back out of the /proc file system but which may have a deleted target

I had supposed in comment 2 [https://bugzilla.yoctoproject.org/show_bug.cgi?id=13288#c2] that the last stage readlink would always fail when the last stage was bogus:
/proc/6724/fd/63 -> pipe:[21346885]
/proc/6724/fd/64 -> /tmp/file (deleted)
But it is evident from the examples in comment 3 [https://bugzilla.yoctoproject.org/show_bug.cgi?id=13288#c3] that real clashing files can exist with those names. 

What makes the difference which I failed to take into account before, is whether or not the link target is absolute, meaning it begins with a /

Real clashing/deleted files can only exist for those names if the symlink target begins with a /

If the link target does NOT begin with a / then it will not leave the /proc file system and we can safely do as I proposed in comment 2 which is:
> instead of never following a symlink on procfs, we could just avoid 
> following the symlink whose lstat failed if it is on a procfs
adding the condition: provided that it was not an absolute symlink
This way /dev/fd/3 will resolve to /proc/self/fd/3 to /proc/6724/fd/3 and stop before resolving to pipe:[21346885] whose lstat will fail. The ultimate filename /proc/6724/fd/3 will work

But if the proc symlink target were an absolute path then we have additional conditions to follow, because the issue is how to safely navigate to that exiting link (/proc/*/fd/3 -> /home/pokybuild/...) in a way that won't choke on " (deleted)" or cross-namespace links.

The absolute symlink in /proc is not like a symlink in a pseudo chroot, or a normal filesystem. It seems to refer to real final paths with any symlinks already resolved, and no need for any chroot path fixups before further symlink resolution (unlike symlinks in the chroot directories on a normal non-proc file system).

So not only must the lstat of the symlink target not fail, but the st_dev and st_ino properties must match those of a stat of the symlink, and thus we know that the symlink target exists and is the original valid target and not a new one with the same name.

Therefore (if it is likely to matter in real life) we can tell whether " (deleted)" is part of the name or not, whether or not the target symlink is valid.

If they match, then the symlink target is valid however many times " (deleted)" may appear in it due to tricksie hobbitses, and thus we have the link target that we can continue resolving, having successfully navigated outside of the /proc filesystem

Note: there are absolute symlinks in /proc which refer back to /proc, e.g.: 
* /proc/1/task/1/fd/10 -> /proc/1/mountinfo
* /proc/482/fd/11 -> /proc/sys/kernel/hostname
but that won't be a problem with these methods, /proc links just continue to get processed as /proc as described above.

Does this seem reasonable?

Cross namespace links

Although these are mentioned in comment #3 I don't know how much of real a issue this is.

But because absolute links are resolved, you won't easily find anything like /proc/2345/fd/5 -> /proc/1234/fd/2
Even if you try to open the fd of another process with 5>/proc/1234/fd/2 ultimately /proc/2345/fd/5 will point to whatever /proc/1234/fd/2 is pointing to.

There are absolute symlinks in /proc which may back to another related process, e.g.:
/proc/4170249/root -> /proc/2527499/fdinfo\ (deleted)
/proc/4170249/task/4177332/fd/37 -> /proc/4170249/status
/proc/1337266/fd/8 -> /proc/1337266/mountinfo

but I'm not certain they can be an issue. 
Maybe we can refuse to traverse links from /proc/X/ to /proc/Y/ where X != Y
Maybe where they aren't part of the same process group? Or session? Or mount namespace? Or children of a common pseudo invocation?

Do we expect such symlinks to occur in /proc where the target also needs some path massaging to avoid an abort? Where the target despite being in a different namespace is in a pseudo chroot path that this instance of pseudo is controlling and can validly massage?

Surely if it's possible it's much less likely than the situation that reverted Matt's patch or the bug fixed by the Matt's patch, and it can be addressed when it happens.

(I wish I had kept my original patch now, and I see I didn't attach it to #13483)

Simply put quit processing without error for symlinks anywhere whose target in /proc fails lstat rather than symlink sources in /proc whose target fails lstat

In /proc if lstat target succeeds and (target is not in /proc) then we add the st_ino st_dev test to confirm


step by step:

* resolve the symlink to the fully qualified target
* if lstat(target) fails
  * and the target is in /proc then return the symlink not the target and let the kernel resolve it on use (it will be pipe:[...] or something like that
  * if st_no and st_dev of stat(symlink) don't match lstat of target then return the target knowing it will fail on use
* continue processing based on the new target as input

Sam
Comment 15 Randy MacLeod 2023-06-22 15:05:31 UTC
Sakib doesn't have time to continue to test this since it failed some YP autobuilder tests. -- YP bug review
Comment 16 Trevor Gamblin 2023-09-14 15:56:32 UTC
Moved to M4.
Comment 17 Randy MacLeod 2023-10-30 15:29:09 UTC
Bulk move to 5.0 M1. -- Randy
Comment 18 Randy MacLeod 2024-01-25 14:28:45 UTC
Bulk move of bugs owned by "unassigned" from 5.0-M2 to 5.0-M3.
Comment 19 Randy MacLeod 2024-02-22 19:53:25 UTC
Bulk move of bugs from 5.0-M3 to 5.99.
The bugs in this list are all unassigned and are ones that I consider to be nice to do some time but not essential track from milestone to milestone in 5.0 or 5.1. We agreed on this bug handling approach in the YP bug review on Feb 22, 2024.

If you disagree, please move back to a 5.[012...]-M* milestone.
Comment 20 Paul Barker 2026-05-13 13:26:28 UTC
Is this resolved by commit 814e554564b2 ("pseudo_util.c: Skip realpath like expansion for /proc on Linux") in pseudo?
Comment 21 Paul Barker 2026-05-14 15:16:11 UTC
The issue was fixed as far as we could with the patch mentioned in my last comment, this doesn't exactly match the proposed fix described in earlier comments but it's the best we can reasonably do.