Merge pull request #2532 from lucab/ups/repo-mode-bare-split-xattrs

lib/core: introduce 'bare-split-xattrs' mode
This commit is contained in:
Colin Walters 2022-03-03 09:41:45 -05:00 committed by GitHub
commit 06d13ff16c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 187 additions and 13 deletions

View File

@ -59,6 +59,7 @@ test_programs = \
$(NULL)
_installed_or_uninstalled_test_scripts = \
tests/test-basic.sh \
tests/test-basic-bare-split-xattrs.sh \
tests/test-basic-user.sh \
tests/test-basic-user-only.sh \
tests/test-basic-root.sh \
@ -201,6 +202,7 @@ dist_installed_test_data = tests/archive-test.sh \
tests/ostree-path-traverse.tar.gz \
tests/pre-signed-pull-data.tar.gz \
tests/libtest-core.sh \
tests/fixtures/bare-split-xattrs/basic.tar.xz \
$(NULL)
EXTRA_DIST += tests/libtest.sh

View File

@ -63,18 +63,32 @@ Other disadvantages of `archive`:
- One doesn't know the total size (compressed or uncompressed) of content
before downloading everything
## Aside: the bare and bare-user formats
## Aside: bare formats
The most common operation is to pull from an `archive` repository
into a `bare` or `bare-user` formatted repository. These latter two
are not compressed on disk. In other words, pulling to them is
similar to unpacking (but not installing) an RPM/deb package.
The most common operation is to pull from a remote `archive` repository
into a local one. This latter is not compressed on disk. In other
words, pulling to a local repository is similar to unpacking (but not
installing) the content of an RPM/deb package.
The `bare` repository format is the simplest one. In this mode regular files
are directly stored to disk, and all metadata (e.g. uid/gid and xattrs) is
reflected to the filesystem.
It allows further direct access to content and metadata, but it may require
elevated privileges when writing objects to the repository.
The `bare-user` format is a bit special in that the uid/gid and xattrs
from the content are ignored. This is primarily useful if you want to
have the same OSTree-managed content that can be run on a host system
or an unprivileged container.
Similarly, the `bare-split-xattrs` format is a special mode where xattrs
are stored as separate repository objects, and not directly reflected to
the filesystem.
This is primarily useful when transporting xattrs through lossy environments
(e.g. tar streams and containerized environments). It also allows carrying
security-sensitive xattrs (e.g. SELinux labels) out-of-band without involving
OS filesystem logic.
## Static deltas
OSTree itself was originally focused on a continuous delivery model, where

View File

@ -81,15 +81,37 @@ warnings such as GNU Tar emitting "implausibly old time stamp" with 0; however,
until we have a mechanism to transition cleanly to 1, for compatibilty OSTree
is reverted to use zero again.
### Xattrs objects
In some repository modes (e.g. `bare-split-xattrs`), xattrs are stored on the
side of the content objects they refer to. This is done via two dedicated
object types, `file-xattrs` and `file-xattrs-link`.
`file-xattrs` store xattrs data, encoded as GVariant. Each object is keyed by
the checksum of the xattrs content, allowing for multiple references.
`file-xattrs-link` are hardlinks which are associated to file objects.
Each object is keyed by the same checksum of the corresponding file
object. The target of the hardlink is an existing `file-xattrs` object.
In case of reaching the limit of too many links, this object could be
a plain file too.
# Repository types and locations
Also unlike git, an OSTree repository can be in one of four separate
modes: `bare`, `bare-user`, `bare-user-only`, and `archive`. A bare repository is
one where content files are just stored as regular files; it's
designed to be the source of a "hardlink farm", where each operating
system checkout is merely links into it. If you want to store files
Also unlike git, an OSTree repository can be in one of five separate
modes: `bare`, `bare-split-xattrs, ``bare-user`, `bare-user-only`, and
`archive`.
A `bare` repository is one where content files are just stored as regular
files; it's designed to be the source of a "hardlink farm", where each
operating system checkout is merely links into it. If you want to store files
owned by e.g. root in this mode, you must run OSTree as root.
The `bare-split-xattrs` mode is similar to the above one, but it does store
xattrs as separate objects. This is meant to avoid conflicts with
kernel-enforced constraints (e.g. on SELinux labels) and with other softwares
that may perform ephemeral changes to xattrs (e.g. container runtimes).
The `bare-user` mode is a later addition that is like `bare` in that
files are unpacked, but it can (and should generally) be created as
non-root. In this mode, extended metadata such as owner uid, gid, and

