diff --git a/src/libostree/ostree-repo-pull.c b/src/libostree/ostree-repo-pull.c index 7f6d0123..2e93f90b 100644 --- a/src/libostree/ostree-repo-pull.c +++ b/src/libostree/ostree-repo-pull.c @@ -111,6 +111,7 @@ typedef struct { gboolean is_mirror; gboolean is_commit_only; gboolean is_untrusted; + gboolean is_bareuseronly_files; GPtrArray *dirs; @@ -556,6 +557,94 @@ pull_matches_subdir (OtPullData *pull_data, return FALSE; } +/* This bit mirrors similar code in commit_loose_content_object() for the + * bare-user-only mode. It's opt-in though for all pulls. + */ +static gboolean +validate_bareuseronly_mode (OtPullData *pull_data, + const char *checksum, + guint32 content_mode, + GError **error) +{ + if (!pull_data->is_bareuseronly_files) + return TRUE; + + if (S_ISREG (content_mode)) + { + const guint32 invalid_modebits = ((content_mode & ~S_IFMT) & ~0775); + if (invalid_modebits > 0) + return glnx_throw (error, "object %s.file: invalid mode 0%04o with bits 0%04o", + checksum, content_mode, invalid_modebits); + } + else if (S_ISLNK (content_mode)) + ; /* Nothing */ + else + g_assert_not_reached (); + + return TRUE; +} + +/* Import a single content object in the case where + * we have pull_data->remote_repo_local. + * + * One important special case here is handling the + * OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES flag. + */ +static gboolean +import_one_local_content_object (OtPullData *pull_data, + const char *checksum, + GCancellable *cancellable, + GError **error) +{ + g_assert (pull_data->remote_repo_local); + + const gboolean trusted = !pull_data->is_untrusted; + if (trusted && !pull_data->is_bareuseronly_files) + { + if (!ostree_repo_import_object_from_with_trust (pull_data->repo, pull_data->remote_repo_local, + OSTREE_OBJECT_TYPE_FILE, checksum, + trusted, + cancellable, error)) + return FALSE; + } + else + { + /* In this case we either need to validate the checksum + * or the file mode. + */ + g_autoptr(GInputStream) content_input = NULL; + g_autoptr(GFileInfo) content_finfo = NULL; + g_autoptr(GVariant) content_xattrs = NULL; + + if (!ostree_repo_load_file (pull_data->remote_repo_local, checksum, + &content_input, &content_finfo, &content_xattrs, + cancellable, error)) + return FALSE; + + if (!validate_bareuseronly_mode (pull_data, checksum, + g_file_info_get_attribute_uint32 (content_finfo, "unix::mode"), + error)) + return FALSE; + + /* Now that we've potentially validated it, convert to object stream */ + guint64 length; + g_autoptr(GInputStream) object_stream = NULL; + if (!ostree_raw_file_to_content_stream (content_input, content_finfo, + content_xattrs, &object_stream, + &length, cancellable, error)) + return FALSE; + + g_autofree guchar *real_csum = NULL; + if (!ostree_repo_write_content (pull_data->repo, checksum, + object_stream, length, + &real_csum, + cancellable, error)) + return FALSE; + } + + return TRUE; +} + static gboolean scan_dirtree_object (OtPullData *pull_data, const char *checksum, @@ -595,15 +684,19 @@ scan_dirtree_object (OtPullData *pull_data, &file_is_stored, cancellable, error)) return FALSE; - if (!file_is_stored && pull_data->remote_repo_local) + /* If we already have this object, move on to the next */ + if (file_is_stored) + continue; + + /* Is this a local repo? */ + if (pull_data->remote_repo_local) { - if (!ostree_repo_import_object_from_with_trust (pull_data->repo, pull_data->remote_repo_local, - OSTREE_OBJECT_TYPE_FILE, file_checksum, !pull_data->is_untrusted, - cancellable, error)) + if (!import_one_local_content_object (pull_data, file_checksum, cancellable, error)) return FALSE; } - else if (!file_is_stored && !g_hash_table_lookup (pull_data->requested_content, file_checksum)) + else if (!g_hash_table_lookup (pull_data->requested_content, file_checksum)) { + /* In this case we're doing HTTP pulls */ g_hash_table_add (pull_data->requested_content, file_checksum); enqueue_one_object_request (pull_data, file_checksum, OSTREE_OBJECT_TYPE_FILE, path, FALSE, FALSE); file_checksum = NULL; /* Transfer ownership */ @@ -2775,6 +2868,7 @@ ostree_repo_pull_with_options (OstreeRepo *self, pull_data->is_mirror = (flags & OSTREE_REPO_PULL_FLAGS_MIRROR) > 0; pull_data->is_commit_only = (flags & OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY) > 0; pull_data->is_untrusted = (flags & OSTREE_REPO_PULL_FLAGS_UNTRUSTED) > 0; + pull_data->is_bareuseronly_files = (flags & OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES) > 0; pull_data->cancellable = cancellable ? g_object_ref (cancellable) : NULL; if (error) @@ -3042,11 +3136,21 @@ ostree_repo_pull_with_options (OstreeRepo *self, } } - /* For local pulls, default to disabling static deltas so that the - * exact object files are copied. - */ - if (pull_data->remote_repo_local && !pull_data->require_static_deltas) - pull_data->disable_static_deltas = TRUE; + if (pull_data->remote_repo_local) + { + /* For local pulls, default to disabling static deltas so that the + * exact object files are copied. + */ + if (!pull_data->require_static_deltas) + pull_data->disable_static_deltas = TRUE; + + } + else if (pull_data->is_bareuseronly_files) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Can't use bareuseronly-files with non-local origin repo"); + goto out; + } /* We can't use static deltas if pulling into an archive-z2 repo. */ if (self->mode == OSTREE_REPO_MODE_ARCHIVE_Z2) diff --git a/src/libostree/ostree-repo.h b/src/libostree/ostree-repo.h index 86bed09c..beacbd58 100644 --- a/src/libostree/ostree-repo.h +++ b/src/libostree/ostree-repo.h @@ -1015,12 +1015,14 @@ gboolean ostree_repo_prune_from_reachable (OstreeRepo *self, * @OSTREE_REPO_PULL_FLAGS_MIRROR: Write out refs suitable for mirrors * @OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY: Fetch only the commit metadata * @OSTREE_REPO_PULL_FLAGS_UNTRUSTED: Don't trust local remote + * @OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES: Since 2017.7. Reject writes of content objects with modes outside of 0775. */ typedef enum { OSTREE_REPO_PULL_FLAGS_NONE, OSTREE_REPO_PULL_FLAGS_MIRROR = (1 << 0), OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY = (1 << 1), - OSTREE_REPO_PULL_FLAGS_UNTRUSTED = (1 << 2) + OSTREE_REPO_PULL_FLAGS_UNTRUSTED = (1 << 2), + OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES = (1 << 3) } OstreeRepoPullFlags; _OSTREE_PUBLIC diff --git a/src/ostree/ot-builtin-pull-local.c b/src/ostree/ot-builtin-pull-local.c index 28717b85..b19a4c6a 100644 --- a/src/ostree/ot-builtin-pull-local.c +++ b/src/ostree/ot-builtin-pull-local.c @@ -33,6 +33,7 @@ static char *opt_remote; static gboolean opt_disable_fsync; static gboolean opt_untrusted; +static gboolean opt_bareuseronly_files; static gboolean opt_require_static_deltas; static gboolean opt_gpg_verify; static gboolean opt_gpg_verify_summary; @@ -42,6 +43,7 @@ static GOptionEntry options[] = { { "remote", 0, 0, G_OPTION_ARG_STRING, &opt_remote, "Add REMOTE to refspec", "REMOTE" }, { "disable-fsync", 0, 0, G_OPTION_ARG_NONE, &opt_disable_fsync, "Do not invoke fsync()", NULL }, { "untrusted", 0, 0, G_OPTION_ARG_NONE, &opt_untrusted, "Do not trust source", NULL }, + { "bareuseronly-files", 0, 0, G_OPTION_ARG_NONE, &opt_bareuseronly_files, "Reject regular files with mode outside of 0775 (world writable, suid, etc.)", NULL }, { "require-static-deltas", 0, 0, G_OPTION_ARG_NONE, &opt_require_static_deltas, "Require static deltas", NULL }, { "gpg-verify", 0, 0, G_OPTION_ARG_NONE, &opt_gpg_verify, "GPG verify commits (must specify --remote)", NULL }, { "gpg-verify-summary", 0, 0, G_OPTION_ARG_NONE, &opt_gpg_verify_summary, "GPG verify summary (must specify --remote)", NULL }, @@ -92,6 +94,8 @@ ostree_builtin_pull_local (int argc, char **argv, GCancellable *cancellable, GEr if (opt_untrusted) pullflags |= OSTREE_REPO_PULL_FLAGS_UNTRUSTED; + if (opt_bareuseronly_files) + pullflags |= OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES; if (opt_disable_fsync) ostree_repo_set_disable_fsync (repo, TRUE); diff --git a/src/ostree/ot-builtin-pull.c b/src/ostree/ot-builtin-pull.c index d88c5ee8..bd3ac115 100644 --- a/src/ostree/ot-builtin-pull.c +++ b/src/ostree/ot-builtin-pull.c @@ -34,6 +34,7 @@ static gboolean opt_dry_run; static gboolean opt_disable_static_deltas; static gboolean opt_require_static_deltas; static gboolean opt_untrusted; +static gboolean opt_bareuseronly_files; static char** opt_subpaths; static char** opt_http_headers; static char* opt_cache_dir; @@ -50,6 +51,7 @@ static GOptionEntry options[] = { { "mirror", 0, 0, G_OPTION_ARG_NONE, &opt_mirror, "Write refs suitable for a mirror", NULL }, { "subpath", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_subpaths, "Only pull the provided subpath(s)", NULL }, { "untrusted", 0, 0, G_OPTION_ARG_NONE, &opt_untrusted, "Do not trust (local) sources", NULL }, + { "bareuseronly-files", 0, 0, G_OPTION_ARG_NONE, &opt_bareuseronly_files, "Reject regular files with mode outside of 0775 (world writable, suid, etc.)", NULL }, { "dry-run", 0, 0, G_OPTION_ARG_NONE, &opt_dry_run, "Only print information on what will be downloaded (requires static deltas)", NULL }, { "depth", 0, 0, G_OPTION_ARG_INT, &opt_depth, "Traverse DEPTH parents (-1=infinite) (default: 0)", "DEPTH" }, { "url", 0, 0, G_OPTION_ARG_STRING, &opt_url, "Pull objects from this URL instead of the one from the remote config", NULL }, @@ -167,6 +169,8 @@ ostree_builtin_pull (int argc, char **argv, GCancellable *cancellable, GError ** if (opt_untrusted) pullflags |= OSTREE_REPO_PULL_FLAGS_UNTRUSTED; + if (opt_bareuseronly_files) + pullflags |= OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES; if (opt_dry_run && !opt_require_static_deltas) { diff --git a/tests/basic-test.sh b/tests/basic-test.sh index f6aa72c2..ba051e03 100644 --- a/tests/basic-test.sh +++ b/tests/basic-test.sh @@ -19,7 +19,7 @@ set -euo pipefail -echo "1..$((66 + ${extra_basic_tests:-0}))" +echo "1..$((68 + ${extra_basic_tests:-0}))" $CMD_PREFIX ostree --version > version.yaml python -c 'import yaml; yaml.safe_load(open("version.yaml"))' @@ -267,6 +267,32 @@ test2_commit_relpath=/objects/${test2_commitid:0:2}/${test2_commitid:2}.commit assert_files_hardlinked repo/${test2_commit_relpath} repo2/${test2_commit_relpath} echo "ok pull-local (hardlinking metadata)" +cd ${test_tmpdir} +rm repo2 -rf && mkdir repo2 +ostree_repo_init repo2 --mode=$opposite_mode +${CMD_PREFIX} ostree --repo=repo2 pull-local --bareuseronly-files repo test2 +${CMD_PREFIX} ostree --repo=repo2 fsck -q +echo "ok pull-local --bareuseronly-files" + +# This is mostly a copy of the suid test in test-basic-user-only.sh, +# but for the `pull --bareuseronly-files` case. +cd ${test_tmpdir} +rm repo-input -rf +ostree_repo_init repo-input init --mode=archive +cd ${test_tmpdir} +cat > statoverride.txt < files/some-setuid +chmod 0644 files/some-setuid +$CMD_PREFIX ostree --repo=repo-input commit -b content-with-suid --statoverride=statoverride.txt --tree=dir=files +if $CMD_PREFIX ostree pull-local --repo=repo --bareuseronly-files repo-input content-with-suid 2>err.txt; then + assert_not_reached "copying suid file with --bareuseronly-files worked?" +fi +assert_file_has_content err.txt 'object.*\.file: invalid mode.*with bits 040.*' +echo "ok pull-local (bareuseronly files)" + cd ${test_tmpdir} ${CMD_PREFIX} ostree --repo=repo2 checkout ${CHECKOUT_U_ARG} test2 test2-checkout-from-local-clone cd test2-checkout-from-local-clone diff --git a/tests/pull-test.sh b/tests/pull-test.sh index 4a4ef069..f1ebca4c 100644 --- a/tests/pull-test.sh +++ b/tests/pull-test.sh @@ -35,7 +35,7 @@ function verify_initial_contents() { assert_file_has_content baz/cow '^moo$' } -echo "1..20" +echo "1..21" # Try both syntaxes repo_init --no-gpg-verify @@ -79,6 +79,13 @@ ${CMD_PREFIX} ostree --repo=mirrorrepo pull origin main ${CMD_PREFIX} ostree --repo=mirrorrepo fsck echo "ok pull (refuses deltas)" +if ${CMD_PREFIX} ostree --repo=mirrorrepo \ + pull origin main --bareuseronly-files 2>err.txt; then + assert_not_reached "--bareuseronly-files unexpectedly succeeded" +fi +assert_file_has_content err.txt 'bareuseronly-files with non-local' +echo "ok pull (refuses bareuseronly)" + cd ${test_tmpdir} rm mirrorrepo/refs/remotes/* -rf ${CMD_PREFIX} ostree --repo=mirrorrepo prune --refs-only