Fix clearing set-uid and set-gid bits on a file when replying a write

POSIX requires that set-uid and set-gid bits to be removed when an
unprivileged user writes to a file and ZFS does that during normal
operation.

The problem arrises when the write is stored in the ZIL and replayed.
During replay we have no access to original credentials of the process
doing the write, so zfs_write() will be performed with the root
credentials. When root is doing the write set-uid and set-gid bits
are not removed from the file.

To correct that, log a separate TX_SETATTR entry that removed those bits
on first write to such file.

Idea from:	Christian Schwarz

Add test for ZIL replay of setuid/setgid clearing.

Improve various edge cases when clearing setid bits:
- The setid bits can be readded during a single write, so make sure to check
  for them on every chunk write.
- Log TX_SETATTR record at most once per transaction group (if the setid bits
  are keep coming back).
- Move zfs_log_setattr() outside of zp->z_acl_lock.

Reviewed-by: Dan McDonald <danmcd@joyent.com>
Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Co-authored-by: Christian Schwarz <me@cschwarz.com>
Signed-off-by: Pawel Jakub Dawidek <pawel@dawidek.net>
Closes #13027
This commit is contained in:
Pawel Jakub Dawidek
2022-02-03 14:37:57 -08:00
committed by Tony Hutter
parent 9221ff1888
commit 3e27b589cf
9 changed files with 261 additions and 104 deletions
@@ -7,6 +7,7 @@ dist_pkgdata_SCRIPTS = \
suid_write_to_sgid.ksh \
suid_write_to_suid_sgid.ksh \
suid_write_to_none.ksh \
suid_write_zil_replay.ksh \
cleanup.ksh \
setup.ksh
@@ -29,86 +29,16 @@
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
static void
test_stat_mode(mode_t extra)
{
struct stat st;
int i, fd;
char fpath[1024];
char *penv[] = {"TESTDIR", "TESTFILE0"};
char buf[] = "test";
mode_t res;
mode_t mode = 0777 | extra;
/*
* Get the environment variable values.
*/
for (i = 0; i < sizeof (penv) / sizeof (char *); i++) {
if ((penv[i] = getenv(penv[i])) == NULL) {
fprintf(stderr, "getenv(penv[%d])\n", i);
exit(1);
}
}
umask(0);
if (stat(penv[0], &st) == -1 && mkdir(penv[0], mode) == -1) {
perror("mkdir");
exit(2);
}
snprintf(fpath, sizeof (fpath), "%s/%s", penv[0], penv[1]);
unlink(fpath);
if (stat(fpath, &st) == 0) {
fprintf(stderr, "%s exists\n", fpath);
exit(3);
}
fd = creat(fpath, mode);
if (fd == -1) {
perror("creat");
exit(4);
}
close(fd);
if (setuid(65534) == -1) {
perror("setuid");
exit(5);
}
fd = open(fpath, O_RDWR);
if (fd == -1) {
perror("open");
exit(6);
}
if (write(fd, buf, sizeof (buf)) == -1) {
perror("write");
exit(7);
}
close(fd);
if (stat(fpath, &st) == -1) {
perror("stat");
exit(8);
}
unlink(fpath);
/* Verify SUID/SGID are dropped */
res = st.st_mode & (0777 | S_ISUID | S_ISGID);
if (res != (mode & 0777)) {
fprintf(stderr, "stat(2) %o\n", res);
exit(9);
}
}
#include <stdbool.h>
int
main(int argc, char *argv[])
{
const char *name;
const char *name, *phase;
mode_t extra;
struct stat st;
if (argc < 2) {
if (argc < 3) {
fprintf(stderr, "Invalid argc\n");
exit(1);
}
@@ -127,7 +57,77 @@ main(int argc, char *argv[])
exit(1);
}
test_stat_mode(extra);
const char *testdir = getenv("TESTDIR");
if (!testdir) {
fprintf(stderr, "getenv(TESTDIR)\n");
exit(1);
}
umask(0);
if (stat(testdir, &st) == -1 && mkdir(testdir, 0777) == -1) {
perror("mkdir");
exit(2);
}
char fpath[1024];
snprintf(fpath, sizeof (fpath), "%s/%s", testdir, name);
phase = argv[2];
if (strcmp(phase, "PRECRASH") == 0) {
/* clean up last run */
unlink(fpath);
if (stat(fpath, &st) == 0) {
fprintf(stderr, "%s exists\n", fpath);
exit(3);
}
int fd;
fd = creat(fpath, 0777 | extra);
if (fd == -1) {
perror("creat");
exit(4);
}
close(fd);
if (setuid(65534) == -1) {
perror("setuid");
exit(5);
}
fd = open(fpath, O_RDWR);
if (fd == -1) {
perror("open");
exit(6);
}
const char buf[] = "test";
if (write(fd, buf, sizeof (buf)) == -1) {
perror("write");
exit(7);
}
close(fd);
} else if (strcmp(phase, "REPLAY") == 0) {
/* created in PRECRASH run */
} else {
fprintf(stderr, "Invalid phase %s\n", phase);
exit(1);
}
if (stat(fpath, &st) == -1) {
perror("stat");
exit(8);
}
/* Verify SUID/SGID are dropped */
mode_t res = st.st_mode & (0777 | S_ISUID | S_ISGID);
if (res != 0777) {
fprintf(stderr, "stat(2) %o\n", res);
exit(9);
}
return (0);
}
@@ -47,6 +47,6 @@ function cleanup
log_onexit cleanup
log_note "Verify write(2) to regular file by non-owner"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "NONE"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "NONE" "PRECRASH"
log_pass "Verify write(2) to regular file by non-owner passed"
@@ -47,6 +47,6 @@ function cleanup
log_onexit cleanup
log_note "Verify write(2) to SGID file by non-owner"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SGID"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SGID" "PRECRASH"
log_pass "Verify write(2) to SGID file by non-owner passed"
@@ -47,6 +47,6 @@ function cleanup
log_onexit cleanup
log_note "Verify write(2) to SUID file by non-owner"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SUID"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SUID" "PRECRASH"
log_pass "Verify write(2) to SUID file by non-owner passed"
@@ -47,6 +47,6 @@ function cleanup
log_onexit cleanup
log_note "Verify write(2) to SUID/SGID file by non-owner"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SUID_SGID"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SUID_SGID" "PRECRASH"
log_pass "Verify write(2) to SUID/SGID file by non-owner passed"
@@ -0,0 +1,99 @@
#!/bin/ksh -p
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
# Copyright 2007 Sun Microsystems, Inc. All rights reserved.
# Use is subject to license terms.
#
. $STF_SUITE/tests/functional/slog/slog.kshlib
verify_runnable "global"
function cleanup_fs
{
cleanup
}
log_assert "Verify ZIL replay results in correct SUID/SGID bits for unprivileged write to SUID/SGID files"
log_onexit cleanup_fs
log_must setup
#
# 1. Create a file system (TESTFS)
#
log_must zpool destroy "$TESTPOOL"
log_must zpool create $TESTPOOL $VDEV log mirror $LDEV
log_must zfs set compression=on $TESTPOOL
log_must zfs create -o mountpoint="$TESTDIR" $TESTPOOL/$TESTFS
# Make all the writes from suid_write_to_file.c sync
log_must zfs set sync=always "$TESTPOOL/$TESTFS"
#
# This dd command works around an issue where ZIL records aren't created
# after freezing the pool unless a ZIL header already exists. Create a file
# synchronously to force ZFS to write one out.
#
log_must dd if=/dev/zero of=$TESTDIR/sync \
conv=fdatasync,fsync bs=1 count=1
#
# 2. Freeze TESTFS
#
log_must zpool freeze $TESTPOOL
#
# 3. Unprivileged write to a setuid file
#
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "NONE" "PRECRASH"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SUID" "PRECRASH"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SGID" "PRECRASH"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SUID_SGID" "PRECRASH"
#
# 4. Unmount filesystem and export the pool
#
# At this stage TESTFS is empty again and frozen, the intent log contains
# a complete set of deltas to replay.
#
log_must zfs unmount $TESTPOOL/$TESTFS
log_note "List transactions to replay:"
log_must zdb -iv $TESTPOOL/$TESTFS
log_must zpool export $TESTPOOL
#
# 5. Remount TESTFS <which replays the intent log>
#
# Import the pool to unfreeze it and claim log blocks. It has to be
# `zpool import -f` because we can't write a frozen pool's labels!
#
log_must zpool import -f -d $VDIR $TESTPOOL
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "NONE" "REPLAY"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SUID" "REPLAY"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SGID" "REPLAY"
log_must $STF_SUITE/tests/functional/suid/suid_write_to_file "SUID_SGID" "REPLAY"
log_pass