View File

@ -197,7 +197,8 @@ _ostree_repo_mode_is_bare (OstreeRepoMode mode)
return
mode == OSTREE_REPO_MODE_BARE ||
mode == OSTREE_REPO_MODE_BARE_USER ||
mode == OSTREE_REPO_MODE_BARE_USER_ONLY;
mode == OSTREE_REPO_MODE_BARE_USER_ONLY ||
mode == OSTREE_REPO_MODE_BARE_SPLIT_XATTRS;
}
#ifndef OSTREE_DISABLE_GPGME

View File

@ -39,6 +39,7 @@ G_STATIC_ASSERT(OSTREE_REPO_MODE_ARCHIVE_Z2 == 1);
G_STATIC_ASSERT(OSTREE_REPO_MODE_ARCHIVE == OSTREE_REPO_MODE_ARCHIVE_Z2);
G_STATIC_ASSERT(OSTREE_REPO_MODE_BARE_USER == 2);
G_STATIC_ASSERT(OSTREE_REPO_MODE_BARE_USER_ONLY == 3);
G_STATIC_ASSERT(OSTREE_REPO_MODE_BARE_SPLIT_XATTRS == 4);
static GBytes *variant_to_lenprefixed_buffer (GVariant *variant);
@ -1228,6 +1229,10 @@ ostree_object_type_to_string (OstreeObjectType objtype)
return "commitmeta";
case OSTREE_OBJECT_TYPE_PAYLOAD_LINK:
return "payload-link";
case OSTREE_OBJECT_TYPE_FILE_XATTRS:
return "file-xattrs";
case OSTREE_OBJECT_TYPE_FILE_XATTRS_LINK:
return "file-xattrs-link";
default:
g_assert_not_reached ();
return NULL;
@ -1257,6 +1262,10 @@ ostree_object_type_from_string (const char *str)
return OSTREE_OBJECT_TYPE_COMMIT_META;
else if (!strcmp (str, "payload-link"))
return OSTREE_OBJECT_TYPE_PAYLOAD_LINK;
else if (!strcmp (str, "file-xattrs"))
return OSTREE_OBJECT_TYPE_FILE_XATTRS;
else if (!strcmp (str, "file-xattrs-link"))
return OSTREE_OBJECT_TYPE_FILE_XATTRS_LINK;
g_assert_not_reached ();
return 0;
}
@ -2141,6 +2150,8 @@ _ostree_validate_structureof_metadata (OstreeObjectType objtype,
/* TODO */
break;
case OSTREE_OBJECT_TYPE_FILE:
case OSTREE_OBJECT_TYPE_FILE_XATTRS:
case OSTREE_OBJECT_TYPE_FILE_XATTRS_LINK:
g_assert_not_reached ();
break;
}

View File

