diff --git a/Makefile-tests.am b/Makefile-tests.am index 6bae65cf..5d39ee5e 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -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 diff --git a/docs/formats.md b/docs/formats.md index 0943aafa..c3723279 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -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 diff --git a/docs/repo.md b/docs/repo.md index 69f26172..580281ca 100644 --- a/docs/repo.md +++ b/docs/repo.md @@ -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 diff --git a/src/libostree/ostree-core-private.h b/src/libostree/ostree-core-private.h index 34f86a6c..2bd2f984 100644 --- a/src/libostree/ostree-core-private.h +++ b/src/libostree/ostree-core-private.h @@ -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 diff --git a/src/libostree/ostree-core.c b/src/libostree/ostree-core.c index 0671ed35..f0d0e698 100644 --- a/src/libostree/ostree-core.c +++ b/src/libostree/ostree-core.c @@ -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; } diff --git a/src/libostree/ostree-core.h b/src/libostree/ostree-core.h index 48a75f92..9a14192d 100644 --- a/src/libostree/ostree-core.h +++ b/src/libostree/ostree-core.h @@ -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; /** diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c index 21ce288f..5b16be5b 100644 --- a/src/libostree/ostree-repo-commit.c +++ b/src/libostree/ostree-repo-commit.c @@ -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 */ diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c index 6c541029..a27591b3 100644 --- a/src/libostree/ostree-repo.c +++ b/src/libostree/ostree-repo.c @@ -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 (); diff --git a/tests/fixtures/bare-split-xattrs/basic.tar.xz b/tests/fixtures/bare-split-xattrs/basic.tar.xz new file mode 100644 index 00000000..cec6717e Binary files /dev/null and b/tests/fixtures/bare-split-xattrs/basic.tar.xz differ diff --git a/tests/test-basic-bare-split-xattrs.sh b/tests/test-basic-bare-split-xattrs.sh new file mode 100755 index 00000000..ac8ebffd --- /dev/null +++ b/tests/test-basic-bare-split-xattrs.sh @@ -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