Skip to content
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

Add fchmodat fallback on Linux when flags is nonzero. #17954

Closed
wants to merge 4 commits into from
Closed

Add fchmodat fallback on Linux when flags is nonzero. #17954

wants to merge 4 commits into from

Conversation

The-King-of-Toasters
Copy link
Contributor

After updating the syscall list, I researched why fchmodat2 was introduced. The fchmodat syscall doesn't take a flags argument, meaning this wrapper has been subtly broken since its introduction. This pr changes fchmodat to:

  • Use the libc implementation if linking with libc.
  • Use fchmodat if flags is zero.
  • Use fchmodat2 if available.
  • Use a workaround via procfs if all else fails.

The procfs workaround is what glibc and musl do. This pr copies the glibc logic as it requires the least amount of syscalls.

I've also replaced the fchmodat tests to check for this case and ensured they passed on Linux and other systems (FreeBSD 13, NetBSD 10).

@The-King-of-Toasters
Copy link
Contributor Author

Dang, I have no idea why this new test is flaky. This shouldn't even throw an error given that we're not chmoding a regular file:

    try os.fchmodat(tmp.dir.fd, "regfile", 0o600, os.AT.SYMLINK_NOFOLLOW);

@The-King-of-Toasters
Copy link
Contributor Author

The-King-of-Toasters commented Nov 15, 2023

I think I understand what's happening. CI is failing when linking with glibc, and the version being used is 2.31. Looking at the fchmodat impl, it's easy to see what's wrong:

int
fchmodat (int fd, const char *file, mode_t mode, int flag)
{
  if (flag & ~AT_SYMLINK_NOFOLLOW)
    return INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);
#ifndef __NR_lchmod		/* Linux so far has no lchmod syscall.  */
  if (flag & AT_SYMLINK_NOFOLLOW)
    return INLINE_SYSCALL_ERROR_RETURN_VALUE (ENOTSUP);
#endif

  return INLINE_SYSCALL (fchmodat, 3, fd, file, mode);
}

lchmod was never a syscall in its own right, so this branch is always taken. I guess the solution is to skip over using libc entirely, or check if the glibc version is known to be bad (2.32 was the first to include the workaround)

Copy link
Contributor

@rootbeer rootbeer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got some minor comments and suggestions. Looks like a good direction to me. That said, this is just a drive-by review, I'm not a Zig maintainer or anything (so feel free to ignore me).

return syscall3(.fchmodat, @bitCast(@as(isize, fd)), @intFromPtr(path), mode);
}

pub fn fchmodat2(fd: i32, path: [*:0]const u8, mode: mode_t, flags: u32) usize {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth adding a comment about this fchmod vs the other one? (And maybe note which Linux versions they came in on?)

Comment on lines +451 to +460
// Fallback to changing permissions using procfs:
//
// 1. Open `path` as an `O.PATH` descriptor.
// 2. Stat the fd and check if it isn't a symbolic link.
// 3. Generate the procfs reference to the fd via `/proc/self/fd/{fd}`.
// 4. Pass the procfs path to `chmod` with the `mode`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe factor this implementation out into a fn fchmodat_fallback(..) function?

@The-King-of-Toasters
Copy link
Contributor Author

I've rebased this PR atop of master due to the recent Atomic changes. I'd really like someone from the core team to give a simple yea/nay on the changes since it is a correctness fix.

@The-King-of-Toasters
Copy link
Contributor Author

FYI: I authored a similar patch that was merged into Cosmopolitan a while ago. The difference was that atomic booleans aren't used, instead relying on ENOSYS being returned.

Copy link
Member

@andrewrk andrewrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, this is nice work.

I have one very specific request: please take advantage of inline to make it so that when flags is comptime-known to be 0, the fallback code is not compiled.

Inside the body of the inline function it should compute the value of use_c and then call the appropriate function.

@The-King-of-Toasters
Copy link
Contributor Author

The-King-of-Toasters commented Jan 11, 2024

I don't quite follow, do you mean to split the fallback code into an inline function, or to mark os.fchmodat inline? Since use_c is the conditional for the while loop, wouldn't this have the same effect?

@andrewrk
Copy link
Member

I was going to link you to the relevant section of the language reference but I realized that the section was missing, so I created it: aba8d4f

Hopefully it should be clear after reading these new docs.

@The-King-of-Toasters
Copy link
Contributor Author

Hopefully it should be clear after reading these new docs.

Not really? If fchmodat is marked inline, then use_c will be comptime true if flags == 0 and the rest of the code isn't generated. But wouldn't this needlessly bloat the binary size like you mentioned?

@andrewrk
Copy link
Member

I am not suggesting to inline all the logic, only the use_c calculation.

This syscall was added to simplify the the libc implementations of
fchmodat, as the original syscall does not take a `flags` argument.

Another syscall, `map_shadow_stack`, was also added for x86_64.
The check for determining whether to use the fallback code has been
moved into an inline function as per Andrew's comments in #17954.
Based on the Linux kernel fchmodat2 test
@The-King-of-Toasters
Copy link
Contributor Author

I think I understand now, is this acceptable?

BTW #18131 gives a similar improvement to a function used here, might be worth adding to the review pile ;).

@andrewrk
Copy link
Member

No, there is too much logic in the inline function. Do you want me to show you?

@The-King-of-Toasters
Copy link
Contributor Author

The-King-of-Toasters commented Jan 13, 2024

Yes please. Edit: Unless you mean something like:

inline fn skipFchmodatFallback(flags: u32) bool {
    return flags == 0;
}

Honestly I'm more confused. Is there a way for me to verify that either of these approaches?

andrewrk pushed a commit that referenced this pull request Jan 14, 2024
The check for determining whether to use the fallback code has been
moved into an inline function as per Andrew's comments in #17954.
@andrewrk
Copy link
Member

It looks like you've unchecked the box "allow maintainers to push commits to this branch" so I moved the PR here: #18547

Have a look and let me know if it is satisfactory

@andrewrk andrewrk closed this Jan 14, 2024
@The-King-of-Toasters The-King-of-Toasters deleted the fchmod-fixes branch January 15, 2024 08:59
bilaliscarioth pushed a commit to bilaliscarioth/zig that referenced this pull request Jan 27, 2024
The check for determining whether to use the fallback code has been
moved into an inline function as per Andrew's comments in ziglang#17954.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants