freebsd-skq/lib/libarchive/test/test_write_disk_perms.c
Tim Kientzle 9136384dc2 Make suid/sgid restore be "opportunistic" if
owner restore is not requested.  If you ask
for permissions to be restored but not owner,
you will now get no error if suid/sgid bits
cannot be set.  (It's a security hole to restore
suid/sgid bits if the owner/group aren't restored.)

This fixes an obscure problem where a simple
"tar -xf" with no other options will sometimes
fail gratuitously because of suid/sgid bits.
This is causing occasional problems for people
using bsdtar as a drop-in replacement for
"that other tar program." ;-)

Note: If you do ask for owner restore, then suid/sgid
restore failures still issue an error.  This
only suppresses the error in the case where an
suid/sgid bit restore fails because of an owner
mismatch and owner restore was not requested.

Approved by: re (bmah)
MFC after: 7 days
2007-08-12 17:35:05 +00:00

451 lines
16 KiB
C

/*-
* Copyright (c) 2003-2007 Tim Kientzle
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "test.h"
__FBSDID("$FreeBSD$");
#if ARCHIVE_VERSION_STAMP >= 1009000
#define UMASK 022
static long _default_gid = -1;
static long _invalid_gid = -1;
static long _alt_gid = -1;
/*
* To fully test SGID restores, we need three distinct GIDs to work
* with:
* * the GID that files are created with by default (for the
* current user in the current directory)
* * An "alt gid" that this user can create files with
* * An "invalid gid" that this user is not permitted to create
* files with.
* The second fails if this user doesn't belong to at least two groups;
* the third fails if the current user is root.
*/
static void
searchgid(void)
{
static int _searched = 0;
uid_t uid = getuid();
gid_t gid = 0;
unsigned int n;
struct stat st;
int fd;
/* If we've already looked this up, we're done. */
if (_searched)
return;
_searched = 1;
/* Create a file on disk in the current default dir. */
fd = open("test_gid", O_CREAT, 0664);
failure("Couldn't create a file for gid testing.");
assert(fd > 0);
/* See what GID it ended up with. This is our "valid" GID. */
assert(fstat(fd, &st) == 0);
_default_gid = st.st_gid;
/* Find a GID for which fchown() fails. This is our "invalid" GID. */
_invalid_gid = -1;
/* This loop stops when we wrap the gid or examine 10,000 gids. */
for (gid = 1, n = 1; gid == n && n < 10000 ; n++, gid++) {
if (fchown(fd, uid, gid) != 0) {
_invalid_gid = gid;
break;
}
}
/*
* Find a GID for which fchown() succeeds, but which isn't the
* default. This is the "alternate" gid.
*/
_alt_gid = -1;
for (gid = 0, n = 0; gid == n && n < 10000 ; n++, gid++) {
/* _alt_gid must be different than _default_gid */
if (gid == (gid_t)_default_gid)
continue;
if (fchown(fd, uid, gid) == 0) {
_alt_gid = gid;
break;
}
}
close(fd);
}
static int
altgid(void)
{
searchgid();
return (_alt_gid);
}
static int
invalidgid(void)
{
searchgid();
return (_invalid_gid);
}
static int
defaultgid(void)
{
searchgid();
return (_default_gid);
}
#endif
/*
* Exercise permission and ownership restores.
* In particular, try to exercise a bunch of border cases related
* to files/dirs that already exist, SUID/SGID bits, etc.
*/
DEFINE_TEST(test_write_disk_perms)
{
#if ARCHIVE_VERSION_STAMP < 1009000
skipping("archive_write_disk interface");
#else
struct archive *a;
struct archive_entry *ae;
struct stat st;
/*
* Set ownership of the current directory to the group of this
* process. Otherwise, the SGID tests below fail if the
* /tmp directory is owned by a group to which we don't belong
* and we're on a system where group ownership is inherited.
* (Because we're not allowed to SGID files with defaultgid().)
*/
assertEqualInt(0, chown(".", getuid(), getgid()));
/* Create an archive_write_disk object. */
assert((a = archive_write_disk_new()) != NULL);
/* Write a regular file to it. */
assert((ae = archive_entry_new()) != NULL);
archive_entry_copy_pathname(ae, "file_0755");
archive_entry_set_mode(ae, S_IFREG | 0777);
assert(0 == archive_write_header(a, ae));
assert(0 == archive_write_finish_entry(a));
archive_entry_free(ae);
/* Write a regular file, then write over it. */
/* For files, the perms should get updated. */
assert((ae = archive_entry_new()) != NULL);
archive_entry_copy_pathname(ae, "file_overwrite_0144");
archive_entry_set_mode(ae, S_IFREG | 0777);
assert(0 == archive_write_header(a, ae));
archive_entry_free(ae);
assert(0 == archive_write_finish_entry(a));
/* Check that file was created with different perms. */
assert(0 == stat("file_overwrite_0144", &st));
failure("file_overwrite_0144: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) != 0144);
/* Overwrite, this should change the perms. */
assert((ae = archive_entry_new()) != NULL);
archive_entry_copy_pathname(ae, "file_overwrite_0144");
archive_entry_set_mode(ae, S_IFREG | 0144);
assert(0 == archive_write_header(a, ae));
archive_entry_free(ae);
assert(0 == archive_write_finish_entry(a));
/* Write a regular dir. */
assert((ae = archive_entry_new()) != NULL);
archive_entry_copy_pathname(ae, "dir_0514");
archive_entry_set_mode(ae, S_IFDIR | 0514);
assert(0 == archive_write_header(a, ae));
archive_entry_free(ae);
assert(0 == archive_write_finish_entry(a));
/* Overwrite an existing dir. */
/* For dir, the first perms should get left. */
assert(mkdir("dir_overwrite_0744", 0744) == 0);
/* Check original perms. */
assert(0 == stat("dir_overwrite_0744", &st));
failure("dir_overwrite_0744: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == 0744);
/* Overwrite shouldn't edit perms. */
assert((ae = archive_entry_new()) != NULL);
archive_entry_copy_pathname(ae, "dir_overwrite_0744");
archive_entry_set_mode(ae, S_IFDIR | 0777);
assert(0 == archive_write_header(a, ae));
archive_entry_free(ae);
assert(0 == archive_write_finish_entry(a));
/* Make sure they're unchanged. */
assert(0 == stat("dir_overwrite_0744", &st));
failure("dir_overwrite_0744: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == 0744);
/* Write a regular file with SUID bit, but don't use _EXTRACT_PERM. */
assert((ae = archive_entry_new()) != NULL);
archive_entry_copy_pathname(ae, "file_no_suid");
archive_entry_set_mode(ae, S_IFREG | S_ISUID | 0777);
archive_write_disk_set_options(a, 0);
assert(0 == archive_write_header(a, ae));
assert(0 == archive_write_finish_entry(a));
/* Write a regular file with ARCHIVE_EXTRACT_PERM. */
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_0777");
archive_entry_set_mode(ae, S_IFREG | 0777);
archive_write_disk_set_options(a, ARCHIVE_EXTRACT_PERM);
assert(0 == archive_write_header(a, ae));
assert(0 == archive_write_finish_entry(a));
/* Write a regular file with ARCHIVE_EXTRACT_PERM & SUID bit */
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_4742");
archive_entry_set_mode(ae, S_IFREG | S_ISUID | 0742);
archive_entry_set_uid(ae, getuid());
archive_write_disk_set_options(a, ARCHIVE_EXTRACT_PERM);
assert(0 == archive_write_header(a, ae));
assert(0 == archive_write_finish_entry(a));
/*
* Write a regular file with ARCHIVE_EXTRACT_PERM & SUID bit,
* but wrong uid. POSIX says you shouldn't restore SUID bit
* unless the UID could be restored.
*/
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_bad_suid");
archive_entry_set_mode(ae, S_IFREG | S_ISUID | 0742);
archive_entry_set_uid(ae, getuid() + 1);
archive_write_disk_set_options(a, ARCHIVE_EXTRACT_PERM);
assertA(0 == archive_write_header(a, ae));
/*
* Because we didn't ask for owner, the failure to
* restore SUID shouldn't return a failure.
* We check below to make sure SUID really wasn't set.
* See more detailed comments below.
*/
failure("Opportunistic SUID failure shouldn't return error.");
assertEqualInt(0, archive_write_finish_entry(a));
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_bad_suid2");
archive_entry_set_mode(ae, S_IFREG | S_ISUID | 0742);
archive_entry_set_uid(ae, getuid() + 1);
archive_write_disk_set_options(a,
ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_OWNER);
assertA(0 == archive_write_header(a, ae));
/* Owner change should fail here. */
failure("Non-opportunistic SUID failure should return error.");
assertEqualInt(ARCHIVE_WARN, archive_write_finish_entry(a));
/* Write a regular file with ARCHIVE_EXTRACT_PERM & SGID bit */
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_perm_sgid");
archive_entry_set_mode(ae, S_IFREG | S_ISGID | 0742);
archive_entry_set_gid(ae, defaultgid());
archive_write_disk_set_options(a, ARCHIVE_EXTRACT_PERM);
assert(0 == archive_write_header(a, ae));
failure("Setting SGID bit should succeed here.");
assertEqualIntA(a, 0, archive_write_finish_entry(a));
if (altgid() == -1) {
/*
* Current user must belong to at least two groups or
* else we can't test setting the GID to another group.
*/
printf("Current user can't test gid restore: must belong to more than one group.\n");
} else {
/*
* Write a regular file with ARCHIVE_EXTRACT_PERM & SGID bit
* but without ARCHIVE_EXTRACT_OWNER.
*/
/*
* This is a weird case: The user has asked for permissions to
* be restored but not asked for ownership to be restored. As
* a result, the default file creation will create a file with
* the wrong group. There are several possible behaviors for
* libarchive in this scenario:
* = Set the SGID bit. It is wrong and a security hole to
* set SGID with the wrong group. Even POSIX thinks so.
* = Implicitly set the group. I don't like this.
* = drop the SGID bit and warn (the old libarchive behavior)
* = drop the SGID bit and don't warn (the current libarchive
* behavior).
* The current behavior sees SGID/SUID restore when you
* don't ask for owner restore as an "opportunistic"
* action. That is, libarchive should do it if it can,
* but if it can't, it's not an error.
*/
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_alt_sgid");
archive_entry_set_mode(ae, S_IFREG | S_ISGID | 0742);
archive_entry_set_uid(ae, getuid());
archive_entry_set_gid(ae, altgid());
archive_write_disk_set_options(a, ARCHIVE_EXTRACT_PERM);
assert(0 == archive_write_header(a, ae));
failure("Setting SGID bit should fail because of group mismatch but the failure should be silent because we didn't ask for the group to be set.");
assertEqualIntA(a, 0, archive_write_finish_entry(a));
/*
* As above, but add _EXTRACT_OWNER to verify that it
* does succeed.
*/
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_alt_sgid_owner");
archive_entry_set_mode(ae, S_IFREG | S_ISGID | 0742);
archive_entry_set_uid(ae, getuid());
archive_entry_set_gid(ae, altgid());
archive_write_disk_set_options(a,
ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_OWNER);
assert(0 == archive_write_header(a, ae));
failure("Setting SGID bit should succeed here.");
assertEqualIntA(a, ARCHIVE_OK, archive_write_finish_entry(a));
}
/*
* Write a regular file with ARCHIVE_EXTRACT_PERM & SGID bit,
* but wrong GID. POSIX says you shouldn't restore SGID bit
* unless the GID could be restored.
*/
if (invalidgid() == -1) {
/* This test always fails for root. */
printf("Running as root: Can't test SGID failures.\n");
} else {
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_bad_sgid");
archive_entry_set_mode(ae, S_IFREG | S_ISGID | 0742);
archive_entry_set_gid(ae, invalidgid());
archive_write_disk_set_options(a, ARCHIVE_EXTRACT_PERM);
assertA(0 == archive_write_header(a, ae));
failure("This SGID restore should fail without an error.");
assertEqualIntA(a, 0, archive_write_finish_entry(a));
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_bad_sgid2");
archive_entry_set_mode(ae, S_IFREG | S_ISGID | 0742);
archive_entry_set_gid(ae, invalidgid());
archive_write_disk_set_options(a,
ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_OWNER);
assertA(0 == archive_write_header(a, ae));
failure("This SGID restore should fail with an error.");
assertEqualIntA(a, ARCHIVE_WARN, archive_write_finish_entry(a));
}
/* Set ownership should fail if we're not root. */
if (getuid() == 0) {
printf("Running as root: Can't test setuid failures.\n");
} else {
assert(archive_entry_clear(ae) != NULL);
archive_entry_copy_pathname(ae, "file_bad_owner");
archive_entry_set_mode(ae, S_IFREG | 0744);
archive_entry_set_uid(ae, getuid() + 1);
archive_write_disk_set_options(a, ARCHIVE_EXTRACT_OWNER);
assertA(0 == archive_write_header(a, ae));
assertEqualIntA(a,ARCHIVE_WARN,archive_write_finish_entry(a));
}
#if ARCHIVE_API_VERSION > 1
assert(0 == archive_write_finish(a));
#else
archive_write_finish(a);
#endif
archive_entry_free(ae);
/* Test the entries on disk. */
assert(0 == stat("file_0755", &st));
failure("file_0755: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == 0755);
assert(0 == stat("file_overwrite_0144", &st));
failure("file_overwrite_0144: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == 0144);
assert(0 == stat("dir_0514", &st));
failure("dir_0514: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == 0514);
assert(0 == stat("dir_overwrite_0744", &st));
failure("dir_overwrite_0744: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == 0744);
assert(0 == stat("file_no_suid", &st));
failure("file_0755: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == 0755);
assert(0 == stat("file_0777", &st));
failure("file_0777: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == 0777);
/* SUID bit should get set here. */
assert(0 == stat("file_4742", &st));
failure("file_4742: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == (S_ISUID | 0742));
/* SUID bit should NOT have been set here. */
assert(0 == stat("file_bad_suid", &st));
failure("file_bad_suid: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == (0742));
/* SUID bit should NOT have been set here. */
assert(0 == stat("file_bad_suid2", &st));
failure("file_bad_suid2: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == (0742));
/* SGID should be set here. */
assert(0 == stat("file_perm_sgid", &st));
failure("file_perm_sgid: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == (S_ISGID | 0742));
if (altgid() != -1) {
/* SGID should not be set here. */
assert(0 == stat("file_alt_sgid", &st));
failure("file_alt_sgid: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == (0742));
/* SGID should be set here. */
assert(0 == stat("file_alt_sgid_owner", &st));
failure("file_alt_sgid: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == (S_ISGID | 0742));
}
if (invalidgid() != -1) {
/* SGID should NOT be set here. */
assert(0 == stat("file_bad_sgid", &st));
failure("file_bad_sgid: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == (0742));
/* SGID should NOT be set here. */
assert(0 == stat("file_bad_sgid2", &st));
failure("file_bad_sgid2: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == (0742));
}
if (getuid() != 0) {
assert(0 == stat("file_bad_owner", &st));
failure("file_bad_owner: st.st_mode=%o", st.st_mode);
assert((st.st_mode & 07777) == (0744));
failure("file_bad_owner: st.st_uid=%d getuid()=%d",
st.st_uid, getuid());
/* The entry had getuid()+1, but because we're
* not root, we should not have been able to set that. */
assert(st.st_uid == getuid());
}
#endif
}