@ -67,6 +67,8 @@ G_BEGIN_DECLS
* @OSTREE_OBJECT_TYPE_TOMBSTONE_COMMIT: Toplevel object, refers to a deleted commit
* @OSTREE_OBJECT_TYPE_COMMIT_META: Detached metadata for a commit
* @OSTREE_OBJECT_TYPE_PAYLOAD_LINK: Symlink to a .file given its checksum on the payload only.
* @OSTREE_OBJECT_TYPE_FILE_XATTRS: Detached xattrs content, for 'bare-split-xattrs' mode.
* @OSTREE_OBJECT_TYPE_FILE_XATTRS_LINK: Hardlink to a .file-xattrs given the checksum of its .file object.
*
* Enumeration for core object types; %OSTREE_OBJECT_TYPE_FILE is for
* content, the other types are metadata.
@ -78,7 +80,9 @@ typedef enum {
OSTREE_OBJECT_TYPE_COMMIT = 4, /* .commit */
OSTREE_OBJECT_TYPE_TOMBSTONE_COMMIT = 5, /* .commit-tombstone */
OSTREE_OBJECT_TYPE_COMMIT_META = 6, /* .commitmeta */
OSTREE_OBJECT_TYPE_PAYLOAD_LINK = 7, /* .payload-link */
OSTREE_OBJECT_TYPE_PAYLOAD_LINK = 7, /* .payload-link */
OSTREE_OBJECT_TYPE_FILE_XATTRS = 8, /* .file-xattrs */
OSTREE_OBJECT_TYPE_FILE_XATTRS_LINK = 9, /* .file-xattrs-link */
} OstreeObjectType;
/**
@ -94,7 +98,7 @@ typedef enum {
*
* Last valid object type; use this to validate ranges.
*/
#define OSTREE_OBJECT_TYPE_LAST OSTREE_OBJECT_TYPE_PAYLOAD_LINK
#define OSTREE_OBJECT_TYPE_LAST OSTREE_OBJECT_TYPE_FILE_XATTRS_LINK
/**
* OSTREE_DIRMETA_GVARIANT_FORMAT:
@ -189,6 +193,7 @@ typedef enum {
* @OSTREE_REPO_MODE_ARCHIVE_Z2: Legacy alias for `OSTREE_REPO_MODE_ARCHIVE`
* @OSTREE_REPO_MODE_BARE_USER: Files are stored as themselves, except ownership; can be written by user. Hardlinks work only in user checkouts.
* @OSTREE_REPO_MODE_BARE_USER_ONLY: Same as BARE_USER, but all metadata is not stored, so it can only be used for user checkouts. Does not need xattrs.
* @OSTREE_REPO_MODE_BARE_SPLIT_XATTRS: Same as BARE_USER, but xattrs are stored separately from file content, with dedicated object types.
*
* See the documentation of #OstreeRepo for more information about the
* possible modes.
@ -199,6 +204,7 @@ typedef enum {
OSTREE_REPO_MODE_ARCHIVE_Z2 = OSTREE_REPO_MODE_ARCHIVE,
OSTREE_REPO_MODE_BARE_USER,
OSTREE_REPO_MODE_BARE_USER_ONLY,
OSTREE_REPO_MODE_BARE_SPLIT_XATTRS,
} OstreeRepoMode;
/**

View File

@ -920,6 +920,9 @@ write_content_object (OstreeRepo *self,
return FALSE;
OstreeRepoMode repo_mode = ostree_repo_get_mode (self);
if (repo_mode == OSTREE_REPO_MODE_BARE_SPLIT_XATTRS &&
g_getenv ("OSTREE_EXP_WRITE_BARE_SPLIT_XATTRS") == NULL)
return glnx_throw (error, "Not allowed due to repo mode");
GInputStream *file_input; /* Unowned alias */
g_autoptr(GInputStream) file_input_owned = NULL; /* We need a temporary for bare-user symlinks */

View File

