-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
nixos: setuid-wrapper: simplify readlink logic #35893
Conversation
f84eba5
to
a7c8ce7
Compare
Would love some additional eyes on this... @ixmatus can you take a look? |
// not positive it's safe... | ||
char selfPath[PATH_MAX]; | ||
int selfPathSize = readlink("/proc/self/exe", selfPath, sizeof(selfPath)); | ||
int selfExe = open("/proc/self/exe", O_PATH | O_CLOEXEC | O_NOFOLLOW); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand why you're opening with O_PATH | O_CLOEXEC
but I'm slightly confused by O_NOFOLLOW
which prevents opening a symbolic link, which /proc/self/exe
is, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ixmatus O_NOFOLLOW
prevents following the symlink. So when we later fstat
and readlinkat
we address the symlink itself, not the target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From open(2)
, the section on O_PATH
If pathname is a symbolic link and the O_NOFOLLOW flag is also specified, then the call returns a file descriptor referring to the symbolic link.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shlevy ah! That was the piece I was missing. Thank you for explaining.
char selfPath[PATH_MAX]; | ||
int selfPathSize = readlink("/proc/self/exe", selfPath, sizeof(selfPath)); | ||
int selfExe = open("/proc/self/exe", O_PATH | O_CLOEXEC | O_NOFOLLOW); | ||
assert(selfExe != -1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of my todos after adding setcap wrapper support that got lost in the cracks was to improve error messaging in the wrapper program. What do you think about checking the return and printing the error code to the user? Or perhaps we can do that if the debug env var is set? I think it would be useful if we did that.
Don't consider this a blocking comment, if you agree, we can address it in a refactoring-pass patch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do agree, yes. No need for a debug env var I think, the error path is allowed to be slow :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shlevy we already support a WRAPPER_DEBUG
env var in this program code, so we could use that if you felt that was a slightly nicer way to surface these wrapper-specific errors to the user.
For example, you can turn it on like this:
$ WRAPPER_DEBUG=1 ping google.com
cap_setpcap in set, skipping it
raised cap_net_raw+p into the Ambient capability set
PING google.com(iad23s42-in-x200e.1e100.net (2607:f8b0:4004:80c::200e)) 56 data bytes
64 bytes from iad23s42-in-x200e.1e100.net (2607:f8b0:4004:80c::200e): icmp_seq=1 ttl=51 time=40.1 ms
64 bytes from iad23s42-in-x200e.1e100.net (2607:f8b0:4004:80c::200e): icmp_seq=2 ttl=51 time=39.9 ms
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think WRAPPER_DEBUG
makes sense for debug info, but on the error path I don't think it's necessary. Especially since it's often too late to turn on WRAPPER_DEBUG
when you hit an issue 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that is a good point.
struct stat st; | ||
assert(fstat(selfExe, &st) != -1); | ||
size_t selfPathCap = st.st_size + 1; | ||
// Purposefully leak this, it's not going to be around for long... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "it's not going to be around for long..." you mean that the operating system will clean this up once the program exits? What is the purpose of purposefully leaking (sorry if I'm being thick)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ixmatus Our last use of selfPath
is just before execve
. If that succeeds, the memory will be freed. If it fails, it will be freed when we exit a few lines later. The only reason to explicitly call free
would be to satisfy humans checking for good coding practices, and the explicit comment is easier to find than scanning for the matching free
IMO. But happy to throw in a free
if that would be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "purposefully" bit is just to say "I know this is leaked, it's not an error" for reviewers :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shlevy oh I understand. I think being explicit with a free
would be clearer than being implicit and explaining that with a comment.
assert(selfExe != -1); | ||
struct stat st; | ||
assert(fstat(selfExe, &st) != -1); | ||
size_t selfPathCap = st.st_size + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this, I think it's better than the PATH_MAX that I was pretty unhappy about to begin with.
// contents are being truncated. | ||
assert(selfPathSize < sizeof(selfPath)); | ||
// This probably isn't possible with readlinkat following fstat, but not hard to check... | ||
assert(selfPathSize < selfPathCap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, given that we are sizing selfPathCap
correctly this code path is unlikely to be hit since we do not need to be concerned about truncation. I also think it is fine to leave the check in here.
However, would it be too much trouble to explain this a bit more in the comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ixmatus Sure, will do.
a7c8ce7
to
c32a14b
Compare
@ixmatus Updated based on feedback. |
c32a14b
to
38e85b0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shlevy has addressed my feedback and some confusion with explanations in the PR comments and additional comments in the code that I also think will help future authors understand this code.
LGTM though I recommend waiting for one more qualified reviewer before merging.
// not positive it's safe... | ||
char selfPath[PATH_MAX]; | ||
int selfPathSize = readlink("/proc/self/exe", selfPath, sizeof(selfPath)); | ||
// O_PATH | O_NOFOLLOW gives us a fd pointint to the symlink |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: s/pointint/pointing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😄 Caught this simultaneously
Thanks for taking such a detailed look! |
// between opening a symlink fd and readlinkat on it, so this is | ||
// probably not necessary. Doubly so since this is /proc/self/exe, | ||
// not a normal symlink. But it's trivial to check. | ||
assert(selfPathSize < selfPathCap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this always fail? I mean, selfPathSize
is the number of bytes put into the buffer, terminating-zero included, and selfPathCap
is the size without terminating zero, plus 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, selfPathSize
is the result of readlinkat
which also lacks the 0
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update the comments with this though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just in case there were not enough ways to start a heap walk… Missed that, yes, my bad.
38e85b0
to
dffa3d2
Compare
@GrahamcOfBorg eval |
Receiving a failure in
as of this patch. Error message is:
No further investigation has been done yet. |
Ah right, that's a kernel virtual symlink, and |
I guess starting with |
Ack. I'll just revert :( |
Given that the path is «something short» + «make it more than 255 bytes and people with ext4 will complain a lot», and |
Yeah, just wanted to be cleaner |
For every dirty hack you want to remove, there is a dirty hack in the kernel that makes your dirty hack neccessary… |
Basically, |
No description provided.