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

Support overlayfs whiteouts on checkout #2717

Merged
merged 1 commit into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,14 @@ jobs:
# An empty string isn't valid, so a dummy --label option is always
# added.
options: --label ostree ${{ matrix.container-options }}
# make sure tests are performed on a non-overlayfs filesystem
volumes:
- tmp_dir:/test-tmp
env:
TEST_TMPDIR: /test-tmp

steps:

- name: Pre-checkout setup
run: ${{ matrix.pre-checkout-setup }}
if: ${{ matrix.pre-checkout-setup }}
Expand All @@ -187,7 +193,7 @@ jobs:
run: ./ci/gh-install.sh ${{ matrix.extra-packages }}

- name: Add non-root user
run: "useradd builder && chown -R -h builder: ."
run: "useradd builder && chown -R -h builder: . $TEST_TMPDIR"

- name: Build and test
run: runuser -u builder -- ./ci/gh-build.sh ${{ matrix.configure-options }}
Expand Down
1 change: 1 addition & 0 deletions Makefile-tests.am
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ _installed_or_uninstalled_test_scripts = \
tests/test-admin-deploy-nomerge.sh \
tests/test-admin-deploy-none.sh \
tests/test-admin-deploy-bootid-gc.sh \
tests/test-admin-deploy-whiteouts.sh \
tests/test-osupdate-dtb.sh \
tests/test-admin-instutil-set-kargs.sh \
tests/test-admin-upgrade-not-backwards.sh \
Expand Down
1 change: 1 addition & 0 deletions bash/ostree
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ _ostree_checkout() {
--union-identical
--user-mode -U
--whiteouts
--process-passthrough-whiteouts
"

local options_with_args="
Expand Down
11 changes: 11 additions & 0 deletions man/ostree-checkout.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ License along with this library. If not, see <https://www.gnu.org/licenses/>.
</para></listitem>
</varlistentry>

<varlistentry>
<term><option>--process-passthrough-whiteouts</option></term>

<listitem><para>
Enable overlayfs whiteout extraction into 0:0 character devices.
Overlayfs whiteouts are encoded inside ostree as <literal>.ostree-wh.filename</literal>
and extracted as 0:0 character devices. This is useful to carry
container storage embedded into ostree.
</para></listitem>
</varlistentry>

<varlistentry>
<term><option>--allow-noent</option></term>

Expand Down
129 changes: 128 additions & 1 deletion src/libostree/ostree-repo-checkout.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
#define WHITEOUT_PREFIX ".wh."
#define OPAQUE_WHITEOUT_NAME ".wh..wh..opq"

#define OVERLAYFS_WHITEOUT_PREFIX ".ostree-wh."

/* Per-checkout call state/caching */
typedef struct {
GString *path_buf; /* buffer for real path if filtering enabled */
Expand Down Expand Up @@ -582,6 +584,117 @@ checkout_file_hardlink (OstreeRepo *self,
return TRUE;
}

static gboolean
_checkout_overlayfs_whiteout_at_no_overwrite (OstreeRepoCheckoutAtOptions *options,
int destination_dfd,
const char *destination_name,
GFileInfo *file_info,
GVariant *xattrs,
gboolean *found_exant_file,
GCancellable *cancellable,
GError **error)
{
if (found_exant_file != NULL)
*found_exant_file = FALSE;
guint32 file_mode = g_file_info_get_attribute_uint32 (file_info, "unix::mode");
if (mknodat(destination_dfd, destination_name, (file_mode & ~S_IFMT) | S_IFCHR, (dev_t)0) < 0)
Copy link
Member

Choose a reason for hiding this comment

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

Don't we store the file type in the ostree mode too? Curious why you're explicitly forcing on S_IFCHR here.

Copy link
Contributor Author

@mangelajo mangelajo Sep 27, 2022

Choose a reason for hiding this comment

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

In ostree it's stored as a regular file, otherwise in bare mode we would have to create character devices to get those bits preserved. Then it would look more like my original approach here #2713 which was more complex

Copy link
Contributor Author

Choose a reason for hiding this comment

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

or are we extracting the S_IFCHR before this call? checking,..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah, no, we just detect the .ostree-wh. prefix for a regular empty file, which removes the prefix and triggers this call

{
if (errno == EEXIST && found_exant_file != NULL)
{
*found_exant_file = TRUE;
return TRUE;
}
return glnx_throw_errno_prefix (error, "Creating whiteout char device");
}
if (options->mode != OSTREE_REPO_CHECKOUT_MODE_USER)
{
if (xattrs != NULL &&
!glnx_dfd_name_set_all_xattrs(destination_dfd, destination_name, xattrs,
cancellable, error))
return glnx_throw_errno_prefix (error, "Setting xattrs for whiteout char device");

if (TEMP_FAILURE_RETRY(fchownat(destination_dfd, destination_name,
g_file_info_get_attribute_uint32 (file_info, "unix::uid"),
g_file_info_get_attribute_uint32 (file_info, "unix::gid"),
AT_SYMLINK_NOFOLLOW) < 0))
return glnx_throw_errno_prefix (error, "fchownat");
if (TEMP_FAILURE_RETRY (fchmodat (destination_dfd, destination_name, file_mode & ~S_IFMT, 0)) < 0)
return glnx_throw_errno_prefix (error, "fchmodat %s to 0%o", destination_name, file_mode & ~S_IFMT);
}

return TRUE;
}

static gboolean
_checkout_overlayfs_whiteout_at (OstreeRepo *repo,
OstreeRepoCheckoutAtOptions *options,
int destination_dfd,
const char *destination_name,
GFileInfo *file_info,
GVariant *xattrs,
GCancellable *cancellable,
GError **error)
{
gboolean found_exant_file = FALSE;
if (!_checkout_overlayfs_whiteout_at_no_overwrite(options, destination_dfd, destination_name,
file_info, xattrs,&found_exant_file,
cancellable, error))
return FALSE;

if (!found_exant_file)
return TRUE;

guint32 uid = g_file_info_get_attribute_uint32 (file_info, "unix::uid");
guint32 gid = g_file_info_get_attribute_uint32 (file_info, "unix::gid");
guint32 file_mode = g_file_info_get_attribute_uint32 (file_info, "unix::mode");

struct stat dest_stbuf;

switch(options->overwrite_mode)
{
case OSTREE_REPO_CHECKOUT_OVERWRITE_NONE:
return FALSE;
case OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES:
if (!ot_ensure_unlinked_at (destination_dfd, destination_name, error))
return FALSE;
return _checkout_overlayfs_whiteout_at_no_overwrite(options, destination_dfd, destination_name,
file_info, xattrs, NULL, cancellable, error);
case OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES:
return TRUE;

case OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_IDENTICAL:
if (!glnx_fstatat(destination_dfd, destination_name, &dest_stbuf, AT_SYMLINK_NOFOLLOW,
error))
return FALSE;
if (!(repo->disable_xattrs || repo->mode == OSTREE_REPO_MODE_BARE_USER_ONLY))
{
g_autoptr(GVariant) fs_xattrs;
if (!glnx_dfd_name_get_all_xattrs (destination_dfd, destination_name,
&fs_xattrs, cancellable, error))
return FALSE;
if (!g_variant_equal(fs_xattrs, xattrs))
return glnx_throw(error, "existing destination file %s xattrs don't match",
destination_name);
}
if (options->mode != OSTREE_REPO_CHECKOUT_MODE_USER)
{
if (gid != dest_stbuf.st_gid)
return glnx_throw(error, "existing destination file %s does not match gid %d",
destination_name, gid);

if (uid != dest_stbuf.st_uid)
return glnx_throw(error, "existing destination file %s does not match uid %d",
destination_name, gid);

if ((file_mode & ALLPERMS) != (dest_stbuf.st_mode & ALLPERMS))
return glnx_throw(error, "existing destination file %s does not match mode %o",
destination_name, file_mode);
}
break;
}
return TRUE;
}

static gboolean
checkout_one_file_at (OstreeRepo *repo,
OstreeRepoCheckoutAtOptions *options,
Expand All @@ -603,7 +716,8 @@ checkout_one_file_at (OstreeRepo *repo,

/* FIXME - avoid the GFileInfo here */
g_autoptr(GFileInfo) source_info = NULL;
if (!ostree_repo_load_file (repo, checksum, NULL, &source_info, NULL,
g_autoptr(GVariant) source_xattrs = NULL;
if (!ostree_repo_load_file (repo, checksum, NULL, &source_info, &source_xattrs,
cancellable, error))
return FALSE;

Expand All @@ -623,6 +737,7 @@ checkout_one_file_at (OstreeRepo *repo,
const gboolean is_unreadable = (!is_symlink && (source_mode & S_IRUSR) == 0);
const gboolean is_whiteout = (!is_symlink && options->process_whiteouts &&
g_str_has_prefix (destination_name, WHITEOUT_PREFIX));
const gboolean is_overlayfs_whiteout = (!is_symlink && g_str_has_prefix (destination_name, OVERLAYFS_WHITEOUT_PREFIX));
const gboolean is_reg_zerosized = (!is_symlink && g_file_info_get_size (source_info) == 0);
const gboolean override_user_unreadable = (options->mode == OSTREE_REPO_CHECKOUT_MODE_USER && is_unreadable);

Expand All @@ -643,6 +758,18 @@ checkout_one_file_at (OstreeRepo *repo,

need_copy = FALSE;
}
else if (is_overlayfs_whiteout && options->process_passthrough_whiteouts)
{
const char *name = destination_name + (sizeof (OVERLAYFS_WHITEOUT_PREFIX) - 1);
mangelajo marked this conversation as resolved.
Show resolved Hide resolved

if (!name[0])
return glnx_throw (error, "Invalid empty overlayfs whiteout '%s'", name);

g_assert (name[0] != '/'); /* Sanity */

return _checkout_overlayfs_whiteout_at(repo, options, destination_dfd, name,
source_info, source_xattrs, cancellable, error);
}
else if (is_reg_zerosized || override_user_unreadable)
{
/* In https://github.com/ostreedev/ostree/commit/673cacd633f9d6b653cdea530657d3e780a41bbd we
Expand Down
5 changes: 3 additions & 2 deletions src/libostree/ostree-repo.h
Original file line number Diff line number Diff line change
Expand Up @@ -989,8 +989,9 @@ typedef struct {
gboolean force_copy; /* Since: 2017.6 */
gboolean bareuseronly_dirs; /* Since: 2017.7 */
gboolean force_copy_zerosized; /* Since: 2018.9 */
gboolean unused_bools[4];
/* 4 byte hole on 64 bit */
gboolean process_passthrough_whiteouts;
gboolean unused_bools[3];
/* 3 byte hole on 64 bit */

const char *subpath;

Expand Down
2 changes: 1 addition & 1 deletion src/libostree/ostree-sysroot-deploy.c
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ checkout_deployment_tree (OstreeSysroot *sysroot,
return FALSE;

/* Generate hardlink farm, then opendir it */
OstreeRepoCheckoutAtOptions checkout_opts = { 0, };
OstreeRepoCheckoutAtOptions checkout_opts = { .process_passthrough_whiteouts = TRUE };
if (!ostree_repo_checkout_at (repo, &checkout_opts, osdeploy_dfd,
checkout_target_name, csum,
cancellable, error))
Expand Down
7 changes: 6 additions & 1 deletion src/ostree/ot-builtin-checkout.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ static gboolean opt_union;
static gboolean opt_union_add;
static gboolean opt_union_identical;
static gboolean opt_whiteouts;
static gboolean opt_process_passthrough_whiteouts;
static gboolean opt_from_stdin;
static char *opt_from_file;
static gboolean opt_disable_fsync;
Expand Down Expand Up @@ -77,6 +78,7 @@ static GOptionEntry options[] = {
{ "union-add", 0, 0, G_OPTION_ARG_NONE, &opt_union_add, "Keep existing files/directories, only add new", NULL },
{ "union-identical", 0, 0, G_OPTION_ARG_NONE, &opt_union_identical, "When layering checkouts, error out if a file would be replaced with a different version, but add new files and directories", NULL },
{ "whiteouts", 0, 0, G_OPTION_ARG_NONE, &opt_whiteouts, "Process 'whiteout' (Docker style) entries", NULL },
{ "process-passthrough-whiteouts", 0, 0, G_OPTION_ARG_NONE, &opt_process_passthrough_whiteouts, "Enable overlayfs whiteout extraction into char 0:0 devices", NULL },
{ "allow-noent", 0, 0, G_OPTION_ARG_NONE, &opt_allow_noent, "Do nothing if specified path does not exist", NULL },
{ "from-stdin", 0, 0, G_OPTION_ARG_NONE, &opt_from_stdin, "Process many checkouts from standard input", NULL },
{ "from-file", 0, 0, G_OPTION_ARG_STRING, &opt_from_file, "Process many checkouts from input file", "FILE" },
Expand Down Expand Up @@ -129,7 +131,8 @@ process_one_checkout (OstreeRepo *repo,
if (opt_disable_cache || opt_whiteouts || opt_require_hardlinks ||
opt_union_add || opt_force_copy || opt_force_copy_zerosized ||
opt_bareuseronly_dirs || opt_union_identical ||
opt_skiplist_file || opt_selinux_policy || opt_selinux_prefix)
opt_skiplist_file || opt_selinux_policy || opt_selinux_prefix ||
opt_process_passthrough_whiteouts)
{
OstreeRepoCheckoutAtOptions checkout_options = { 0, };

Expand Down Expand Up @@ -162,6 +165,8 @@ process_one_checkout (OstreeRepo *repo,
}
if (opt_whiteouts)
checkout_options.process_whiteouts = TRUE;
if (opt_process_passthrough_whiteouts)
checkout_options.process_passthrough_whiteouts = TRUE;
if (subpath)
checkout_options.subpath = subpath;

Expand Down
7 changes: 6 additions & 1 deletion tests/archive-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ mkdir -p test-overlays
date > test-overlays/overlaid-file
$OSTREE commit ${COMMIT_ARGS} -b test-base --base test2 --owner-uid 42 --owner-gid 42 test-overlays/
$OSTREE ls -R test-base > ls.txt
assert_streq "$(wc -l < ls.txt)" 14
if can_create_whiteout_devices; then
assert_streq "$(wc -l < ls.txt)" 17
else
assert_streq "$(wc -l < ls.txt)" 14
fi

assert_streq "$(grep '42.*42' ls.txt | wc -l)" 2
echo "ok commit overlay base"
29 changes: 28 additions & 1 deletion tests/basic-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

set -euo pipefail

echo "1..$((88 + ${extra_basic_tests:-0}))"
echo "1..$((90 + ${extra_basic_tests:-0}))"

CHECKOUT_U_ARG=""
CHECKOUT_H_ARGS="-H"
Expand Down Expand Up @@ -1203,3 +1203,30 @@ if test "$(id -u)" != "0"; then
else
echo "ok # SKIP not run when root"
fi

if ! skip_one_without_whiteouts_devices; then
cd ${test_tmpdir}
rm checkout-test2 -rf
$OSTREE checkout test2 checkout-test2

assert_not_has_file checkout-test2/whiteouts/whiteout
assert_not_has_file checkout-test2/whiteouts/whiteout2
assert_has_file checkout-test2/whiteouts/.ostree-wh.whiteout
assert_has_file checkout-test2/whiteouts/.ostree-wh.whiteout2

echo "ok checkout: no whiteout passthrough by default"
fi

if ! skip_one_without_whiteouts_devices; then
cd ${test_tmpdir}
rm checkout-test2 -rf
$OSTREE checkout --process-passthrough-whiteouts test2 checkout-test2

assert_not_has_file checkout-test2/whiteouts/.ostree-wh.whiteout
assert_not_has_file checkout-test2/whiteouts/.ostree-wh.whiteout2

assert_is_whiteout_device checkout-test2/whiteouts/whiteout
assert_is_whiteout_device checkout-test2/whiteouts/whiteout2

echo "ok checkout: whiteout with overlayfs passthrough processing"
fi
7 changes: 7 additions & 0 deletions tests/kolainst/data-shared/libtest-core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ assert_file_has_mode () {
fi
}

assert_is_whiteout_device () {
device_details="$(stat -c '%F %t:%T' $1)"
if [ "$device_details" != "character special file 0:0" ]; then
fatal "File '$1' is not a whiteout character device 0:0"
fi
}

assert_symlink_has_content () {
if ! test -L "$1"; then
fatal "File '$1' is not a symbolic link"
Expand Down
Loading