@ -2704,6 +2704,9 @@ ostree_repo_mode_to_string (OstreeRepoMode mode,
/* Legacy alias */
ret_mode ="archive-z2";
break;
case OSTREE_REPO_MODE_BARE_SPLIT_XATTRS:
ret_mode = "bare-split-xattrs";
break;
default:
return glnx_throw (error, "Invalid mode '%d'", mode);
}
@ -2734,6 +2737,8 @@ ostree_repo_mode_from_string (const char *mode,
else if (strcmp (mode, "archive-z2") == 0 ||
strcmp (mode, "archive") == 0)
ret_mode = OSTREE_REPO_MODE_ARCHIVE;
else if (strcmp (mode, "bare-split-xattrs") == 0)
ret_mode = OSTREE_REPO_MODE_BARE_SPLIT_XATTRS;
else
return glnx_throw (error, "Invalid mode '%s' in repository configuration", mode);
@ -4171,6 +4176,32 @@ repo_load_file_archive (OstreeRepo *self,
}
}
static GVariant *
_ostree_repo_read_xattrs_file_link (OstreeRepo *self,
const char *checksum,
GCancellable *cancellable,
GError **error)
{
g_assert (self != NULL);
g_assert (checksum != NULL);
char xattr_path[_OSTREE_LOOSE_PATH_MAX];
_ostree_loose_path (xattr_path, checksum, OSTREE_OBJECT_TYPE_FILE_XATTRS_LINK, self->mode);
g_autoptr(GVariant) xattrs = NULL;
glnx_autofd int fd = -1;
if (!glnx_openat_rdonly (self->objects_dir_fd, xattr_path, FALSE, &fd, error))
return FALSE;
g_assert (fd >= 0);
if (!ot_variant_read_fd (fd, 0, G_VARIANT_TYPE ("a(ayay)"), TRUE,
&xattrs, error))
return glnx_prefix_error_null (error, "Deserializing xattrs content");
g_assert (xattrs != NULL);
return g_steal_pointer (&xattrs);
}
gboolean
_ostree_repo_load_file_bare (OstreeRepo *self,
const char *checksum,
@ -4305,6 +4336,15 @@ _ostree_repo_load_file_bare (OstreeRepo *self,
return FALSE;
}
}
else if (self->mode == OSTREE_REPO_MODE_BARE_SPLIT_XATTRS)
{
if (out_xattrs)
{
ret_xattrs = _ostree_repo_read_xattrs_file_link(self, checksum, cancellable, error);
if (ret_xattrs == NULL)
return FALSE;
}
}
else
{
g_assert_not_reached ();

Binary file not shown.

View File

@ -0,0 +1,75 @@
#!/usr/bin/env bash
#
# SPDX-License-Identifier: LGPL-2.0+
set -euo pipefail
. $(dirname $0)/libtest.sh
mode="bare-split-xattrs"
OSTREE="${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo"
SUDO="sudo --non-interactive"
PRIVILEGED="false"
if [ $(id -u) -eq 0 ]; then
PRIVILEGED="true"
SUDO=""
elif $(${SUDO} -v); then
PRIVILEGED="true"
fi
cd ${test_tmpdir}
${OSTREE} init --mode "${mode}"
${OSTREE} config get core.mode > mode.txt
assert_file_has_content mode.txt "${mode}"
tap_ok "repo init"
rm -rf -- repo mode.txt
cd ${test_tmpdir}
${OSTREE} init --mode "${mode}"
${OSTREE} fsck --all
tap_ok "repo fsck"
rm -rf -- repo
cd ${test_tmpdir}
mkdir -p "${test_tmpdir}/files"
touch files/foo
${OSTREE} init --mode "${mode}"
if ${OSTREE} commit --orphan -m "not implemented" files; then
assert_not_reached "commit to bare-split-xattrs should have failed"
fi
${OSTREE} fsck --all
tap_ok "commit not implemented"
rm -rf -- repo files
cd ${test_tmpdir}
mkdir -p "${test_tmpdir}/files"
touch files/foo
${OSTREE} init --mode "${mode}"
OSTREE_EXP_WRITE_BARE_SPLIT_XATTRS=true ${OSTREE} commit --orphan -m "experimental" files
if ${OSTREE} fsck --all; then
assert_not_reached "fsck should have failed"
fi
tap_ok "commit exp override"
rm -rf -- repo files
if [ "${PRIVILEGED}" = "true" ]; then
COMMIT="d614c428015227259031b0f19b934dade908942fd71c49047e0daa70e7800a5d"
cd ${test_tmpdir}
${SUDO} tar --same-permissions --same-owner -xaf ${test_srcdir}/fixtures/bare-split-xattrs/basic.tar.xz
${SUDO} ${OSTREE} fsck --all
${OSTREE} log ${COMMIT} > out.txt
assert_file_has_content_literal out.txt "fixtures: bare-split-xattrs repo"
${OSTREE} ls ${COMMIT} -X /foo > out.txt
assert_file_has_content_literal out.txt "{ @a(ayay) [] } /foo"
${OSTREE} ls ${COMMIT} -X /bar > out.txt
assert_file_has_content_literal out.txt "{ [(b'user.mykey', [byte 0x6d, 0x79, 0x76, 0x61, 0x6c, 0x75, 0x65])] } /bar"
${OSTREE} ls ${COMMIT} /foolink > out.txt
assert_file_has_content_literal out.txt "/foolink -> foo"
tap_ok "reading simple fixture"
${SUDO} rm -rf -- repo log.txt
else
tap_ok "reading simple fixture # skip Unable to sudo"
fi
tap_end