Skip to content
Permalink
Browse files Browse the repository at this point in the history
Prevent cross-mountpoint attacks via .augsave during saving
Previously Augeas would open PATH.augsave for writing if a rename from PATH to
PATH.augsave failed, then write the file contents in.  Now if the rename fails,
it tries to unlink PATH.augsave and open it with O_EXCL first.

Mountpoints remain permitted at either PATH or PATH.augnew provided
/augeas/save/copy_if_rename_fails exists.

* src/transform.c (clone_file):
    add argument to perform unlink and O_EXCL on destination filename after a
    rename failure to prevent PATH.augsave being a mountpoint
* src/transform.c (transform_save, remove_file):
    always try to unlink PATH.augsave if rename fails, only allowing PATH to be
    a mountpoint; allow PATH or PATH.augnew to be mountpoints
* tests/
    test-put-mount: check PATH being a mountpoint is supported
    test-put-mount-augnew.sh: check PATH.augnew being a mountpoint is supported
    test-put-mount-augsave.sh: check unlink error when PATH.augsave is a mount

Fixes BZ 772261
  • Loading branch information
Dominic Cleal authored and David Lutterkort committed Jul 19, 2012
1 parent 1638774 commit b8de6a8
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 8 deletions.
40 changes: 34 additions & 6 deletions src/transform.c
Expand Up @@ -27,6 +27,7 @@

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <selinux/selinux.h>
#include <stdbool.h>
Expand Down Expand Up @@ -844,14 +845,21 @@ static int transfer_file_attrs(FILE *from, FILE *to,
* means that FROM or TO is a bindmounted file), and COPY_IF_RENAME_FAILS
* is true, copy the contents of FROM into TO and delete FROM.
*
* If COPY_IF_RENAME_FAILS and UNLINK_IF_RENAME_FAILS are true, and the above
* copy mechanism is used, it will unlink the TO path and open with O_EXCL
* to ensure we only copy *from* a bind mount rather than into an attacker's
* mount placed at TO (e.g. for .augsave).
*
* Return 0 on success (either rename succeeded or we copied the contents
* over successfully), -1 on failure.
*/
static int clone_file(const char *from, const char *to,
const char **err_status, int copy_if_rename_fails) {
const char **err_status, int copy_if_rename_fails,
int unlink_if_rename_fails) {
FILE *from_fp = NULL, *to_fp = NULL;
char buf[BUFSIZ];
size_t len;
int to_fd = -1, to_oflags, r;
int result = -1;

if (rename(from, to) == 0)
Expand All @@ -867,10 +875,23 @@ static int clone_file(const char *from, const char *to,
goto done;
}

if (!(to_fp = fopen(to, "w"))) {
if (unlink_if_rename_fails) {
r = unlink(to);
if (r < 0) {
*err_status = "clone_unlink_dst";
goto done;
}
}

to_oflags = unlink_if_rename_fails ? O_EXCL : O_TRUNC;
if ((to_fd = open(to, O_WRONLY|O_CREAT|to_oflags, S_IRUSR|S_IWUSR)) < 0) {
*err_status = "clone_open_dst";
goto done;
}
if (!(to_fp = fdopen(to_fd, "w"))) {
*err_status = "clone_fdopen_dst";
goto done;
}

if (transfer_file_attrs(from_fp, to_fp, err_status) < 0)
goto done;
Expand All @@ -897,8 +918,15 @@ static int clone_file(const char *from, const char *to,
done:
if (from_fp != NULL)
fclose(from_fp);
if (to_fp != NULL && fclose(to_fp) != 0)
if (to_fp != NULL) {
if (fclose(to_fp) != 0) {
*err_status = "clone_fclose_dst";
result = -1;
}
} else if (to_fd >= 0 && close(to_fd) < 0) {
*err_status = "clone_close_dst";
result = -1;
}
if (result != 0)
unlink(to);
if (result == 0)
Expand Down Expand Up @@ -1132,15 +1160,15 @@ int transform_save(struct augeas *aug, struct tree *xfm,
goto done;
}

r = clone_file(augorig_canon, augsave, &err_status, 1);
r = clone_file(augorig_canon, augsave, &err_status, 1, 1);
if (r != 0) {
dyn_err_status = strappend(err_status, "_augsave");
goto done;
}
}
}

r = clone_file(augtemp, augdest, &err_status, copy_if_rename_fails);
r = clone_file(augtemp, augdest, &err_status, copy_if_rename_fails, 0);
if (r != 0) {
dyn_err_status = strappend(err_status, "_augtemp");
goto done;
Expand Down Expand Up @@ -1298,7 +1326,7 @@ int remove_file(struct augeas *aug, struct tree *tree) {
goto error;
}

r = clone_file(augorig_canon, augsave, &err_status, 1);
r = clone_file(augorig_canon, augsave, &err_status, 1, 1);
if (r != 0) {
dyn_err_status = strappend(err_status, "_augsave");
goto error;
Expand Down
5 changes: 3 additions & 2 deletions tests/Makefile.am
Expand Up @@ -184,8 +184,9 @@ check_SCRIPTS = \
$(lens_tests) \
test-get.sh test-augtool.sh \
test-put-symlink.sh test-put-symlink-augnew.sh \
test-put-symlink-augsave.sh test-put-symlink-augtemp.sh test-save-empty.sh \
test-bug-1.sh test-idempotent.sh test-preserve.sh \
test-put-symlink-augsave.sh test-put-symlink-augtemp.sh \
test-put-mount.sh test-put-mount-augnew.sh test-put-mount-augsave.sh \
test-save-empty.sh test-bug-1.sh test-idempotent.sh test-preserve.sh \
test-events-saved.sh test-save-mode.sh test-unlink-error.sh \
test-augtool-empty-line.sh test-augtool-modify-root.sh

Expand Down
69 changes: 69 additions & 0 deletions tests/test-put-mount-augnew.sh
@@ -0,0 +1,69 @@
#! /bin/bash

# Test that we can write into a bind mount placed at PATH.augnew with the
# copy_if_rename_fails flag.
# This requires that EXDEV or EBUSY is returned from rename(2) to activate the
# code path, so set up a bind mount on Linux.

if [ $UID -ne 0 -o "$(uname -s)" != "Linux" ]; then
echo "Test can only be run as root on Linux to create bind mounts"
exit 77
fi

ROOT=$abs_top_builddir/build/test-put-mount-augnew
LENSES=$abs_top_srcdir/lenses

HOSTS=$ROOT/etc/hosts
HOSTS_AUGNEW=${HOSTS}.augnew
TARGET=$ROOT/other/real_hosts

rm -rf $ROOT
mkdir -p $(dirname $HOSTS)
mkdir -p $(dirname $TARGET)

echo 127.0.0.1 localhost > $HOSTS
touch $TARGET $HOSTS_AUGNEW

mount --bind $TARGET $HOSTS_AUGNEW
Exit() {
umount $HOSTS_AUGNEW
exit $1
}

HOSTS_SUM=$(sum $HOSTS)

augtool --nostdinc -I $LENSES -r $ROOT --new <<EOF
set /augeas/save/copy_if_rename_fails 1
set /files/etc/hosts/1/alias myhost
save
print /augeas//error
EOF

if [ ! -f $HOSTS ] ; then
echo "/etc/hosts is no longer a regular file"
Exit 1
fi
if [ ! "x${HOSTS_SUM}" = "x$(sum $HOSTS)" ]; then
echo "/etc/hosts has changed"
Exit 1
fi
if [ ! "x${HOSTS_SUM}" = "x$(sum $HOSTS)" ]; then
echo "/etc/hosts has changed"
Exit 1
fi

if [ ! -s $HOSTS_AUGNEW ]; then
echo "/etc/hosts.augnew is empty"
Exit 1
fi
if [ ! -s $TARGET ]; then
echo "/other/real_hosts is empty"
Exit 1
fi

if ! grep myhost $TARGET >/dev/null; then
echo "/other/real_hosts does not contain the modification"
Exit 1
fi

Exit 0
62 changes: 62 additions & 0 deletions tests/test-put-mount-augsave.sh
@@ -0,0 +1,62 @@
#! /bin/bash

# Test that we don't follow bind mounts when writing to .augsave.
# This requires that EXDEV or EBUSY is returned from rename(2) to activate the
# code path, so set up a bind mount on Linux.

if [ $UID -ne 0 -o "$(uname -s)" != "Linux" ]; then
echo "Test can only be run as root on Linux to create bind mounts"
exit 77
fi

actual() {
(augtool --nostdinc -I $LENSES -r $ROOT --backup | grep ^/augeas) <<EOF
set /augeas/save/copy_if_rename_fails 1
set /files/etc/hosts/1/alias myhost
save
print /augeas//error
EOF
}

expected() {
cat <<EOF
/augeas/files/etc/hosts/error = "clone_unlink_dst_augsave"
/augeas/files/etc/hosts/error/message = "Device or resource busy"
EOF
}

ROOT=$abs_top_builddir/build/test-put-mount-augsave
LENSES=$abs_top_srcdir/lenses

HOSTS=$ROOT/etc/hosts
HOSTS_AUGSAVE=${HOSTS}.augsave

ATTACK_FILE=$ROOT/other/attack

rm -rf $ROOT
mkdir -p $(dirname $HOSTS)
mkdir -p $(dirname $ATTACK_FILE)

echo 127.0.0.1 localhost > $HOSTS
touch $ATTACK_FILE $HOSTS_AUGSAVE

mount --bind $ATTACK_FILE $HOSTS_AUGSAVE
Exit() {
umount $HOSTS_AUGSAVE
exit $1
}

ACTUAL=$(actual)
EXPECTED=$(expected)
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "No error when trying to unlink augsave (a bind mount):"
echo "$ACTUAL"
exit 1
fi

if [ -s $ATTACK_FILE ]; then
echo "/other/attack now contains data, should be blank"
Exit 1
fi

Exit 0
55 changes: 55 additions & 0 deletions tests/test-put-mount.sh
@@ -0,0 +1,55 @@
#! /bin/bash

# Test that we can write into a bind mount with the copy_if_rename_fails flag.
# This requires that EXDEV or EBUSY is returned from rename(2) to activate the
# code path, so set up a bind mount on Linux.

if [ $UID -ne 0 -o "$(uname -s)" != "Linux" ]; then
echo "Test can only be run as root on Linux to create bind mounts"
exit 77
fi

ROOT=$abs_top_builddir/build/test-put-mount
LENSES=$abs_top_srcdir/lenses

HOSTS=$ROOT/etc/hosts
TARGET=$ROOT/other/real_hosts

rm -rf $ROOT
mkdir -p $(dirname $HOSTS)
mkdir -p $(dirname $TARGET)

echo 127.0.0.1 localhost > $TARGET
touch $HOSTS

mount --bind $TARGET $HOSTS
Exit() {
umount $HOSTS
exit $1
}

HOSTS_SUM=$(sum $HOSTS)

augtool --nostdinc -I $LENSES -r $ROOT <<EOF
set /augeas/save/copy_if_rename_fails 1
set /files/etc/hosts/1/alias myhost
save
print /augeas//error
EOF

if [ ! "x${HOSTS_SUM}" != "x$(sum $HOSTS)" ]; then
echo "/etc/hosts hasn't changed"
Exit 1
fi

if [ ! "x${HOSTS_SUM}" != "x$(sum $TARGET)" ]; then
echo "/other/real_hosts hasn't changed"
Exit 1
fi

if ! grep myhost $TARGET >/dev/null; then
echo "/other/real_hosts does not contain the modification"
Exit 1
fi

Exit 0

0 comments on commit b8de6a8

Please sign in to comment.