/* * Copyright (C) 2011,2013 Colin Walters * * SPDX-License-Identifier: LGPL-2.0+ * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * * Author: Colin Walters */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include "otutil.h" #include "ostree.h" #include "ostree-core-private.h" #include "ostree-repo-private.h" #include "ostree-sepolicy-private.h" #include "ostree-repo-file-enumerator.h" #include "ostree-checksum-input-stream.h" #include "ostree-varint.h" /* The standardized version of BTRFS_IOC_CLONE */ #ifndef FICLONE #define FICLONE _IOW(0x94, 9, int) #endif /* Understanding ostree's fsync strategy * * A long time ago, ostree used to invoke fsync() on each object, * then move it into the objects directory. However, it turned * out to be a *lot* faster to write the objects into a separate "staging" * directory (letting the filesystem handle writeback how it likes) * and then only walk over each of the files, fsync(), then rename() * into place. See also https://lwn.net/Articles/789024/ * * (We also support a "disable fsync entirely" mode, where you don't * care about integrity; e.g. test suites using disposable VMs). * * This "delayed fsync" pattern though is much worse for other concurrent processes * like databases because it forces a lot to go through the filesystem * journal at once once we do the sync. So now we support a `per_object_fsync` * option that again invokes `fsync()` directly. This also notably * provides "backpressure", ensuring we aren't queuing up a huge amount * of I/O at once. */ /* The directory where we place content */ static int commit_dest_dfd (OstreeRepo *self) { if (self->per_object_fsync) return self->objects_dir_fd; else if (self->in_transaction && !self->disable_fsync) return self->commit_stagedir.fd; else return self->objects_dir_fd; } /* If we don't have O_TMPFILE, or for symlinks we'll create temporary * files. If we have a txn, use the staging dir to ensure that * things are consistently locked against concurrent cleanup, and * in general we have all of our data in one place. */ static int commit_tmp_dfd (OstreeRepo *self) { if (self->in_transaction) return self->commit_stagedir.fd; else return self->tmp_dir_fd; } /* The objects/ directory has a two-character directory prefix for checksums * to avoid putting lots of files in a single directory. This technique * is quite old, but Git also uses it for example. */ gboolean _ostree_repo_ensure_loose_objdir_at (int dfd, const char *loose_path, GCancellable *cancellable, GError **error) { char loose_prefix[3]; loose_prefix[0] = loose_path[0]; loose_prefix[1] = loose_path[1]; loose_prefix[2] = '\0'; if (mkdirat (dfd, loose_prefix, 0777) == -1) { if (G_UNLIKELY (errno != EEXIST)) { glnx_set_error_from_errno (error); return FALSE; } } return TRUE; } /* This GVariant is the header for content objects (regfiles and symlinks) */ static GVariant * create_file_metadata (guint32 uid, guint32 gid, guint32 mode, GVariant *xattrs) { GVariant *ret_metadata = NULL; g_autoptr(GVariant) tmp_xattrs = NULL; if (xattrs == NULL) tmp_xattrs = g_variant_ref_sink (g_variant_new_array (G_VARIANT_TYPE ("(ayay)"), NULL, 0)); ret_metadata = g_variant_new ("(uuu@a(ayay))", GUINT32_TO_BE (uid), GUINT32_TO_BE (gid), GUINT32_TO_BE (mode), xattrs ? xattrs : tmp_xattrs); g_variant_ref_sink (ret_metadata); return ret_metadata; } /* bare-user repositories store file metadata as a user xattr */ gboolean _ostree_write_bareuser_metadata (int fd, guint32 uid, guint32 gid, guint32 mode, GVariant *xattrs, GError **error) { g_autoptr(GVariant) filemeta = create_file_metadata (uid, gid, mode, xattrs); if (TEMP_FAILURE_RETRY (fsetxattr (fd, "user.ostreemeta", (char*)g_variant_get_data (filemeta), g_variant_get_size (filemeta), 0)) != 0) return glnx_throw_errno_prefix (error, "fsetxattr(user.ostreemeta)"); return TRUE; } /* See https://github.com/ostreedev/ostree/pull/698 */ #ifdef WITH_SMACK #define XATTR_NAME_SMACK "security.SMACK64" #endif static void ot_security_smack_reset_dfd_name (int dfd, const char *name) { #ifdef WITH_SMACK char buf[PATH_MAX]; /* See glnx-xattrs.c */ snprintf (buf, sizeof (buf), "/proc/self/fd/%d/%s", dfd, name); (void) lremovexattr (buf, XATTR_NAME_SMACK); #endif } static void ot_security_smack_reset_fd (int fd) { #ifdef WITH_SMACK (void) fremovexattr (fd, XATTR_NAME_SMACK); #endif } /* Given an O_TMPFILE regular file, link it into place. */ gboolean _ostree_repo_commit_tmpf_final (OstreeRepo *self, const char *checksum, OstreeObjectType objtype, GLnxTmpfile *tmpf, GCancellable *cancellable, GError **error) { char tmpbuf[_OSTREE_LOOSE_PATH_MAX]; _ostree_loose_path (tmpbuf, checksum, objtype, self->mode); int dest_dfd = commit_dest_dfd (self); if (!_ostree_repo_ensure_loose_objdir_at (dest_dfd, tmpbuf, cancellable, error)) return FALSE; if (!_ostree_tmpf_fsverity (self, tmpf, error)) return FALSE; if (!glnx_link_tmpfile_at (tmpf, GLNX_LINK_TMPFILE_NOREPLACE_IGNORE_EXIST, dest_dfd, tmpbuf, error)) return FALSE; /* We're done with the fd */ glnx_tmpfile_clear (tmpf); return TRUE; } /* Given a dfd+path combination (may be regular file or symlink), * rename it into place. */ static gboolean commit_path_final (OstreeRepo *self, const char *checksum, OstreeObjectType objtype, OtCleanupUnlinkat *tmp_path, GCancellable *cancellable, GError **error) { /* The final renameat() */ char tmpbuf[_OSTREE_LOOSE_PATH_MAX]; _ostree_loose_path (tmpbuf, checksum, objtype, self->mode); int dest_dfd = commit_dest_dfd (self); if (!_ostree_repo_ensure_loose_objdir_at (dest_dfd, tmpbuf, cancellable, error)) return FALSE; if (renameat (tmp_path->dfd, tmp_path->path, dest_dfd, tmpbuf) == -1) { if (errno != EEXIST) return glnx_throw_errno_prefix (error, "Storing file '%s'", tmp_path->path); /* Otherwise, the caller's cleanup will unlink+free */ } else { /* The tmp path was consumed */ ot_cleanup_unlinkat_clear (tmp_path); } return TRUE; } /* Given either a file or symlink, apply the final metadata to it depending on * the repository mode. Note that @checksum is assumed to have been validated by * the caller. */ static gboolean commit_loose_regfile_object (OstreeRepo *self, const char *checksum, GLnxTmpfile *tmpf, guint32 uid, guint32 gid, guint32 mode, GVariant *xattrs, GCancellable *cancellable, GError **error) { if (self->mode == OSTREE_REPO_MODE_BARE) { if (TEMP_FAILURE_RETRY (fchown (tmpf->fd, uid, gid)) < 0) return glnx_throw_errno_prefix (error, "fchown"); if (TEMP_FAILURE_RETRY (fchmod (tmpf->fd, mode)) < 0) return glnx_throw_errno_prefix (error, "fchmod"); if (xattrs) { ot_security_smack_reset_fd (tmpf->fd); if (!glnx_fd_set_all_xattrs (tmpf->fd, xattrs, cancellable, error)) return FALSE; } } else if (self->mode == OSTREE_REPO_MODE_BARE_USER) { if (!_ostree_write_bareuser_metadata (tmpf->fd, uid, gid, mode, xattrs, error)) return FALSE; /* Note that previously this path added `| 0755` which made every * file executable, see * https://github.com/ostreedev/ostree/issues/907 * We then changed it to mask by 0775, but we always need at least read * permission when running as non-root, so explicitly mask that in. * * Again here, symlinks in bare-user are a hairy special case; only do a * chmod for a *real* regular file, otherwise we'll take the default 0644. */ if (S_ISREG (mode)) { const mode_t content_mode = (mode & (S_IFREG | 0775)) | S_IRUSR; if (!glnx_fchmod (tmpf->fd, content_mode, error)) return FALSE; } else g_assert (S_ISLNK (mode)); } else if (self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY) { if (!_ostree_validate_bareuseronly_mode (mode, checksum, error)) return FALSE; if (!glnx_fchmod (tmpf->fd, mode, error)) return FALSE; } if (_ostree_repo_mode_is_bare (self->mode)) { /* To satisfy tools such as guile which compare mtimes * to determine whether or not source files need to be compiled, * set the modification time to OSTREE_TIMESTAMP. */ const struct timespec times[2] = { { OSTREE_TIMESTAMP, UTIME_OMIT }, { OSTREE_TIMESTAMP, 0} }; if (TEMP_FAILURE_RETRY (futimens (tmpf->fd, times)) < 0) return glnx_throw_errno_prefix (error, "futimens"); } /* Ensure that in case of a power cut, these files have the data we * want. See http://lwn.net/Articles/322823/ */ if (!self->disable_fsync && self->per_object_fsync) { if (fsync (tmpf->fd) == -1) return glnx_throw_errno_prefix (error, "fsync"); } if (!_ostree_repo_commit_tmpf_final (self, checksum, OSTREE_OBJECT_TYPE_FILE, tmpf, cancellable, error)) return FALSE; return TRUE; } /* This is used by OSTREE_REPO_COMMIT_MODIFIER_FLAGS_GENERATE_SIZES */ typedef struct { OstreeObjectType objtype; goffset unpacked; goffset archived; } OstreeContentSizeCacheEntry; static OstreeContentSizeCacheEntry * content_size_cache_entry_new (OstreeObjectType objtype, goffset unpacked, goffset archived) { OstreeContentSizeCacheEntry *entry = g_slice_new0 (OstreeContentSizeCacheEntry); entry->objtype = objtype; entry->unpacked = unpacked; entry->archived = archived; return entry; } static void content_size_cache_entry_free (gpointer entry) { if (entry) g_slice_free (OstreeContentSizeCacheEntry, entry); } void _ostree_repo_setup_generate_sizes (OstreeRepo *self, OstreeRepoCommitModifier *modifier) { if (modifier && modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_GENERATE_SIZES) { if (ostree_repo_get_mode (self) == OSTREE_REPO_MODE_ARCHIVE) { self->generate_sizes = TRUE; /* Clear any stale data in the object sizes hash table */ if (self->object_sizes != NULL) g_hash_table_remove_all (self->object_sizes); } else g_debug ("Not generating sizes for non-archive repo"); } } static void repo_ensure_size_entries (OstreeRepo *self) { if (G_UNLIKELY (self->object_sizes == NULL)) self->object_sizes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, content_size_cache_entry_free); } static gboolean repo_has_size_entry (OstreeRepo *self, OstreeObjectType objtype, const gchar *checksum) { /* Only file, dirtree and dirmeta objects appropriate for size metadata */ if (objtype > OSTREE_OBJECT_TYPE_DIR_META) return TRUE; repo_ensure_size_entries (self); return (g_hash_table_lookup (self->object_sizes, checksum) != NULL); } static void repo_store_size_entry (OstreeRepo *self, OstreeObjectType objtype, const gchar *checksum, goffset unpacked, goffset archived) { /* Only file, dirtree and dirmeta objects appropriate for size metadata */ if (objtype > OSTREE_OBJECT_TYPE_DIR_META) return; repo_ensure_size_entries (self); g_hash_table_replace (self->object_sizes, g_strdup (checksum), content_size_cache_entry_new (objtype, unpacked, archived)); } static int compare_ascii_checksums_for_sorting (gconstpointer a_pp, gconstpointer b_pp) { char *a = *((char**)a_pp); char *b = *((char**)b_pp); return strcmp (a, b); } /* * Create sizes metadata GVariant and add it to the metadata variant given. */ static GVariant * add_size_index_to_metadata (OstreeRepo *self, GVariant *original_metadata) { g_autoptr(GVariantBuilder) builder = NULL; /* original_metadata may be NULL */ builder = ot_util_variant_builder_from_variant (original_metadata, G_VARIANT_TYPE ("a{sv}")); if (self->object_sizes && g_hash_table_size (self->object_sizes) > 0) { GVariantBuilder index_builder; g_variant_builder_init (&index_builder, G_VARIANT_TYPE ("a" _OSTREE_OBJECT_SIZES_ENTRY_SIGNATURE)); /* Sort the checksums so we can bsearch if desired */ g_autoptr(GPtrArray) sorted_keys = g_ptr_array_new (); GLNX_HASH_TABLE_FOREACH (self->object_sizes, const char*, e_checksum) g_ptr_array_add (sorted_keys, (gpointer)e_checksum); g_ptr_array_sort (sorted_keys, compare_ascii_checksums_for_sorting); for (guint i = 0; i < sorted_keys->len; i++) { guint8 csum[OSTREE_SHA256_DIGEST_LEN]; const char *e_checksum = sorted_keys->pdata[i]; g_autoptr(GString) buffer = g_string_new (NULL); ostree_checksum_inplace_to_bytes (e_checksum, csum); g_string_append_len (buffer, (char*)csum, sizeof (csum)); OstreeContentSizeCacheEntry *e_size = g_hash_table_lookup (self->object_sizes, e_checksum); _ostree_write_varuint64 (buffer, e_size->archived); _ostree_write_varuint64 (buffer, e_size->unpacked); g_string_append_c (buffer, (gchar) e_size->objtype); g_variant_builder_add (&index_builder, "@ay", ot_gvariant_new_bytearray ((guint8*)buffer->str, buffer->len)); } g_variant_builder_add (builder, "{sv}", "ostree.sizes", g_variant_builder_end (&index_builder)); /* Clear the object sizes hash table for a subsequent commit. */ g_hash_table_remove_all (self->object_sizes); } return g_variant_ref_sink (g_variant_builder_end (builder)); } static gboolean throw_min_free_space_error (OstreeRepo *self, guint64 bytes_required, GError **error) { const char *err_msg = NULL; g_autofree char *err_msg_owned = NULL; if (bytes_required > 0) { g_autofree char *formatted_required = g_format_size (bytes_required); err_msg = err_msg_owned = g_strdup_printf ("would be exceeded, at least %s requested", formatted_required); } else err_msg = "would be exceeded"; if (self->min_free_space_mb > 0) return glnx_throw (error, "min-free-space-size %" G_GUINT64_FORMAT "MB %s", self->min_free_space_mb, err_msg); else return glnx_throw (error, "min-free-space-percent '%u%%' %s", self->min_free_space_percent, err_msg); } typedef struct { gboolean initialized; GLnxTmpfile tmpf; char *expected_checksum; OtChecksum checksum; guint64 content_len; guint64 bytes_written; guint uid; guint gid; guint mode; GVariant *xattrs; } OstreeRealRepoBareContent; G_STATIC_ASSERT (sizeof (OstreeRepoBareContent) >= sizeof (OstreeRealRepoBareContent)); /* Create a tmpfile for writing a bare file. Currently just used * by the static delta code, but will likely later be extended * to be used also by the dfd_iter commit path. */ gboolean _ostree_repo_bare_content_open (OstreeRepo *self, const char *expected_checksum, guint64 content_len, guint uid, guint gid, guint mode, GVariant *xattrs, OstreeRepoBareContent *out_regwrite, GCancellable *cancellable, GError **error) { OstreeRealRepoBareContent *real = (OstreeRealRepoBareContent*) out_regwrite; g_assert (!real->initialized); real->initialized = TRUE; g_assert (S_ISREG (mode)); if (!glnx_open_tmpfile_linkable_at (commit_tmp_dfd (self), ".", O_WRONLY|O_CLOEXEC, &real->tmpf, error)) return FALSE; ot_checksum_init (&real->checksum); real->expected_checksum = g_strdup (expected_checksum); real->content_len = content_len; real->bytes_written = 0; real->uid = uid; real->gid = gid; real->mode = mode; real->xattrs = xattrs ? g_variant_ref (xattrs) : NULL; /* Initialize the checksum with the header info */ g_autoptr(GFileInfo) finfo = _ostree_mode_uidgid_to_gfileinfo (mode, uid, gid); g_autoptr(GBytes) header = _ostree_file_header_new (finfo, xattrs); ot_checksum_update_bytes (&real->checksum, header); return TRUE; } gboolean _ostree_repo_bare_content_write (OstreeRepo *repo, OstreeRepoBareContent *barewrite, const guint8 *buf, size_t len, GCancellable *cancellable, GError **error) { OstreeRealRepoBareContent *real = (OstreeRealRepoBareContent*) barewrite; g_assert (real->initialized); ot_checksum_update (&real->checksum, buf, len); if (glnx_loop_write (real->tmpf.fd, buf, len) < 0) return glnx_throw_errno_prefix (error, "write"); return TRUE; } gboolean _ostree_repo_bare_content_commit (OstreeRepo *self, OstreeRepoBareContent *barewrite, char *checksum_buf, size_t buflen, GCancellable *cancellable, GError **error) { OstreeRealRepoBareContent *real = (OstreeRealRepoBareContent*) barewrite; g_assert (real->initialized); if ((self->min_free_space_percent > 0 || self->min_free_space_mb > 0) && self->in_transaction) { struct stat st_buf; if (!glnx_fstat (real->tmpf.fd, &st_buf, error)) return FALSE; g_mutex_lock (&self->txn_lock); g_assert_cmpint (self->txn.blocksize, >, 0); const fsblkcnt_t object_blocks = (st_buf.st_size / self->txn.blocksize) + 1; if (object_blocks > self->txn.max_blocks) { self->cleanup_stagedir = TRUE; g_mutex_unlock (&self->txn_lock); return throw_min_free_space_error (self, st_buf.st_size, error); } /* This is the main bit that needs mutex protection */ self->txn.max_blocks -= object_blocks; g_mutex_unlock (&self->txn_lock); } ot_checksum_get_hexdigest (&real->checksum, checksum_buf, buflen); if (real->expected_checksum && !_ostree_compare_object_checksum (OSTREE_OBJECT_TYPE_FILE, real->expected_checksum, checksum_buf, error)) return FALSE; if (!commit_loose_regfile_object (self, checksum_buf, &real->tmpf, real->uid, real->gid, real->mode, real->xattrs, cancellable, error)) return FALSE; /* Let's have a guarantee that after commit the object is cleaned up */ _ostree_repo_bare_content_cleanup (barewrite); return TRUE; } void _ostree_repo_bare_content_cleanup (OstreeRepoBareContent *regwrite) { OstreeRealRepoBareContent *real = (OstreeRealRepoBareContent*) regwrite; if (!real->initialized) return; glnx_tmpfile_clear (&real->tmpf); ot_checksum_clear (&real->checksum); g_clear_pointer (&real->expected_checksum, (GDestroyNotify)g_free); g_clear_pointer (&real->xattrs, (GDestroyNotify)g_variant_unref); real->initialized = FALSE; } /* Allocate an O_TMPFILE, write everything from @input to it, but * not exceeding @length. Used for every object in archive repos, * and content objects in all bare-type repos. */ static gboolean create_regular_tmpfile_linkable_with_content (OstreeRepo *self, guint64 length, GInputStream *input, GLnxTmpfile *out_tmpf, GCancellable *cancellable, GError **error) { g_auto(GLnxTmpfile) tmpf = { 0, }; if (!glnx_open_tmpfile_linkable_at (commit_tmp_dfd (self), ".", O_WRONLY|O_CLOEXEC, &tmpf, error)) return FALSE; if (!glnx_try_fallocate (tmpf.fd, 0, length, error)) return FALSE; if (G_IS_FILE_DESCRIPTOR_BASED (input)) { int infd = g_file_descriptor_based_get_fd ((GFileDescriptorBased*) input); if (glnx_regfile_copy_bytes (infd, tmpf.fd, (off_t)length) < 0) return glnx_throw_errno_prefix (error, "regfile copy"); } else { /* We used to do a g_output_stream_splice(), but there are two issues with that: * - We want to honor the size provided, to avoid malicious content that says it's * e.g. 10 bytes but is actually gigabytes. * - Due to GLib bugs that pointlessly calls `poll()` on the output fd for every write */ char buf[8192]; guint64 remaining = length; while (remaining > 0) { const gssize bytes_read = g_input_stream_read (input, buf, MIN (remaining, sizeof (buf)), cancellable, error); if (bytes_read < 0) return FALSE; else if (bytes_read == 0) return glnx_throw (error, "Unexpected EOF with %" G_GUINT64_FORMAT "/%" G_GUINT64_FORMAT " bytes remaining", remaining, length); if (glnx_loop_write (tmpf.fd, buf, bytes_read) < 0) return glnx_throw_errno_prefix (error, "write"); remaining -= bytes_read; } } if (!glnx_fchmod (tmpf.fd, 0644, error)) return FALSE; *out_tmpf = tmpf; tmpf.initialized = FALSE; return TRUE; } static gboolean _check_support_reflink (OstreeRepo *dest, gboolean *supported, GError **error) { /* We have not checked yet if the destination file system supports reflinks, do it here */ if (g_atomic_int_get (&dest->fs_support_reflink) == 0) { glnx_autofd int src_fd = -1; g_auto(GLnxTmpfile) dest_tmpf = { 0, }; if (!glnx_openat_rdonly (dest->repo_dir_fd, "config", TRUE, &src_fd, error)) return FALSE; if (!glnx_open_tmpfile_linkable_at (commit_tmp_dfd (dest), ".", O_WRONLY|O_CLOEXEC, &dest_tmpf, error)) return FALSE; if (ioctl (dest_tmpf.fd, FICLONE, src_fd) == 0) g_atomic_int_set (&dest->fs_support_reflink, 1); else if (errno == EOPNOTSUPP) /* Ignore other kind of errors as they might be temporary failures */ g_atomic_int_set (&dest->fs_support_reflink, -1); } *supported = g_atomic_int_get (&dest->fs_support_reflink) >= 0; return TRUE; } static gboolean _create_payload_link (OstreeRepo *self, const char *checksum, const char *payload_checksum, GFileInfo *file_info, GCancellable *cancellable, GError **error) { gboolean reflinks_supported = FALSE; if (!_check_support_reflink (self, &reflinks_supported, error)) return FALSE; if (!reflinks_supported) return TRUE; if (g_file_info_get_file_type (file_info) != G_FILE_TYPE_REGULAR || !G_IN_SET(self->mode, OSTREE_REPO_MODE_BARE, OSTREE_REPO_MODE_BARE_USER, OSTREE_REPO_MODE_BARE_USER_ONLY)) return TRUE; if (payload_checksum == NULL || g_file_info_get_size (file_info) < self->payload_link_threshold) return TRUE; char target_buf[_OSTREE_LOOSE_PATH_MAX + _OSTREE_PAYLOAD_LINK_PREFIX_LEN]; strcpy (target_buf, _OSTREE_PAYLOAD_LINK_PREFIX); _ostree_loose_path (target_buf + _OSTREE_PAYLOAD_LINK_PREFIX_LEN, checksum, OSTREE_OBJECT_TYPE_FILE, self->mode); if (symlinkat (target_buf, commit_tmp_dfd (self), payload_checksum) < 0) { if (errno != EEXIST) return glnx_throw_errno_prefix (error, "symlinkat"); } else { g_auto(OtCleanupUnlinkat) tmp_unlinker = { commit_tmp_dfd (self), g_strdup (payload_checksum) }; if (!commit_path_final (self, payload_checksum, OSTREE_OBJECT_TYPE_PAYLOAD_LINK, &tmp_unlinker, cancellable, error)) return FALSE; } return TRUE; } static gboolean _import_payload_link (OstreeRepo *dest_repo, OstreeRepo *src_repo, const char *checksum, GCancellable *cancellable, GError **error) { gboolean reflinks_supported = FALSE; g_autofree char *payload_checksum = NULL; g_autoptr(GInputStream) is = NULL; glnx_unref_object OtChecksumInstream *checksum_payload = NULL; g_autoptr(GFileInfo) file_info = NULL; /* The two repositories are on different devices */ if (src_repo->device != dest_repo->device) return TRUE; if (!_check_support_reflink (dest_repo, &reflinks_supported, error)) return FALSE; if (!reflinks_supported) return TRUE; if (!G_IN_SET(dest_repo->mode, OSTREE_REPO_MODE_BARE, OSTREE_REPO_MODE_BARE_USER, OSTREE_REPO_MODE_BARE_USER_ONLY)) return TRUE; if (!ostree_repo_load_file (src_repo, checksum, &is, &file_info, NULL, cancellable, error)) return FALSE; if (g_file_info_get_file_type (file_info) != G_FILE_TYPE_REGULAR || g_file_info_get_size (file_info) < dest_repo->payload_link_threshold) return TRUE; checksum_payload = ot_checksum_instream_new (is, G_CHECKSUM_SHA256); guint64 remaining = g_file_info_get_size (file_info); while (remaining) { char buf[8192]; gssize ret = g_input_stream_read ((GInputStream *) checksum_payload, buf, MIN (sizeof (buf), remaining), cancellable, error); if (ret < 0) return FALSE; remaining -= ret; } payload_checksum = ot_checksum_instream_get_string (checksum_payload); return _create_payload_link (dest_repo, checksum, payload_checksum, file_info, cancellable, error); } static gboolean _try_clone_from_payload_link (OstreeRepo *self, OstreeRepo *dest_repo, const char *payload_checksum, GFileInfo *file_info, GLnxTmpfile *tmpf, GCancellable *cancellable, GError **error) { gboolean reflinks_supported = FALSE; int dfd_searches[] = { -1, self->objects_dir_fd }; if (self->commit_stagedir.initialized) dfd_searches[0] = self->commit_stagedir.fd; /* The two repositories are on different devices */ if (self->device != dest_repo->device) return TRUE; if (!_check_support_reflink (dest_repo, &reflinks_supported, error)) return FALSE; if (!reflinks_supported) return TRUE; for (guint i = 0; i < G_N_ELEMENTS (dfd_searches); i++) { glnx_autofd int fdf = -1; char loose_path_buf[_OSTREE_LOOSE_PATH_MAX]; char loose_path_target_buf[_OSTREE_LOOSE_PATH_MAX]; char target_buf[_OSTREE_LOOSE_PATH_MAX + _OSTREE_PAYLOAD_LINK_PREFIX_LEN]; char target_checksum[OSTREE_SHA256_STRING_LEN+1]; int dfd = dfd_searches[i]; ssize_t size; if (dfd == -1) continue; _ostree_loose_path (loose_path_buf, payload_checksum, OSTREE_OBJECT_TYPE_PAYLOAD_LINK, self->mode); size = TEMP_FAILURE_RETRY (readlinkat (dfd, loose_path_buf, target_buf, sizeof (target_buf))); if (size < 0) { if (errno == ENOENT) continue; return glnx_throw_errno_prefix (error, "readlinkat"); } if (size < OSTREE_SHA256_STRING_LEN + _OSTREE_PAYLOAD_LINK_PREFIX_LEN) return glnx_throw (error, "invalid data size for %s", loose_path_buf); sprintf (target_checksum, "%.2s%.62s", target_buf + _OSTREE_PAYLOAD_LINK_PREFIX_LEN, target_buf + _OSTREE_PAYLOAD_LINK_PREFIX_LEN + 3); _ostree_loose_path (loose_path_target_buf, target_checksum, OSTREE_OBJECT_TYPE_FILE, self->mode); if (!ot_openat_ignore_enoent (dfd, loose_path_target_buf, &fdf, error)) return FALSE; if (fdf < 0) { /* If the link is referring to an object that doesn't exist anymore in the repository, just unlink it. */ if (!glnx_unlinkat (dfd, loose_path_buf, 0, error)) return FALSE; } else { /* This undoes all of the previous writes; we want to generate reflinked data. */ if (ftruncate (tmpf->fd, 0) < 0) return glnx_throw_errno_prefix (error, "ftruncate"); if (glnx_regfile_copy_bytes (fdf, tmpf->fd, -1) < 0) return glnx_throw_errno_prefix (error, "regfile copy"); return TRUE; } } if (self->parent_repo) return _try_clone_from_payload_link (self->parent_repo, dest_repo, payload_checksum, file_info, tmpf, cancellable, error); return TRUE; } /* The main driver for writing a content (regfile or symlink) object. * There are a variety of tricky cases here; for example, bare-user * repos store symlinks as regular files. Computing checksums * is optional; if @out_csum is `NULL`, we assume the caller already * knows the checksum. */ static gboolean write_content_object (OstreeRepo *self, const char *expected_checksum, GInputStream *input, GFileInfo *file_info, GVariant *xattrs, guchar **out_csum, GCancellable *cancellable, GError **error) { GLNX_AUTO_PREFIX_ERROR ("Writing content object", error); g_return_val_if_fail (expected_checksum || out_csum, FALSE); if (g_cancellable_set_error_if_cancelled (cancellable, error)) return FALSE; OstreeRepoMode repo_mode = ostree_repo_get_mode (self); GInputStream *file_input; /* Unowned alias */ g_autoptr(GInputStream) file_input_owned = NULL; /* We need a temporary for bare-user symlinks */ glnx_unref_object OtChecksumInstream *checksum_input = NULL; glnx_unref_object OtChecksumInstream *checksum_payload_input = NULL; const GFileType object_file_type = g_file_info_get_file_type (file_info); if (out_csum) { /* Previously we checksummed the input verbatim; now * ostree_repo_write_content() parses without checksumming, then we * re-synthesize a header here. The data should be identical; if somehow * it's not that's not a serious problem because we're still computing a * checksum over the data we actually use. */ gboolean reflinks_supported = FALSE; g_autoptr(GBytes) header = _ostree_file_header_new (file_info, xattrs); size_t len; const guint8 *buf = g_bytes_get_data (header, &len); /* Give a null input if there's no content */ g_autoptr(GInputStream) null_input = NULL; if (!input) { null_input = input = g_memory_input_stream_new_from_data ("", 0, NULL); (void) null_input; /* quiet static analysis */ } checksum_input = ot_checksum_instream_new_with_start (input, G_CHECKSUM_SHA256, buf, len); if (!_check_support_reflink (self, &reflinks_supported, error)) return FALSE; if (xattrs == NULL || !G_IN_SET(self->mode, OSTREE_REPO_MODE_BARE, OSTREE_REPO_MODE_BARE_USER, OSTREE_REPO_MODE_BARE_USER_ONLY) || object_file_type != G_FILE_TYPE_REGULAR || !reflinks_supported) file_input = (GInputStream*)checksum_input; else { /* The payload checksum-input reads from the full object checksum-input; this * means it skips the header. */ checksum_payload_input = ot_checksum_instream_new ((GInputStream*)checksum_input, G_CHECKSUM_SHA256); file_input = (GInputStream*)checksum_payload_input; } } else file_input = input; gboolean phys_object_is_symlink = FALSE; switch (object_file_type) { case G_FILE_TYPE_REGULAR: break; case G_FILE_TYPE_SYMBOLIC_LINK: if (self->mode == OSTREE_REPO_MODE_BARE || self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY) phys_object_is_symlink = TRUE; break; default: return glnx_throw (error, "Unsupported file type %u", object_file_type); } guint64 size; /* For bare-user, convert the symlink target to the input stream */ if (repo_mode == OSTREE_REPO_MODE_BARE_USER && object_file_type == G_FILE_TYPE_SYMBOLIC_LINK) { const char *target_str = g_file_info_get_symlink_target (file_info); g_autoptr(GBytes) target = g_bytes_new (target_str, strlen (target_str) + 1); /* Include the terminating zero so we can e.g. mmap this file */ file_input = file_input_owned = g_memory_input_stream_new_from_bytes (target); size = g_bytes_get_size (target); } else if (!phys_object_is_symlink) size = g_file_info_get_size (file_info); else size = 0; /* Free space check; only applies during transactions */ if ((self->min_free_space_percent > 0 || self->min_free_space_mb > 0) && self->in_transaction) { g_mutex_lock (&self->txn_lock); g_assert_cmpint (self->txn.blocksize, >, 0); const fsblkcnt_t object_blocks = (size / self->txn.blocksize) + 1; if (object_blocks > self->txn.max_blocks) { guint64 bytes_required = (guint64)object_blocks * self->txn.blocksize; self->cleanup_stagedir = TRUE; g_mutex_unlock (&self->txn_lock); return throw_min_free_space_error (self, bytes_required, error); } /* This is the main bit that needs mutex protection */ self->txn.max_blocks -= object_blocks; g_mutex_unlock (&self->txn_lock); } /* For regular files, we create them with default mode, and only * later apply any xattrs and setuid bits. The rationale here * is that an attacker on the network with the ability to MITM * could potentially cause the system to make a temporary setuid * binary with trailing garbage, creating a window on the local * system where a malicious setuid binary exists. * * We use GLnxTmpfile for regular files, and OtCleanupUnlinkat for symlinks. */ g_auto(OtCleanupUnlinkat) tmp_unlinker = { commit_tmp_dfd (self), NULL }; g_auto(GLnxTmpfile) tmpf = { 0, }; goffset unpacked_size = 0; /* Is it a symlink physically? */ if (phys_object_is_symlink) { /* This will not be hit for bare-user or archive */ g_assert (self->mode == OSTREE_REPO_MODE_BARE || self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY); if (!_ostree_make_temporary_symlink_at (commit_tmp_dfd (self), g_file_info_get_symlink_target (file_info), &tmp_unlinker.path, cancellable, error)) return FALSE; } else if (repo_mode != OSTREE_REPO_MODE_ARCHIVE) { if (!create_regular_tmpfile_linkable_with_content (self, size, file_input, &tmpf, cancellable, error)) return FALSE; } else { g_autoptr(GConverter) zlib_compressor = NULL; g_autoptr(GOutputStream) compressed_out_stream = NULL; g_autoptr(GOutputStream) temp_out = NULL; g_assert (repo_mode == OSTREE_REPO_MODE_ARCHIVE); if (!glnx_open_tmpfile_linkable_at (commit_tmp_dfd (self), ".", O_WRONLY|O_CLOEXEC, &tmpf, error)) return FALSE; temp_out = g_unix_output_stream_new (tmpf.fd, FALSE); g_autoptr(GBytes) file_meta_header = _ostree_zlib_file_header_new (file_info, xattrs); gsize file_meta_len; const guint8* file_meta_buf = g_bytes_get_data (file_meta_header, &file_meta_len); { gsize bytes_written; if (!g_output_stream_write_all (temp_out, file_meta_buf, file_meta_len, &bytes_written, cancellable, error)) return FALSE; } if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_REGULAR) { zlib_compressor = (GConverter*)g_zlib_compressor_new (G_ZLIB_COMPRESSOR_FORMAT_RAW, self->zlib_compression_level); compressed_out_stream = g_converter_output_stream_new (temp_out, zlib_compressor); /* Don't close the base; we'll do that later */ g_filter_output_stream_set_close_base_stream ((GFilterOutputStream*)compressed_out_stream, FALSE); if (g_output_stream_splice (compressed_out_stream, file_input, 0, cancellable, error) < 0) return FALSE; unpacked_size = g_file_info_get_size (file_info); } else { /* For a symlink, the size is the length of the target */ unpacked_size = strlen (g_file_info_get_symlink_target (file_info)); } if (!g_output_stream_flush (temp_out, cancellable, error)) return FALSE; if (!glnx_fchmod (tmpf.fd, 0644, error)) return FALSE; } const char *actual_checksum = NULL; g_autofree char *actual_payload_checksum = NULL; g_autofree char *actual_checksum_owned = NULL; if (!checksum_input) actual_checksum = expected_checksum; else { actual_checksum = actual_checksum_owned = ot_checksum_instream_get_string (checksum_input); if (expected_checksum) { if (!_ostree_compare_object_checksum (OSTREE_OBJECT_TYPE_FILE, expected_checksum, actual_checksum, error)) return FALSE; } if (checksum_payload_input) actual_payload_checksum = ot_checksum_instream_get_string (checksum_payload_input); } g_assert (actual_checksum != NULL); /* Pacify static analysis */ /* Update size metadata if configured and entry missing */ if (self->generate_sizes && !repo_has_size_entry (self, OSTREE_OBJECT_TYPE_FILE, actual_checksum)) { struct stat stbuf; if (!glnx_fstat (tmpf.fd, &stbuf, error)) return FALSE; repo_store_size_entry (self, OSTREE_OBJECT_TYPE_FILE, actual_checksum, unpacked_size, stbuf.st_size); } /* See whether or not we have the object, now that we know the * checksum. */ gboolean have_obj; if (!_ostree_repo_has_loose_object (self, actual_checksum, OSTREE_OBJECT_TYPE_FILE, &have_obj, cancellable, error)) return FALSE; /* If we already have it, just update the stats. */ if (have_obj) { g_mutex_lock (&self->txn_lock); self->txn.stats.content_objects_total++; g_mutex_unlock (&self->txn_lock); if (!_create_payload_link (self, actual_checksum, actual_payload_checksum, file_info, cancellable, error)) return FALSE; if (out_csum) *out_csum = ostree_checksum_to_bytes (actual_checksum); /* Note early return */ return TRUE; } const guint32 uid = g_file_info_get_attribute_uint32 (file_info, "unix::uid"); const guint32 gid = g_file_info_get_attribute_uint32 (file_info, "unix::gid"); const guint32 mode = g_file_info_get_attribute_uint32 (file_info, "unix::mode"); /* Is it "physically" a symlink? */ if (phys_object_is_symlink) { if (self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY) { /* We don't store the metadata in bare-user-only, so we're done. */ } else if (self->mode == OSTREE_REPO_MODE_BARE) { /* Now that we know the checksum is valid, apply uid/gid, mode bits, * and extended attributes. * * Note, this does not apply for bare-user repos, as they store symlinks * as regular files. */ if (G_UNLIKELY (fchownat (tmp_unlinker.dfd, tmp_unlinker.path, uid, gid, AT_SYMLINK_NOFOLLOW) == -1)) return glnx_throw_errno_prefix (error, "fchownat"); if (xattrs != NULL) { ot_security_smack_reset_dfd_name (tmp_unlinker.dfd, tmp_unlinker.path); if (!glnx_dfd_name_set_all_xattrs (tmp_unlinker.dfd, tmp_unlinker.path, xattrs, cancellable, error)) return FALSE; } } else { /* We don't do symlinks in archive or bare-user */ g_assert_not_reached (); } if (!commit_path_final (self, actual_checksum, OSTREE_OBJECT_TYPE_FILE, &tmp_unlinker, cancellable, error)) return FALSE; } else { /* Check if a file with the same payload is present in the repository, and in case try to reflink it */ if (actual_payload_checksum && !_try_clone_from_payload_link (self, self, actual_payload_checksum, file_info, &tmpf, cancellable, error)) return FALSE; /* This path is for regular files */ if (!commit_loose_regfile_object (self, actual_checksum, &tmpf, uid, gid, mode, xattrs, cancellable, error)) return FALSE; if (!_create_payload_link (self, actual_checksum, actual_payload_checksum, file_info, cancellable, error)) return FALSE; } /* Update statistics */ g_mutex_lock (&self->txn_lock); self->txn.stats.content_objects_written++; self->txn.stats.content_bytes_written += g_file_info_get_size (file_info); self->txn.stats.content_objects_total++; g_mutex_unlock (&self->txn_lock); if (out_csum) { g_assert (actual_checksum); *out_csum = ostree_checksum_to_bytes (actual_checksum); } return TRUE; } /* A fast path for local commits to `bare` or `bare-user-only` * repos - we basically checksum the file and do a renameat() * into place. * * This could be enhanced down the line to handle cases where we have a modified * stat struct in place; e.g. for `bare` we could do the `chown`, or chmod etc., * and reset the xattrs. * * We could also do this for bare-user, would just involve adding the xattr (and * potentially deleting other ones...not sure if we'd really want e.g. the * security.selinux xattr on setuid binaries and the like to live on). */ static gboolean adopt_and_commit_regfile (OstreeRepo *self, int dfd, const char *name, GFileInfo *finfo, GVariant *xattrs, char *out_checksum_buf, GCancellable *cancellable, GError **error) { GLNX_AUTO_PREFIX_ERROR ("Commit regfile (adopt)", error); g_assert (G_IN_SET (self->mode, OSTREE_REPO_MODE_BARE, OSTREE_REPO_MODE_BARE_USER_ONLY)); g_autoptr(GBytes) header = _ostree_file_header_new (finfo, xattrs); g_auto(OtChecksum) hasher = { 0, }; ot_checksum_init (&hasher); ot_checksum_update_bytes (&hasher, header); glnx_autofd int fd = -1; if (!glnx_openat_rdonly (dfd, name, FALSE, &fd, error)) return FALSE; (void)posix_fadvise (fd, 0, 0, POSIX_FADV_SEQUENTIAL); /* See also https://gist.github.com/cgwalters/0df0d15199009664549618c2188581f0 * and https://github.com/coreutils/coreutils/blob/master/src/ioblksize.h * Turns out bigger block size is better; down the line we should use their * same heuristics. */ char buf[16*1024]; while (TRUE) { ssize_t bytes_read = read (fd, buf, sizeof (buf)); if (bytes_read < 0) return glnx_throw_errno_prefix (error, "read"); if (bytes_read == 0) break; ot_checksum_update (&hasher, (guint8*)buf, bytes_read); } ot_checksum_get_hexdigest (&hasher, out_checksum_buf, OSTREE_SHA256_STRING_LEN+1); const char *checksum = out_checksum_buf; /* TODO: dedup this with commit_path_final() */ char loose_path[_OSTREE_LOOSE_PATH_MAX]; _ostree_loose_path (loose_path, checksum, OSTREE_OBJECT_TYPE_FILE, self->mode); const guint32 src_dev = g_file_info_get_attribute_uint32 (finfo, "unix::device"); const guint64 src_inode = g_file_info_get_attribute_uint64 (finfo, "unix::inode"); int dest_dfd = commit_dest_dfd (self); if (!_ostree_repo_ensure_loose_objdir_at (dest_dfd, loose_path, cancellable, error)) return FALSE; struct stat dest_stbuf; if (!glnx_fstatat_allow_noent (dest_dfd, loose_path, &dest_stbuf, AT_SYMLINK_NOFOLLOW, error)) return FALSE; /* Is the source actually the same device/inode? This can happen with hardlink * checkouts, which is a bit overly conservative for bare-user-only right now. * If so, we can't use renameat() since from `man 2 renameat`: * * "If oldpath and newpath are existing hard links referring to the same file, * then rename() does nothing, and returns a success status." */ if (errno != ENOENT && src_dev == dest_stbuf.st_dev && src_inode == dest_stbuf.st_ino) { if (!glnx_unlinkat (dfd, name, 0, error)) return FALSE; /* Early return */ return TRUE; } /* For bare-user-only we need to canonicalize perms */ if (self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY) { const guint32 src_mode = g_file_info_get_attribute_uint32 (finfo, "unix::mode"); if (fchmod (fd, src_mode & 0755) < 0) return glnx_throw_errno_prefix (error, "fchmod"); } if (renameat (dfd, name, dest_dfd, loose_path) == -1) { if (errno != EEXIST) return glnx_throw_errno_prefix (error, "Storing file '%s'", name); /* We took ownership here, so delete it */ if (!glnx_unlinkat (dfd, name, 0, error)) return FALSE; } return TRUE; } /* Main driver for writing a metadata (non-content) object. */ static gboolean write_metadata_object (OstreeRepo *self, OstreeObjectType objtype, const char *expected_checksum, GBytes *buf, guchar **out_csum, GCancellable *cancellable, GError **error) { GLNX_AUTO_PREFIX_ERROR ("Writing metadata object", error); g_return_val_if_fail (expected_checksum || out_csum, FALSE); if (g_cancellable_set_error_if_cancelled (cancellable, error)) return FALSE; /* In the metadata case, we're not streaming, so we don't bother creating a * tempfile until we compute the checksum. Some metadata like dirmeta is * commonly duplicated, and computing the checksum is going to be cheaper than * making a tempfile. * * However, tombstone commit types don't make sense to checksum, because for * historical reasons we used ostree_repo_write_metadata_trusted() with the * *original* sha256 to say what commit was being killed. */ const gboolean is_tombstone = (objtype == OSTREE_OBJECT_TYPE_TOMBSTONE_COMMIT); char actual_checksum[OSTREE_SHA256_STRING_LEN+1]; if (is_tombstone) { g_assert (expected_checksum != NULL); memcpy (actual_checksum, expected_checksum, sizeof (actual_checksum)); } else { g_auto(OtChecksum) checksum = { 0, }; ot_checksum_init (&checksum); gsize len; const guint8*bufdata = g_bytes_get_data (buf, &len); ot_checksum_update (&checksum, bufdata, len); ot_checksum_get_hexdigest (&checksum, actual_checksum, sizeof (actual_checksum)); gboolean have_obj; if (!_ostree_repo_has_loose_object (self, actual_checksum, objtype, &have_obj, cancellable, error)) return FALSE; /* If we already have the object, we just need to update the tried-to-commit * stat for metadata and be done here. */ if (have_obj) { /* Update size metadata if needed */ if (self->generate_sizes && !repo_has_size_entry (self, objtype, actual_checksum)) repo_store_size_entry (self, objtype, actual_checksum, len, len); g_mutex_lock (&self->txn_lock); self->txn.stats.metadata_objects_total++; g_mutex_unlock (&self->txn_lock); if (out_csum) *out_csum = ostree_checksum_to_bytes (actual_checksum); /* Note early return */ return TRUE; } if (expected_checksum) { if (!_ostree_compare_object_checksum (objtype, expected_checksum, actual_checksum, error)) return FALSE; } } /* Ok, checksum is known, let's get the data */ gsize len; const guint8 *bufp = g_bytes_get_data (buf, &len); /* Update size metadata if needed */ if (self->generate_sizes && !repo_has_size_entry (self, objtype, actual_checksum)) repo_store_size_entry (self, objtype, actual_checksum, len, len); /* Write the metadata to a temporary file */ g_auto(GLnxTmpfile) tmpf = { 0, }; if (!glnx_open_tmpfile_linkable_at (commit_tmp_dfd (self), ".", O_WRONLY|O_CLOEXEC, &tmpf, error)) return FALSE; if (!glnx_try_fallocate (tmpf.fd, 0, len, error)) return FALSE; if (glnx_loop_write (tmpf.fd, bufp, len) < 0) return glnx_throw_errno_prefix (error, "write()"); if (!glnx_fchmod (tmpf.fd, 0644, error)) return FALSE; /* And commit it into place */ if (!_ostree_repo_commit_tmpf_final (self, actual_checksum, objtype, &tmpf, cancellable, error)) return FALSE; if (objtype == OSTREE_OBJECT_TYPE_COMMIT) { GError *local_error = NULL; /* If we are writing a commit, be sure there is no tombstone for it. We may have deleted the commit and now we are trying to pull it again. */ if (!ostree_repo_delete_object (self, OSTREE_OBJECT_TYPE_TOMBSTONE_COMMIT, actual_checksum, cancellable, &local_error)) { if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) g_clear_error (&local_error); else { g_propagate_error (error, local_error); return FALSE; } } } /* Update the stats, note we both wrote one and add to total */ g_mutex_lock (&self->txn_lock); self->txn.stats.metadata_objects_written++; self->txn.stats.metadata_objects_total++; g_mutex_unlock (&self->txn_lock); if (out_csum) *out_csum = ostree_checksum_to_bytes (actual_checksum); return TRUE; } /* Look in a single subdirectory of objects/, building up the * (device,inode) → checksum map. */ static gboolean scan_one_loose_devino (OstreeRepo *self, int object_dir_fd, GHashTable *devino_cache, GCancellable *cancellable, GError **error) { g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; if (!glnx_dirfd_iterator_init_at (object_dir_fd, ".", FALSE, &dfd_iter, error)) return FALSE; while (TRUE) { struct dirent *dent; g_auto(GLnxDirFdIterator) child_dfd_iter = { 0, }; if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error)) return FALSE; if (dent == NULL) break; /* All object directories only have two character entries */ if (strlen (dent->d_name) != 2) continue; if (!glnx_dirfd_iterator_init_at (dfd_iter.fd, dent->d_name, FALSE, &child_dfd_iter, error)) return FALSE; while (TRUE) { struct dirent *child_dent; if (!glnx_dirfd_iterator_next_dent (&child_dfd_iter, &child_dent, cancellable, error)) return FALSE; if (child_dent == NULL) break; const char *name = child_dent->d_name; gboolean skip; switch (self->mode) { case OSTREE_REPO_MODE_ARCHIVE: case OSTREE_REPO_MODE_BARE: case OSTREE_REPO_MODE_BARE_USER: case OSTREE_REPO_MODE_BARE_USER_ONLY: skip = !g_str_has_suffix (name, ".file"); break; default: g_assert_not_reached (); } if (skip) continue; const char *dot = strrchr (name, '.'); g_assert (dot); /* Skip anything that doesn't look like a 64 character checksum */ if ((dot - name) != 62) continue; struct stat stbuf; if (!glnx_fstatat (child_dfd_iter.fd, child_dent->d_name, &stbuf, AT_SYMLINK_NOFOLLOW, error)) return FALSE; OstreeDevIno *key = g_new (OstreeDevIno, 1); key->dev = stbuf.st_dev; key->ino = stbuf.st_ino; memcpy (key->checksum, dent->d_name, 2); memcpy (key->checksum + 2, name, 62); key->checksum[sizeof(key->checksum)-1] = '\0'; g_hash_table_add (devino_cache, key); } } return TRUE; } /* Used by ostree_repo_scan_hardlinks(); see that function for more information. */ static gboolean scan_loose_devino (OstreeRepo *self, GHashTable *devino_cache, GCancellable *cancellable, GError **error) { if (self->parent_repo) { if (!scan_loose_devino (self->parent_repo, devino_cache, cancellable, error)) return FALSE; } if (self->mode == OSTREE_REPO_MODE_ARCHIVE && self->uncompressed_objects_dir_fd != -1) { if (!scan_one_loose_devino (self, self->uncompressed_objects_dir_fd, devino_cache, cancellable, error)) return FALSE; } if (!scan_one_loose_devino (self, self->objects_dir_fd, devino_cache, cancellable, error)) return FALSE; return TRUE; } /* Loook up a (device,inode) pair in our cache, and see if it maps to a known * checksum. */ static const char * devino_cache_lookup (OstreeRepo *self, OstreeRepoCommitModifier *modifier, guint32 device, guint32 inode) { OstreeDevIno dev_ino_key; OstreeDevIno *dev_ino_val; GHashTable *cache; if (self->loose_object_devino_hash) cache = self->loose_object_devino_hash; else if (modifier && modifier->devino_cache) cache = modifier->devino_cache; else return NULL; dev_ino_key.dev = device; dev_ino_key.ino = inode; dev_ino_val = g_hash_table_lookup (cache, &dev_ino_key); if (!dev_ino_val) return NULL; return dev_ino_val->checksum; } /** * ostree_repo_scan_hardlinks: * @self: An #OstreeRepo * @cancellable: Cancellable * @error: Error * * This function is deprecated in favor of using ostree_repo_devino_cache_new(), * which allows a precise mapping to be built up between hardlink checkout files * and their checksums between `ostree_repo_checkout_at()` and * `ostree_repo_write_directory_to_mtree()`. * * When invoking ostree_repo_write_directory_to_mtree(), it has to compute the * checksum of all files. If your commit contains hardlinks from a checkout, * this functions builds a mapping of device numbers and inodes to their * checksum. * * There is an upfront cost to creating this mapping, as this will scan the * entire objects directory. If your commit is composed of mostly hardlinks to * existing ostree objects, then this will speed up considerably, so call it * before you call ostree_repo_write_directory_to_mtree() or similar. However, * ostree_repo_devino_cache_new() is better as it avoids scanning all objects. * * Multithreading: This function is *not* MT safe. */ gboolean ostree_repo_scan_hardlinks (OstreeRepo *self, GCancellable *cancellable, GError **error) { g_return_val_if_fail (self->in_transaction == TRUE, FALSE); if (!self->loose_object_devino_hash) self->loose_object_devino_hash = (GHashTable*)ostree_repo_devino_cache_new (); g_hash_table_remove_all (self->loose_object_devino_hash); return scan_loose_devino (self, self->loose_object_devino_hash, cancellable, error); } /** * ostree_repo_prepare_transaction: * @self: An #OstreeRepo * @out_transaction_resume: (allow-none) (out): Whether this transaction * is resuming from a previous one. This is a legacy state, now OSTree * pulls use per-commit `state/.commitpartial` files. * @cancellable: Cancellable * @error: Error * * Starts or resumes a transaction. In order to write to a repo, you * need to start a transaction. You can complete the transaction with * ostree_repo_commit_transaction(), or abort the transaction with * ostree_repo_abort_transaction(). * * Currently, transactions may result in partial commits or data in the target * repository if interrupted during ostree_repo_commit_transaction(), and * further writing refs is also not currently atomic. * * There can be at most one transaction active on a repo at a time per instance * of `OstreeRepo`; however, it is safe to have multiple threads writing objects * on a single `OstreeRepo` instance as long as their lifetime is bounded by the * transaction. * * Locking: Acquires a `shared` lock; release via commit or abort * Multithreading: This function is *not* MT safe; only one transaction can be * active at a time. */ gboolean ostree_repo_prepare_transaction (OstreeRepo *self, gboolean *out_transaction_resume, GCancellable *cancellable, GError **error) { g_assert (self != NULL); guint64 reserved_bytes = 0; g_return_val_if_fail (self->in_transaction == FALSE, FALSE); g_debug ("Preparing transaction in repository %p", self); /* Set up to abort the transaction if we return early from this function. * This needs to be manually built here due to a circular dependency. */ g_autoptr(OstreeRepoAutoTransaction) txn = g_malloc(sizeof(OstreeRepoAutoTransaction)); g_assert (txn != NULL); txn->repo = self; memset (&self->txn.stats, 0, sizeof (OstreeRepoTransactionStats)); self->txn_locked = ostree_repo_lock_push (self, OSTREE_REPO_LOCK_SHARED, cancellable, error); if (!self->txn_locked) return FALSE; self->in_transaction = TRUE; self->cleanup_stagedir = FALSE; struct statvfs stvfsbuf; if (TEMP_FAILURE_RETRY (fstatvfs (self->repo_dir_fd, &stvfsbuf)) < 0) return glnx_throw_errno_prefix (error, "fstatvfs"); g_mutex_lock (&self->txn_lock); self->txn.blocksize = stvfsbuf.f_bsize; if (!ostree_repo_get_min_free_space_bytes (self, &reserved_bytes, error)) { g_mutex_unlock (&self->txn_lock); return FALSE; } self->reserved_blocks = reserved_bytes / self->txn.blocksize; /* Use the appropriate free block count if we're unprivileged */ guint64 bfree = (getuid () != 0 ? stvfsbuf.f_bavail : stvfsbuf.f_bfree); if (bfree > self->reserved_blocks) self->txn.max_blocks = bfree - self->reserved_blocks; else { self->txn.max_blocks = 0; /* Don't throw_min_free_space_error here; reason being that * this transaction could be just committing metadata objects * which are relatively small in size and we do not really * want to block them via min-free-space-* value. Metadata * objects helps in housekeeping and hence should be kept * out of the strict min-free-space values. * * The main drivers for writing content objects will always honor * the min-free-space value and throw_min_free_space_error in * case of overstepping the number of reserved blocks. */ } g_mutex_unlock (&self->txn_lock); gboolean ret_transaction_resume = FALSE; if (!_ostree_repo_allocate_tmpdir (self->tmp_dir_fd, self->stagedir_prefix, &self->commit_stagedir, &self->commit_stagedir_lock, &ret_transaction_resume, cancellable, error)) return FALSE; /* Success: do not abort the transaction when returning. */ txn->repo = NULL; (void) txn; if (out_transaction_resume) *out_transaction_resume = ret_transaction_resume; return TRUE; } /* Synchronize the directories holding the objects */ static gboolean fsync_object_dirs (OstreeRepo *self, GCancellable *cancellable, GError **error) { GLNX_AUTO_PREFIX_ERROR ("fsync objdirs", error); g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; if (self->disable_fsync) return TRUE; /* No fsync? Nothing to do then. */ if (!glnx_dirfd_iterator_init_at (self->objects_dir_fd, ".", FALSE, &dfd_iter, error)) return FALSE; while (TRUE) { struct dirent *dent; if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&dfd_iter, &dent, cancellable, error)) return FALSE; if (dent == NULL) break; if (dent->d_type != DT_DIR) continue; /* All object directories only have two character entries */ if (strlen (dent->d_name) != 2) continue; glnx_autofd int target_dir_fd = -1; if (!glnx_opendirat (self->objects_dir_fd, dent->d_name, FALSE, &target_dir_fd, error)) return FALSE; /* This synchronizes the directory to ensure all the objects we wrote * are there. We need to do this before removing the .commitpartial * stamp (or have a ref point to the commit). */ if (fsync (target_dir_fd) == -1) return glnx_throw_errno_prefix (error, "fsync"); } /* In case we created any loose object subdirs, make sure they are on disk */ if (fsync (self->objects_dir_fd) == -1) return glnx_throw_errno_prefix (error, "fsync"); return TRUE; } /* Called for commit, to iterate over the "staging" directory and rename all the * objects into the primary objects/ location. Notably this is called only after * syncfs() has potentially been invoked to ensure that all objects have been * written to disk. In the future we may enhance this; see * https://github.com/ostreedev/ostree/issues/1184 */ static gboolean rename_pending_loose_objects (OstreeRepo *self, GCancellable *cancellable, GError **error) { GLNX_AUTO_PREFIX_ERROR ("rename pending", error); g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; if (!glnx_dirfd_iterator_init_at (self->commit_stagedir.fd, ".", FALSE, &dfd_iter, error)) return FALSE; /* Iterate over the outer checksum dir */ while (TRUE) { struct dirent *dent; if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&dfd_iter, &dent, cancellable, error)) return FALSE; if (dent == NULL) break; if (dent->d_type != DT_DIR) continue; /* All object directories only have two character entries */ if (strlen (dent->d_name) != 2) continue; g_auto(GLnxDirFdIterator) child_dfd_iter = { 0, }; if (!glnx_dirfd_iterator_init_at (dfd_iter.fd, dent->d_name, FALSE, &child_dfd_iter, error)) return FALSE; char loose_objpath[_OSTREE_LOOSE_PATH_MAX]; loose_objpath[0] = dent->d_name[0]; loose_objpath[1] = dent->d_name[1]; loose_objpath[2] = '/'; /* Iterate over inner checksum dir */ while (TRUE) { struct dirent *child_dent; if (!glnx_dirfd_iterator_next_dent (&child_dfd_iter, &child_dent, cancellable, error)) return FALSE; if (child_dent == NULL) break; g_strlcpy (loose_objpath + 3, child_dent->d_name, sizeof (loose_objpath)-3); if (!_ostree_repo_ensure_loose_objdir_at (self->objects_dir_fd, loose_objpath, cancellable, error)) return FALSE; if (!glnx_renameat (child_dfd_iter.fd, loose_objpath + 3, self->objects_dir_fd, loose_objpath, error)) return FALSE; } } return TRUE; } /* Try to lock and delete a transaction stage directory created by * ostree_repo_prepare_transaction(). */ static gboolean cleanup_txn_dir (OstreeRepo *self, int dfd, const char *path, GCancellable *cancellable, GError **error) { const char *errprefix = glnx_strjoina ("Cleaning up txn dir ", path); GLNX_AUTO_PREFIX_ERROR (errprefix, error); g_auto(GLnxLockFile) lockfile = { 0, }; gboolean did_lock; /* Try to lock, but if we don't get it, move on */ if (!_ostree_repo_try_lock_tmpdir (dfd, path, &lockfile, &did_lock, error)) return FALSE; if (!did_lock) return TRUE; /* Note early return */ /* If however this is the staging directory for the *current* * boot, then don't delete it now - we may end up reusing it, as * is the point. Delete *only if* we have hit min-free-space* checks * as we don't want to hold onto caches in that case. */ if (g_str_has_prefix (path, self->stagedir_prefix) && !self->cleanup_stagedir) return TRUE; /* Note early return */ /* But, crucially we can now clean up staging directories * from *other* boots. */ if (!glnx_shutil_rm_rf_at (dfd, path, cancellable, error)) return glnx_prefix_error (error, "Removing %s", path); return TRUE; } /* Look in repo/tmp and delete files that are older than a day (by default). * This used to be primarily used by the libsoup fetcher which stored partially * written objects. In practice now that that isn't done anymore, we should * use different logic here. Some more information in * https://github.com/ostreedev/ostree/issues/713 */ static gboolean cleanup_tmpdir (OstreeRepo *self, GCancellable *cancellable, GError **error) { GLNX_AUTO_PREFIX_ERROR ("tmpdir cleanup", error); const guint64 curtime_secs = g_get_real_time () / 1000000; g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; if (!glnx_dirfd_iterator_init_at (self->tmp_dir_fd, ".", TRUE, &dfd_iter, error)) return FALSE; while (TRUE) { struct dirent *dent; if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error)) return FALSE; if (dent == NULL) break; /* Special case this; we create it when opening, and don't want * to blow it away. */ if (strcmp (dent->d_name, "cache") == 0) continue; struct stat stbuf; if (!glnx_fstatat_allow_noent (dfd_iter.fd, dent->d_name, &stbuf, AT_SYMLINK_NOFOLLOW, error)) return FALSE; if (errno == ENOENT) /* Did another cleanup win? */ continue; /* Handle transaction tmpdirs */ if (_ostree_repo_has_staging_prefix (dent->d_name) && S_ISDIR (stbuf.st_mode)) { if (!cleanup_txn_dir (self, dfd_iter.fd, dent->d_name, cancellable, error)) return FALSE; continue; /* We've handled this, move on */ } /* At this point we're looking at an unknown-origin file or directory in * the tmpdir. This could be something like a temporary checkout dir (used * by rpm-ostree), or (from older versions of libostree) a tempfile if we * don't have O_TMPFILE for commits. */ /* Ignore files from the future */ if (stbuf.st_mtime > curtime_secs) continue; /* We're pruning content based on the expiry, which * defaults to a day. That's what we were doing before we * had locking...but in future we can be smarter here. */ guint64 delta = curtime_secs - stbuf.st_mtime; if (delta > self->tmp_expiry_seconds) { if (!glnx_shutil_rm_rf_at (dfd_iter.fd, dent->d_name, cancellable, error)) return glnx_prefix_error (error, "Removing %s", dent->d_name); } } return TRUE; } static void ensure_txn_refs (OstreeRepo *self) { if (self->txn.refs == NULL) self->txn.refs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); if (self->txn.collection_refs == NULL) self->txn.collection_refs = g_hash_table_new_full (ostree_collection_ref_hash, ostree_collection_ref_equal, (GDestroyNotify) ostree_collection_ref_free, g_free); } /** * ostree_repo_mark_commit_partial_reason: * @self: Repo * @checksum: Commit SHA-256 * @is_partial: Whether or not this commit is partial * @in_state: Reason bitmask for partial commit * @error: Error * * Allows the setting of a reason code for a partial commit. Presently * it only supports setting reason bitmask to * OSTREE_REPO_COMMIT_STATE_FSCK_PARTIAL, or * OSTREE_REPO_COMMIT_STATE_NORMAL. This will allow successive ostree * fsck operations to exit properly with an error code if the * repository has been truncated as a result of fsck trying to repair * it. * * Since: 2019.4 */ gboolean ostree_repo_mark_commit_partial_reason (OstreeRepo *self, const char *checksum, gboolean is_partial, OstreeRepoCommitState in_state, GError **error) { g_autofree char *commitpartial_path = _ostree_get_commitpartial_path (checksum); if (is_partial) { glnx_autofd int fd = openat (self->repo_dir_fd, commitpartial_path, O_EXCL | O_CREAT | O_WRONLY | O_CLOEXEC | O_NOCTTY, 0644); if (fd == -1) { if (errno != EEXIST) return glnx_throw_errno_prefix (error, "open(%s)", commitpartial_path); } else { if (in_state & OSTREE_REPO_COMMIT_STATE_FSCK_PARTIAL) if (glnx_loop_write (fd, "f", 1) < 0) return glnx_throw_errno_prefix (error, "write(%s)", commitpartial_path); } } else { if (!ot_ensure_unlinked_at (self->repo_dir_fd, commitpartial_path, 0)) return FALSE; } return TRUE; } /** * ostree_repo_mark_commit_partial: * @self: Repo * @checksum: Commit SHA-256 * @is_partial: Whether or not this commit is partial * @error: Error * * Commits in the "partial" state do not have all their child objects * written. This occurs in various situations, such as during a pull, * but also if a "subpath" pull is used, as well as "commit only" * pulls. * * This function is used by ostree_repo_pull_with_options(); you * should use this if you are implementing a different type of transport. * * Since: 2017.15 */ gboolean ostree_repo_mark_commit_partial (OstreeRepo *self, const char *checksum, gboolean is_partial, GError **error) { return ostree_repo_mark_commit_partial_reason (self, checksum, is_partial, OSTREE_REPO_COMMIT_STATE_NORMAL, error); } /** * ostree_repo_transaction_set_refspec: * @self: An #OstreeRepo * @refspec: The refspec to write * @checksum: (nullable): The checksum to point it to * * Like ostree_repo_transaction_set_ref(), but takes concatenated * @refspec format as input instead of separate remote and name * arguments. * * Multithreading: Since v2017.15 this function is MT safe. */ void ostree_repo_transaction_set_refspec (OstreeRepo *self, const char *refspec, const char *checksum) { g_return_if_fail (self->in_transaction == TRUE); g_mutex_lock (&self->txn_lock); ensure_txn_refs (self); g_hash_table_replace (self->txn.refs, g_strdup (refspec), g_strdup (checksum)); g_mutex_unlock (&self->txn_lock); } /** * ostree_repo_transaction_set_ref: * @self: An #OstreeRepo * @remote: (allow-none): A remote for the ref * @ref: The ref to write * @checksum: (nullable): The checksum to point it to * * If @checksum is not %NULL, then record it as the target of ref named * @ref; if @remote is provided, the ref will appear to originate from that * remote. * * Otherwise, if @checksum is %NULL, then record that the ref should * be deleted. * * The change will be written when the transaction is completed with * ostree_repo_commit_transaction(); that function takes care of writing all of * the objects (such as the commit referred to by @checksum) before updating the * refs. If the transaction is instead aborted with * ostree_repo_abort_transaction(), no changes to the ref will be made to the * repository. * * Note however that currently writing *multiple* refs is not truly atomic; if * the process or system is terminated during * ostree_repo_commit_transaction(), it is possible that just some of the refs * will have been updated. Your application should take care to handle this * case. * * Multithreading: Since v2017.15 this function is MT safe. */ void ostree_repo_transaction_set_ref (OstreeRepo *self, const char *remote, const char *ref, const char *checksum) { g_return_if_fail (self->in_transaction == TRUE); char *refspec; if (remote) refspec = g_strdup_printf ("%s:%s", remote, ref); else refspec = g_strdup (ref); g_mutex_lock (&self->txn_lock); ensure_txn_refs (self); g_hash_table_replace (self->txn.refs, refspec, g_strdup (checksum)); g_mutex_unlock (&self->txn_lock); } /** * ostree_repo_transaction_set_collection_ref: * @self: An #OstreeRepo * @ref: The collection–ref to write * @checksum: (nullable): The checksum to point it to * * If @checksum is not %NULL, then record it as the target of local ref named * @ref. * * Otherwise, if @checksum is %NULL, then record that the ref should * be deleted. * * The change will not be written out immediately, but when the transaction * is completed with ostree_repo_commit_transaction(). If the transaction * is instead aborted with ostree_repo_abort_transaction(), no changes will * be made to the repository. * * Multithreading: Since v2017.15 this function is MT safe. * * Since: 2018.6 */ void ostree_repo_transaction_set_collection_ref (OstreeRepo *self, const OstreeCollectionRef *ref, const char *checksum) { g_return_if_fail (OSTREE_IS_REPO (self)); g_return_if_fail (self->in_transaction == TRUE); g_return_if_fail (ref != NULL); g_return_if_fail (checksum == NULL || ostree_validate_checksum_string (checksum, NULL)); g_mutex_lock (&self->txn_lock); ensure_txn_refs (self); g_hash_table_replace (self->txn.collection_refs, ostree_collection_ref_dup (ref), g_strdup (checksum)); g_mutex_unlock (&self->txn_lock); } /** * ostree_repo_set_ref_immediate: * @self: An #OstreeRepo * @remote: (allow-none): A remote for the ref * @ref: The ref to write * @checksum: (allow-none): The checksum to point it to, or %NULL to unset * @cancellable: GCancellable * @error: GError * * This is like ostree_repo_transaction_set_ref(), except it may be * invoked outside of a transaction. This is presently safe for the * case where we're creating or overwriting an existing ref. * * Multithreading: This function is MT safe. */ gboolean ostree_repo_set_ref_immediate (OstreeRepo *self, const char *remote, const char *ref, const char *checksum, GCancellable *cancellable, GError **error) { const OstreeCollectionRef _ref = { NULL, (gchar *) ref }; return _ostree_repo_write_ref (self, remote, &_ref, checksum, NULL, cancellable, error); } /** * ostree_repo_set_alias_ref_immediate: * @self: An #OstreeRepo * @remote: (allow-none): A remote for the ref * @ref: The ref to write * @target: (allow-none): The ref target to point it to, or %NULL to unset * @cancellable: GCancellable * @error: GError * * Like ostree_repo_set_ref_immediate(), but creates an alias. * * Since: 2017.10 */ gboolean ostree_repo_set_alias_ref_immediate (OstreeRepo *self, const char *remote, const char *ref, const char *target, GCancellable *cancellable, GError **error) { const OstreeCollectionRef _ref = { NULL, (gchar *) ref }; return _ostree_repo_write_ref (self, remote, &_ref, NULL, target, cancellable, error); } /** * ostree_repo_set_collection_ref_immediate: * @self: An #OstreeRepo * @ref: The collection–ref to write * @checksum: (nullable): The checksum to point it to, or %NULL to unset * @cancellable: GCancellable * @error: GError * * This is like ostree_repo_transaction_set_collection_ref(), except it may be * invoked outside of a transaction. This is presently safe for the * case where we're creating or overwriting an existing ref. * * Returns: %TRUE on success, %FALSE otherwise * Since: 2018.6 */ gboolean ostree_repo_set_collection_ref_immediate (OstreeRepo *self, const OstreeCollectionRef *ref, const char *checksum, GCancellable *cancellable, GError **error) { g_return_val_if_fail (OSTREE_IS_REPO (self), FALSE); g_return_val_if_fail (ref != NULL, FALSE); g_return_val_if_fail (checksum == NULL || ostree_validate_checksum_string (checksum, NULL), FALSE); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); return _ostree_repo_write_ref (self, NULL, ref, checksum, NULL, cancellable, error); } /** * ostree_repo_commit_transaction: * @self: An #OstreeRepo * @out_stats: (allow-none) (out): A set of statistics of things * that happened during this transaction. * @cancellable: Cancellable * @error: Error * * Complete the transaction. Any refs set with * ostree_repo_transaction_set_ref() or * ostree_repo_transaction_set_refspec() will be written out. * * Note that if multiple threads are performing writes, all such threads must * have terminated before this function is invoked. * * Locking: Releases `shared` lock acquired by `ostree_repo_prepare_transaction()` * Multithreading: This function is *not* MT safe; only one transaction can be * active at a time. */ gboolean ostree_repo_commit_transaction (OstreeRepo *self, OstreeRepoTransactionStats *out_stats, GCancellable *cancellable, GError **error) { g_return_val_if_fail (self->in_transaction == TRUE, FALSE); g_debug ("Committing transaction in repository %p", self); if ((self->test_error_flags & OSTREE_REPO_TEST_ERROR_PRE_COMMIT) > 0) return glnx_throw (error, "OSTREE_REPO_TEST_ERROR_PRE_COMMIT specified"); /* FIXME: Added OSTREE_SUPPRESS_SYNCFS since valgrind in el7 doesn't know * about `syncfs`...we should delete this later. */ if (!self->disable_fsync && g_getenv ("OSTREE_SUPPRESS_SYNCFS") == NULL) { if (syncfs (self->tmp_dir_fd) < 0) return glnx_throw_errno_prefix (error, "syncfs"); } if (!rename_pending_loose_objects (self, cancellable, error)) return FALSE; if (!fsync_object_dirs (self, cancellable, error)) return FALSE; g_debug ("txn commit %s", glnx_basename (self->commit_stagedir.path)); if (!glnx_tmpdir_delete (&self->commit_stagedir, cancellable, error)) return FALSE; glnx_release_lock_file (&self->commit_stagedir_lock); /* This performs a global cleanup */ if (!cleanup_tmpdir (self, cancellable, error)) return FALSE; if (self->loose_object_devino_hash) g_hash_table_remove_all (self->loose_object_devino_hash); if (self->txn.refs) if (!_ostree_repo_update_refs (self, self->txn.refs, cancellable, error)) return FALSE; if (self->txn.collection_refs) if (!_ostree_repo_update_collection_refs (self, self->txn.collection_refs, cancellable, error)) return FALSE; /* Update the summary if auto-update-summary is set, because doing so was * delayed for each ref change during the transaction. */ if ((self->txn.refs || self->txn.collection_refs) && !_ostree_repo_maybe_regenerate_summary (self, cancellable, error)) return FALSE; g_clear_pointer (&self->txn.refs, g_hash_table_destroy); g_clear_pointer (&self->txn.collection_refs, g_hash_table_destroy); self->in_transaction = FALSE; if (!ot_ensure_unlinked_at (self->repo_dir_fd, "transaction", 0)) return FALSE; if (self->txn_locked) { if (!ostree_repo_lock_pop (self, OSTREE_REPO_LOCK_SHARED, cancellable, error)) return FALSE; self->txn_locked = FALSE; } if (out_stats) *out_stats = self->txn.stats; return TRUE; } /** * ostree_repo_abort_transaction: * @self: An #OstreeRepo * @cancellable: Cancellable * @error: Error * * Abort the active transaction; any staged objects and ref changes will be * discarded. You *must* invoke this if you have chosen not to invoke * ostree_repo_commit_transaction(). Calling this function when not in a * transaction will do nothing and return successfully. */ gboolean ostree_repo_abort_transaction (OstreeRepo *self, GCancellable *cancellable, GError **error) { g_autoptr(GError) cleanup_error = NULL; /* Always ignore the cancellable to avoid the chance that, if it gets * canceled, the transaction may not be fully cleaned up. * See https://github.com/ostreedev/ostree/issues/1491 . */ cancellable = NULL; /* Note early return */ if (!self->in_transaction) return TRUE; g_debug ("Aborting transaction in repository %p", self); if (self->loose_object_devino_hash) g_hash_table_remove_all (self->loose_object_devino_hash); g_clear_pointer (&self->txn.refs, g_hash_table_destroy); g_clear_pointer (&self->txn.collection_refs, g_hash_table_destroy); glnx_tmpdir_unset (&self->commit_stagedir); glnx_release_lock_file (&self->commit_stagedir_lock); /* Do not propagate failures from cleanup_tmpdir() immediately, as we want * to clean up the rest of the internal transaction state first. */ cleanup_tmpdir (self, cancellable, &cleanup_error); self->in_transaction = FALSE; if (self->txn_locked) { if (!ostree_repo_lock_pop (self, OSTREE_REPO_LOCK_SHARED, cancellable, error)) return FALSE; self->txn_locked = FALSE; } /* Propagate cleanup_tmpdir() failure. */ if (cleanup_error != NULL) { g_propagate_error (error, g_steal_pointer (&cleanup_error)); return FALSE; } return TRUE; } /** * ostree_repo_write_metadata: * @self: Repo * @objtype: Object type * @expected_checksum: (allow-none): If provided, validate content against this checksum * @object: Metadata * @out_csum: (out) (array fixed-size=32) (allow-none): Binary checksum * @cancellable: Cancellable * @error: Error * * Store the metadata object @object. Return the checksum * as @out_csum. * * If @expected_checksum is not %NULL, verify it against the * computed checksum. */ gboolean ostree_repo_write_metadata (OstreeRepo *self, OstreeObjectType objtype, const char *expected_checksum, GVariant *object, guchar **out_csum, GCancellable *cancellable, GError **error) { g_autoptr(GVariant) normalized = NULL; /* First, if we have an expected checksum, see if we already have this * object. This mirrors the same logic in ostree_repo_write_content(). */ if (expected_checksum) { gboolean have_obj; if (!_ostree_repo_has_loose_object (self, expected_checksum, objtype, &have_obj, cancellable, error)) return FALSE; if (have_obj) { /* Update size metadata if needed */ if (self->generate_sizes && !repo_has_size_entry (self, objtype, expected_checksum)) { /* Make sure we have a fully serialized object */ g_autoptr(GVariant) trusted = g_variant_get_normal_form (object); gsize size = g_variant_get_size (trusted); repo_store_size_entry (self, objtype, expected_checksum, size, size); } if (out_csum) *out_csum = ostree_checksum_to_bytes (expected_checksum); return TRUE; } /* If the caller is giving us an expected checksum, the object really has * to be normalized already. Otherwise, how would they know the checksum? * There's no sense in redoing it. */ normalized = g_variant_ref (object); } else { normalized = g_variant_get_normal_form (object); } /* For untrusted objects, verify their structure here */ if (expected_checksum) { if (!_ostree_validate_structureof_metadata (objtype, object, error)) return FALSE; } g_autoptr(GBytes) vdata = g_variant_get_data_as_bytes (normalized); if (!write_metadata_object (self, objtype, expected_checksum, vdata, out_csum, cancellable, error)) return FALSE; return TRUE; } /** * ostree_repo_write_metadata_stream_trusted: * @self: Repo * @objtype: Object type * @checksum: Store object with this ASCII SHA256 checksum * @object_input: Metadata object stream * @length: Length, may be 0 for unknown * @cancellable: Cancellable * @error: Error * * Store the metadata object @variant; the provided @checksum is * trusted. */ gboolean ostree_repo_write_metadata_stream_trusted (OstreeRepo *self, OstreeObjectType objtype, const char *checksum, GInputStream *object_input, guint64 length, GCancellable *cancellable, GError **error) { /* This is all pretty ridiculous, but we're keeping this API for backwards * compatibility, it doesn't really need to be fast. */ g_autoptr(GMemoryOutputStream) tmpbuf = (GMemoryOutputStream*)g_memory_output_stream_new_resizable (); if (g_output_stream_splice ((GOutputStream*)tmpbuf, object_input, G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, cancellable, error) < 0) return FALSE; g_autoptr(GBytes) tmpb = g_memory_output_stream_steal_as_bytes (tmpbuf); g_autoptr(GVariant) tmpv = g_variant_new_from_bytes (ostree_metadata_variant_type (objtype), tmpb, TRUE); return ostree_repo_write_metadata_trusted (self, objtype, checksum, tmpv, cancellable, error); } /** * ostree_repo_write_metadata_trusted: * @self: Repo * @objtype: Object type * @checksum: Store object with this ASCII SHA256 checksum * @variant: Metadata object * @cancellable: Cancellable * @error: Error * * Store the metadata object @variant; the provided @checksum is * trusted. */ gboolean ostree_repo_write_metadata_trusted (OstreeRepo *self, OstreeObjectType type, const char *checksum, GVariant *variant, GCancellable *cancellable, GError **error) { return ostree_repo_write_metadata (self, type, checksum, variant, NULL, cancellable, error); } typedef struct { OstreeRepo *repo; OstreeObjectType objtype; char *expected_checksum; GVariant *object; GCancellable *cancellable; GSimpleAsyncResult *result; guchar *result_csum; } WriteMetadataAsyncData; static void write_metadata_async_data_free (gpointer user_data) { WriteMetadataAsyncData *data = user_data; g_clear_object (&data->repo); g_clear_object (&data->cancellable); g_variant_unref (data->object); g_free (data->result_csum); g_free (data->expected_checksum); g_free (data); } static void write_metadata_thread (GSimpleAsyncResult *res, GObject *object, GCancellable *cancellable) { GError *error = NULL; WriteMetadataAsyncData *data; data = g_simple_async_result_get_op_res_gpointer (res); if (!ostree_repo_write_metadata (data->repo, data->objtype, data->expected_checksum, data->object, &data->result_csum, cancellable, &error)) g_simple_async_result_take_error (res, error); } /** * ostree_repo_write_metadata_async: * @self: Repo * @objtype: Object type * @expected_checksum: (allow-none): If provided, validate content against this checksum * @object: Metadata * @cancellable: Cancellable * @callback: Invoked when metadata is writed * @user_data: Data for @callback * * Asynchronously store the metadata object @variant. If provided, * the checksum @expected_checksum will be verified. */ void ostree_repo_write_metadata_async (OstreeRepo *self, OstreeObjectType objtype, const char *expected_checksum, GVariant *object, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { WriteMetadataAsyncData *asyncdata; asyncdata = g_new0 (WriteMetadataAsyncData, 1); asyncdata->repo = g_object_ref (self); asyncdata->objtype = objtype; asyncdata->expected_checksum = g_strdup (expected_checksum); asyncdata->object = g_variant_ref (object); asyncdata->cancellable = cancellable ? g_object_ref (cancellable) : NULL; asyncdata->result = g_simple_async_result_new ((GObject*) self, callback, user_data, ostree_repo_write_metadata_async); g_simple_async_result_set_op_res_gpointer (asyncdata->result, asyncdata, write_metadata_async_data_free); g_simple_async_result_run_in_thread (asyncdata->result, write_metadata_thread, G_PRIORITY_DEFAULT, cancellable); g_object_unref (asyncdata->result); } /** * ostree_repo_write_metadata_finish: * @self: Repo * @result: Result * @out_csum: (out) (array fixed-size=32) (element-type guint8): Binary checksum value * @error: Error * * Complete a call to ostree_repo_write_metadata_async(). */ gboolean ostree_repo_write_metadata_finish (OstreeRepo *self, GAsyncResult *result, guchar **out_csum, GError **error) { GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result); WriteMetadataAsyncData *data; g_warn_if_fail (g_simple_async_result_get_source_tag (simple) == ostree_repo_write_metadata_async); if (g_simple_async_result_propagate_error (simple, error)) return FALSE; data = g_simple_async_result_get_op_res_gpointer (simple); /* Transfer ownership */ *out_csum = data->result_csum; data->result_csum = NULL; return TRUE; } /* Write an object of type OSTREE_OBJECT_TYPE_DIR_META, using @file_info and @xattrs. * Return its (binary) checksum in @out_csum. */ gboolean _ostree_repo_write_directory_meta (OstreeRepo *self, GFileInfo *file_info, GVariant *xattrs, guchar **out_csum, GCancellable *cancellable, GError **error) { if (g_cancellable_set_error_if_cancelled (cancellable, error)) return FALSE; g_autoptr(GVariant) dirmeta = ostree_create_directory_metadata (file_info, xattrs); return ostree_repo_write_metadata (self, OSTREE_OBJECT_TYPE_DIR_META, NULL, dirmeta, out_csum, cancellable, error); } /** * ostree_repo_write_content_trusted: * @self: Repo * @checksum: Store content using this ASCII SHA256 checksum * @object_input: Content stream * @length: Length of @object_input * @cancellable: Cancellable * @error: Data for @callback * * Store the content object streamed as @object_input, with total * length @length. The given @checksum will be treated as trusted. * * This function should be used when importing file objects from local * disk, for example. */ gboolean ostree_repo_write_content_trusted (OstreeRepo *self, const char *checksum, GInputStream *object_input, guint64 length, GCancellable *cancellable, GError **error) { return ostree_repo_write_content (self, checksum, object_input, length, NULL, cancellable, error); } /** * ostree_repo_write_content: * @self: Repo * @expected_checksum: (allow-none): If provided, validate content against this checksum * @object_input: Content object stream * @length: Length of @object_input * @out_csum: (out) (array fixed-size=32) (allow-none): Binary checksum * @cancellable: Cancellable * @error: Error * * Store the content object streamed as @object_input, * with total length @length. The actual checksum will * be returned as @out_csum. */ gboolean ostree_repo_write_content (OstreeRepo *self, const char *expected_checksum, GInputStream *object_input, guint64 length, guchar **out_csum, GCancellable *cancellable, GError **error) { /* First, if we have an expected checksum, see if we already have this * object. This mirrors the same logic in ostree_repo_write_metadata(). * * If size metadata is needed, fall through to write_content_object() * where the entries are made. */ if (expected_checksum && !self->generate_sizes) { gboolean have_obj; if (!_ostree_repo_has_loose_object (self, expected_checksum, OSTREE_OBJECT_TYPE_FILE, &have_obj, cancellable, error)) return FALSE; if (have_obj) { if (out_csum) *out_csum = ostree_checksum_to_bytes (expected_checksum); return TRUE; } } /* Parse the stream */ g_autoptr(GInputStream) file_input = NULL; g_autoptr(GVariant) xattrs = NULL; g_autoptr(GFileInfo) file_info = NULL; if (!ostree_content_stream_parse (FALSE, object_input, length, FALSE, &file_input, &file_info, &xattrs, cancellable, error)) return FALSE; return write_content_object (self, expected_checksum, file_input, file_info, xattrs, out_csum, cancellable, error); } /** * ostree_repo_write_regfile_inline: * @self: repo * @expected_checksum: (allow-none): The expected checksum * @uid: User id * @gid: Group id * @mode: File mode * @xattrs: (allow-none): Extended attributes, GVariant of type (ayay) * @buf: (array length=len) (element-type guint8): File contents * @cancellable: Cancellable * @error: Error * * Synchronously create a file object from the provided content. This API * is intended for small files where it is reasonable to buffer the entire * content in memory. * * Unlike `ostree_repo_write_content()`, if @expected_checksum is provided, * this function will not check for the presence of the object beforehand. * * Returns: (transfer full): Checksum (as a hex string) of the committed file * Since: 2021.2 */ _OSTREE_PUBLIC char * ostree_repo_write_regfile_inline (OstreeRepo *self, const char *expected_checksum, guint32 uid, guint32 gid, guint32 mode, GVariant *xattrs, const guint8* buf, gsize len, GCancellable *cancellable, GError **error) { g_autoptr(GInputStream) memin = g_memory_input_stream_new_from_data (buf, len, NULL); g_autoptr(GFileInfo) finfo = _ostree_mode_uidgid_to_gfileinfo (mode, uid, gid); g_file_info_set_size (finfo, len); g_autofree guint8* csum = NULL; if (!write_content_object (self, expected_checksum, memin, finfo, xattrs, &csum, cancellable, error)) return NULL; return ostree_checksum_from_bytes (csum); } /** * ostree_repo_write_symlink: * @self: repo * @expected_checksum: (allow-none): The expected checksum * @uid: User id * @gid: Group id * @xattrs: (allow-none): Extended attributes, GVariant of type (ayay) * @symlink_target: Target of the symbolic link * @cancellable: Cancellable * @error: Error * * Synchronously create a symlink object. * * Unlike `ostree_repo_write_content()`, if @expected_checksum is provided, * this function will not check for the presence of the object beforehand. * * Returns: (transfer full): Checksum (as a hex string) of the committed file * Since: 2021.2 */ char * ostree_repo_write_symlink (OstreeRepo *self, const char *expected_checksum, guint32 uid, guint32 gid, GVariant *xattrs, const char *symlink_target, GCancellable *cancellable, GError **error) { g_assert (symlink_target != NULL); g_autoptr(GFileInfo) finfo = _ostree_mode_uidgid_to_gfileinfo (S_IFLNK | 0777, uid, gid); g_file_info_set_attribute_byte_string (finfo, "standard::symlink-target", symlink_target); g_autofree guint8* csum = NULL; if (!write_content_object (self, expected_checksum, NULL, finfo, xattrs, &csum, cancellable, error)) return NULL; return ostree_checksum_from_bytes (csum); } /** * ostree_repo_write_regfile: * @self: Repo, * @expected_checksum: (allow-none): Expected checksum (SHA-256 hex string) * @uid: user id * @gid: group id * @mode: Unix file mode * @content_len: Expected content length * @xattrs: (allow-none): Extended attributes (GVariant type `(ayay)`) * @error: Error * * Create an `OstreeContentWriter` that allows streaming output into * the repository. * * Returns: (transfer full): A new writer, or %NULL on error * Since: 2021.2 */ OstreeContentWriter * ostree_repo_write_regfile (OstreeRepo *self, const char *expected_checksum, guint32 uid, guint32 gid, guint32 mode, guint64 content_len, GVariant *xattrs, GError **error) { if (self->mode == OSTREE_REPO_MODE_ARCHIVE) return glnx_null_throw (error, "Cannot currently use ostree_repo_write_regfile() on an archive mode repository"); return _ostree_content_writer_new (self, expected_checksum, uid, gid, mode, content_len, xattrs, error); } typedef struct { OstreeRepo *repo; char *expected_checksum; GInputStream *object; guint64 file_object_length; GCancellable *cancellable; GSimpleAsyncResult *result; guchar *result_csum; } WriteContentAsyncData; static void write_content_async_data_free (gpointer user_data) { WriteContentAsyncData *data = user_data; g_clear_object (&data->repo); g_clear_object (&data->cancellable); g_clear_object (&data->object); g_free (data->result_csum); g_free (data->expected_checksum); g_free (data); } static void write_content_thread (GSimpleAsyncResult *res, GObject *object, GCancellable *cancellable) { GError *error = NULL; WriteContentAsyncData *data; data = g_simple_async_result_get_op_res_gpointer (res); if (!ostree_repo_write_content (data->repo, data->expected_checksum, data->object, data->file_object_length, &data->result_csum, cancellable, &error)) g_simple_async_result_take_error (res, error); } /** * ostree_repo_write_content_async: * @self: Repo * @expected_checksum: (allow-none): If provided, validate content against this checksum * @object: Input * @length: Length of @object * @cancellable: Cancellable * @callback: Invoked when content is writed * @user_data: User data for @callback * * Asynchronously store the content object @object. If provided, the * checksum @expected_checksum will be verified. */ void ostree_repo_write_content_async (OstreeRepo *self, const char *expected_checksum, GInputStream *object, guint64 length, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { WriteContentAsyncData *asyncdata; asyncdata = g_new0 (WriteContentAsyncData, 1); asyncdata->repo = g_object_ref (self); asyncdata->expected_checksum = g_strdup (expected_checksum); asyncdata->object = g_object_ref (object); asyncdata->file_object_length = length; asyncdata->cancellable = cancellable ? g_object_ref (cancellable) : NULL; asyncdata->result = g_simple_async_result_new ((GObject*) self, callback, user_data, ostree_repo_write_content_async); g_simple_async_result_set_op_res_gpointer (asyncdata->result, asyncdata, write_content_async_data_free); g_simple_async_result_run_in_thread (asyncdata->result, write_content_thread, G_PRIORITY_DEFAULT, cancellable); g_object_unref (asyncdata->result); } /** * ostree_repo_write_content_finish: * @self: a #OstreeRepo * @result: a #GAsyncResult * @out_csum: (out) (transfer full): A binary SHA256 checksum of the content object * @error: a #GError * * Completes an invocation of ostree_repo_write_content_async(). */ gboolean ostree_repo_write_content_finish (OstreeRepo *self, GAsyncResult *result, guchar **out_csum, GError **error) { GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result); WriteContentAsyncData *data; g_warn_if_fail (g_simple_async_result_get_source_tag (simple) == ostree_repo_write_content_async); if (g_simple_async_result_propagate_error (simple, error)) return FALSE; data = g_simple_async_result_get_op_res_gpointer (simple); ot_transfer_out_value (out_csum, &data->result_csum); return TRUE; } static GVariant * create_empty_gvariant_dict (void) { GVariantBuilder builder; g_variant_builder_init (&builder, G_VARIANT_TYPE("a{sv}")); return g_variant_builder_end (&builder); } /** * ostree_repo_write_commit: * @self: Repo * @parent: (allow-none): ASCII SHA256 checksum for parent, or %NULL for none * @subject: (allow-none): Subject * @body: (allow-none): Body * @metadata: (allow-none): GVariant of type a{sv}, or %NULL for none * @root: The tree to point the commit to * @out_commit: (out): Resulting ASCII SHA256 checksum for commit * @cancellable: Cancellable * @error: Error * * Write a commit metadata object, referencing @root_contents_checksum * and @root_metadata_checksum. * This uses the current time as the commit timestamp, but it can be * overridden with an explicit timestamp via the * [standard](https://reproducible-builds.org/specs/source-date-epoch/) * `SOURCE_DATE_EPOCH` environment flag. */ gboolean ostree_repo_write_commit (OstreeRepo *self, const char *parent, const char *subject, const char *body, GVariant *metadata, OstreeRepoFile *root, char **out_commit, GCancellable *cancellable, GError **error) { gint64 timestamp = 0; const gchar *env_timestamp = g_getenv ("SOURCE_DATE_EPOCH"); if (env_timestamp == NULL) { g_autoptr(GDateTime) now = g_date_time_new_now_utc (); timestamp = g_date_time_to_unix (now); } else { gchar *ret = NULL; errno = 0; timestamp = g_ascii_strtoll (env_timestamp, &ret, 10); if (errno != 0) return glnx_throw_errno_prefix (error, "Parsing SOURCE_DATE_EPOCH"); if (ret == env_timestamp) return glnx_throw (error, "Failed to convert SOURCE_DATE_EPOCH"); } return ostree_repo_write_commit_with_time (self, parent, subject, body, metadata, root, timestamp, out_commit, cancellable, error); } /** * ostree_repo_write_commit_with_time: * @self: Repo * @parent: (allow-none): ASCII SHA256 checksum for parent, or %NULL for none * @subject: (allow-none): Subject * @body: (allow-none): Body * @metadata: (allow-none): GVariant of type a{sv}, or %NULL for none * @root: The tree to point the commit to * @time: The time to use to stamp the commit * @out_commit: (out): Resulting ASCII SHA256 checksum for commit * @cancellable: Cancellable * @error: Error * * Write a commit metadata object, referencing @root_contents_checksum * and @root_metadata_checksum. */ gboolean ostree_repo_write_commit_with_time (OstreeRepo *self, const char *parent, const char *subject, const char *body, GVariant *metadata, OstreeRepoFile *root, guint64 time, char **out_commit, GCancellable *cancellable, GError **error) { OstreeRepoFile *repo_root = OSTREE_REPO_FILE (root); /* Add sizes information to our metadata object */ g_autoptr(GVariant) new_metadata = add_size_index_to_metadata (self, metadata); g_autoptr(GVariant) commit = g_variant_new ("(@a{sv}@ay@a(say)sst@ay@ay)", new_metadata ? new_metadata : create_empty_gvariant_dict (), parent ? ostree_checksum_to_bytes_v (parent) : ot_gvariant_new_bytearray (NULL, 0), g_variant_new_array (G_VARIANT_TYPE ("(say)"), NULL, 0), subject ? subject : "", body ? body : "", GUINT64_TO_BE (time), ostree_checksum_to_bytes_v (ostree_repo_file_tree_get_contents_checksum (repo_root)), ostree_checksum_to_bytes_v (ostree_repo_file_tree_get_metadata_checksum (repo_root))); g_variant_ref_sink (commit); g_autofree guchar *commit_csum = NULL; if (!ostree_repo_write_metadata (self, OSTREE_OBJECT_TYPE_COMMIT, NULL, commit, &commit_csum, cancellable, error)) return FALSE; g_autofree char *ret_commit = ostree_checksum_from_bytes (commit_csum); ot_transfer_out_value(out_commit, &ret_commit); return TRUE; } /** * ostree_repo_read_commit_detached_metadata: * @self: Repo * @checksum: ASCII SHA256 commit checksum * @out_metadata: (out) (nullable) (transfer full): Metadata associated with commit in with format "a{sv}", or %NULL if none exists * @cancellable: Cancellable * @error: Error * * OSTree commits can have arbitrary metadata associated; this * function retrieves them. If none exists, @out_metadata will be set * to %NULL. */ gboolean ostree_repo_read_commit_detached_metadata (OstreeRepo *self, const char *checksum, GVariant **out_metadata, GCancellable *cancellable, GError **error) { g_assert (out_metadata != NULL); char buf[_OSTREE_LOOSE_PATH_MAX]; _ostree_loose_path (buf, checksum, OSTREE_OBJECT_TYPE_COMMIT_META, self->mode); if (self->commit_stagedir.initialized) { glnx_autofd int fd = -1; if (!ot_openat_ignore_enoent (self->commit_stagedir.fd, buf, &fd, error)) return FALSE; if (fd != -1) return ot_variant_read_fd (fd, 0, G_VARIANT_TYPE ("a{sv}"), TRUE, out_metadata, error); } glnx_autofd int fd = -1; if (!ot_openat_ignore_enoent (self->objects_dir_fd, buf, &fd, error)) return FALSE; if (fd != -1) return ot_variant_read_fd (fd, 0, G_VARIANT_TYPE ("a{sv}"), TRUE, out_metadata, error); if (self->parent_repo) return ostree_repo_read_commit_detached_metadata (self->parent_repo, checksum, out_metadata, cancellable, error); /* Nothing found */ *out_metadata = NULL; return TRUE; } /** * ostree_repo_write_commit_detached_metadata: * @self: Repo * @checksum: ASCII SHA256 commit checksum * @metadata: (allow-none): Metadata to associate with commit in with format "a{sv}", or %NULL to delete * @cancellable: Cancellable * @error: Error * * Replace any existing metadata associated with commit referred to by * @checksum with @metadata. If @metadata is %NULL, then existing * data will be deleted. */ gboolean ostree_repo_write_commit_detached_metadata (OstreeRepo *self, const char *checksum, GVariant *metadata, GCancellable *cancellable, GError **error) { int dest_dfd; if (self->in_transaction) dest_dfd = self->commit_stagedir.fd; else dest_dfd = self->objects_dir_fd; if (!_ostree_repo_ensure_loose_objdir_at (dest_dfd, checksum, cancellable, error)) return FALSE; g_autoptr(GVariant) normalized = NULL; gsize normalized_size = 0; const guint8 *data = NULL; if (metadata != NULL) { normalized = g_variant_get_normal_form (metadata); normalized_size = g_variant_get_size (normalized); data = g_variant_get_data (normalized); } if (data == NULL) data = (guint8*)""; char pathbuf[_OSTREE_LOOSE_PATH_MAX]; _ostree_loose_path (pathbuf, checksum, OSTREE_OBJECT_TYPE_COMMIT_META, self->mode); if (!glnx_file_replace_contents_at (dest_dfd, pathbuf, data, normalized_size, 0, cancellable, error)) { g_prefix_error (error, "Unable to write detached metadata: "); return FALSE; } return TRUE; } /* This generates an in-memory OSTREE_OBJECT_TYPE_DIR_TREE variant, using the * content objects and subdirectories. The input hashes will be sorted */ static GVariant * create_tree_variant_from_hashes (GHashTable *file_checksums, GHashTable *dir_contents_checksums, GHashTable *dir_metadata_checksums) { GVariantBuilder files_builder; g_variant_builder_init (&files_builder, G_VARIANT_TYPE ("a(say)")); GVariantBuilder dirs_builder; g_variant_builder_init (&dirs_builder, G_VARIANT_TYPE ("a(sayay)")); GSList *sorted_filenames = NULL; GLNX_HASH_TABLE_FOREACH (file_checksums, const char*, name) { /* Should have been validated earlier, but be paranoid */ g_assert (ot_util_filename_validate (name, NULL)); sorted_filenames = g_slist_prepend (sorted_filenames, (char*)name); } sorted_filenames = g_slist_sort (sorted_filenames, (GCompareFunc)strcmp); for (GSList *iter = sorted_filenames; iter; iter = iter->next) { const char *name = iter->data; const char *value; value = g_hash_table_lookup (file_checksums, name); g_variant_builder_add (&files_builder, "(s@ay)", name, ostree_checksum_to_bytes_v (value)); } g_slist_free (sorted_filenames); sorted_filenames = NULL; GLNX_HASH_TABLE_FOREACH (dir_metadata_checksums, const char*, name) sorted_filenames = g_slist_prepend (sorted_filenames, (char*)name); sorted_filenames = g_slist_sort (sorted_filenames, (GCompareFunc)strcmp); for (GSList *iter = sorted_filenames; iter; iter = iter->next) { const char *name = iter->data; const char *content_checksum = g_hash_table_lookup (dir_contents_checksums, name); const char *meta_checksum = g_hash_table_lookup (dir_metadata_checksums, name); g_variant_builder_add (&dirs_builder, "(s@ay@ay)", name, ostree_checksum_to_bytes_v (content_checksum), ostree_checksum_to_bytes_v (meta_checksum)); } g_slist_free (sorted_filenames); sorted_filenames = NULL; GVariant *serialized_tree = g_variant_new ("(@a(say)@a(sayay))", g_variant_builder_end (&files_builder), g_variant_builder_end (&dirs_builder)); return g_variant_ref_sink (serialized_tree); } /* If any filtering is set up, perform it, and return modified file info in * @out_modified_info. Note that if no filtering is applied, @out_modified_info * will simply be another reference (with incremented refcount) to @file_info. */ OstreeRepoCommitFilterResult _ostree_repo_commit_modifier_apply (OstreeRepo *self, OstreeRepoCommitModifier *modifier, const char *path, GFileInfo *file_info, GFileInfo **out_modified_info) { gboolean canonicalize_perms = FALSE; gboolean has_filter = FALSE; OstreeRepoCommitFilterResult result = OSTREE_REPO_COMMIT_FILTER_ALLOW; GFileInfo *modified_info; /* Auto-detect bare-user-only repo, force canonical permissions. */ if (self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY) canonicalize_perms = TRUE; if (modifier != NULL) { if ((modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_CANONICAL_PERMISSIONS) != 0) canonicalize_perms = TRUE; if (modifier->filter != NULL) has_filter = TRUE; } if (!(canonicalize_perms || has_filter)) { *out_modified_info = g_object_ref (file_info); return OSTREE_REPO_COMMIT_FILTER_ALLOW; /* Note: early return (no actions needed) */ } modified_info = g_file_info_dup (file_info); if (has_filter) result = modifier->filter (self, path, modified_info, modifier->user_data); if (canonicalize_perms) { guint mode = g_file_info_get_attribute_uint32 (modified_info, "unix::mode"); switch (g_file_info_get_file_type (file_info)) { case G_FILE_TYPE_REGULAR: /* In particular, we want to squash the s{ug}id bits, but this also * catches the sticky bit for example. */ g_file_info_set_attribute_uint32 (modified_info, "unix::mode", mode & (S_IFREG | 0755)); break; case G_FILE_TYPE_DIRECTORY: /* Like the above but for directories */ g_file_info_set_attribute_uint32 (modified_info, "unix::mode", mode & (S_IFDIR | 0755)); break; case G_FILE_TYPE_SYMBOLIC_LINK: break; default: g_assert_not_reached (); } g_file_info_set_attribute_uint32 (modified_info, "unix::uid", 0); g_file_info_set_attribute_uint32 (modified_info, "unix::gid", 0); } *out_modified_info = modified_info; return result; } /* Convert @path into a string */ static char * ptrarray_path_join (GPtrArray *path) { GString *path_buf = g_string_new (""); if (path->len == 0) g_string_append_c (path_buf, '/'); else { for (guint i = 0; i < path->len; i++) { const char *elt = path->pdata[i]; g_string_append_c (path_buf, '/'); g_string_append (path_buf, elt); } } return g_string_free (path_buf, FALSE); } static gboolean get_final_xattrs (OstreeRepo *self, OstreeRepoCommitModifier *modifier, const char *relpath, GFileInfo *file_info, GFile *path, int dfd, const char *dfd_subpath, GVariant *source_xattrs, GVariant **out_xattrs, gboolean *out_modified, GCancellable *cancellable, GError **error) { /* track whether the returned xattrs differ from the file on disk */ gboolean modified = TRUE; const gboolean skip_xattrs = (modifier && (modifier->flags & (OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS | OSTREE_REPO_COMMIT_MODIFIER_FLAGS_CANONICAL_PERMISSIONS)) > 0) || self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY; /* fetch on-disk xattrs if needed & not disabled */ g_autoptr(GVariant) original_xattrs = NULL; if (!skip_xattrs && !self->disable_xattrs) { if (source_xattrs) original_xattrs = g_variant_ref (source_xattrs); else if (path && OSTREE_IS_REPO_FILE (path)) { if (!ostree_repo_file_get_xattrs (OSTREE_REPO_FILE (path), &original_xattrs, cancellable, error)) return FALSE; } else if (path) { if (!glnx_dfd_name_get_all_xattrs (AT_FDCWD, gs_file_get_path_cached (path), &original_xattrs, cancellable, error)) return FALSE; } else if (dfd_subpath == NULL) { g_assert (dfd != -1); if (!glnx_fd_get_all_xattrs (dfd, &original_xattrs, cancellable, error)) return FALSE; } else { g_assert (dfd != -1); if (!glnx_dfd_name_get_all_xattrs (dfd, dfd_subpath, &original_xattrs, cancellable, error)) return FALSE; } g_assert (original_xattrs); } g_autoptr(GVariant) ret_xattrs = NULL; if (modifier && modifier->xattr_callback) { ret_xattrs = modifier->xattr_callback (self, relpath, file_info, modifier->xattr_user_data); } /* if callback returned NULL or didn't exist, default to on-disk state */ if (!ret_xattrs && original_xattrs) ret_xattrs = g_variant_ref (original_xattrs); if (modifier && modifier->sepolicy) { g_autofree char *label = NULL; if (!ostree_sepolicy_get_label (modifier->sepolicy, relpath, g_file_info_get_attribute_uint32 (file_info, "unix::mode"), &label, cancellable, error)) return FALSE; if (!label && (modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_ERROR_ON_UNLABELED) > 0) { return glnx_throw (error, "Failed to look up SELinux label for '%s'", relpath); } else if (label) { g_autoptr(GVariantBuilder) builder = NULL; if (ret_xattrs) { /* drop out any existing SELinux policy from the set, so we don't end up * counting it twice in the checksum */ GVariant* new_ret_xattrs = _ostree_filter_selinux_xattr (ret_xattrs); g_variant_unref (ret_xattrs); ret_xattrs = new_ret_xattrs; } /* ret_xattrs may be NULL */ builder = ot_util_variant_builder_from_variant (ret_xattrs, G_VARIANT_TYPE ("a(ayay)")); g_variant_builder_add_value (builder, g_variant_new ("(@ay@ay)", g_variant_new_bytestring ("security.selinux"), g_variant_new_bytestring (label))); if (ret_xattrs) g_variant_unref (ret_xattrs); ret_xattrs = g_variant_builder_end (builder); g_variant_ref_sink (ret_xattrs); } } if (original_xattrs && ret_xattrs && g_variant_equal (original_xattrs, ret_xattrs)) modified = FALSE; if (out_xattrs) *out_xattrs = g_steal_pointer (&ret_xattrs); if (out_modified) *out_modified = modified; return TRUE; } static gboolean write_directory_to_mtree_internal (OstreeRepo *self, GFile *dir, OstreeMutableTree *mtree, OstreeRepoCommitModifier *modifier, GPtrArray *path, GCancellable *cancellable, GError **error); static gboolean write_dfd_iter_to_mtree_internal (OstreeRepo *self, GLnxDirFdIterator *src_dfd_iter, OstreeMutableTree *mtree, OstreeRepoCommitModifier *modifier, GPtrArray *path, GCancellable *cancellable, GError **error); typedef enum { WRITE_DIR_CONTENT_FLAGS_NONE = 0, WRITE_DIR_CONTENT_FLAGS_CAN_ADOPT = 1, } WriteDirContentFlags; /* Given either a dir_enum or a dfd_iter, writes the directory entry (which is * itself a directory) to the mtree. For subdirs, we go back through either * write_dfd_iter_to_mtree_internal (dfd_iter case) or * write_directory_to_mtree_internal (dir_enum case) which will do the actual * dirmeta + dirent iteration. */ static gboolean write_dir_entry_to_mtree_internal (OstreeRepo *self, OstreeRepoFile *repo_dir, GFileEnumerator *dir_enum, GLnxDirFdIterator *dfd_iter, WriteDirContentFlags writeflags, GFileInfo *child_info, OstreeMutableTree *mtree, OstreeRepoCommitModifier *modifier, GPtrArray *path, GCancellable *cancellable, GError **error) { g_assert (dir_enum != NULL || dfd_iter != NULL); g_assert (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY); const char *name = g_file_info_get_name (child_info); /* We currently only honor the CONSUME flag in the dfd_iter case to avoid even * more complexity in this function, and it'd mostly only be useful when * operating on local filesystems anyways. */ const gboolean delete_after_commit = dfd_iter && modifier && (modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_CONSUME); /* Build the full path which we need for callbacks */ g_ptr_array_add (path, (char*)name); g_autofree char *child_relpath = ptrarray_path_join (path); /* Call the filter */ g_autoptr(GFileInfo) modified_info = NULL; OstreeRepoCommitFilterResult filter_result = _ostree_repo_commit_modifier_apply (self, modifier, child_relpath, child_info, &modified_info); if (filter_result != OSTREE_REPO_COMMIT_FILTER_ALLOW) { g_ptr_array_remove_index (path, path->len - 1); if (delete_after_commit) { g_assert (dfd_iter); if (!glnx_shutil_rm_rf_at (dfd_iter->fd, name, cancellable, error)) return FALSE; } /* Note: early return */ return TRUE; } g_autoptr(GFile) child = NULL; if (dir_enum != NULL) child = g_file_enumerator_get_child (dir_enum, child_info); g_autoptr(OstreeMutableTree) child_mtree = NULL; if (!ostree_mutable_tree_ensure_dir (mtree, name, &child_mtree, error)) return FALSE; /* Finally, recurse on the dir */ if (dir_enum != NULL) { if (!write_directory_to_mtree_internal (self, child, child_mtree, modifier, path, cancellable, error)) return FALSE; } else if (repo_dir) { g_assert (dir_enum != NULL); g_debug ("Adding: %s", gs_file_get_path_cached (child)); if (!ostree_mutable_tree_replace_file (mtree, name, ostree_repo_file_get_checksum ((OstreeRepoFile*) child), error)) return FALSE; } else { g_assert (dfd_iter != NULL); g_auto(GLnxDirFdIterator) child_dfd_iter = { 0, }; if (!glnx_dirfd_iterator_init_at (dfd_iter->fd, name, FALSE, &child_dfd_iter, error)) return FALSE; if (!write_dfd_iter_to_mtree_internal (self, &child_dfd_iter, child_mtree, modifier, path, cancellable, error)) return FALSE; if (delete_after_commit) { if (!glnx_unlinkat (dfd_iter->fd, name, AT_REMOVEDIR, error)) return FALSE; } } g_ptr_array_remove_index (path, path->len - 1); return TRUE; } /* Given either a dir_enum or a dfd_iter, writes a non-dir (regfile/symlink) to * the mtree. */ static gboolean write_content_to_mtree_internal (OstreeRepo *self, OstreeRepoFile *repo_dir, GFileEnumerator *dir_enum, GLnxDirFdIterator *dfd_iter, WriteDirContentFlags writeflags, GFileInfo *child_info, OstreeMutableTree *mtree, OstreeRepoCommitModifier *modifier, GPtrArray *path, GCancellable *cancellable, GError **error) { g_assert (dir_enum != NULL || dfd_iter != NULL); GFileType file_type = g_file_info_get_file_type (child_info); const char *name = g_file_info_get_name (child_info); /* Load flags into boolean constants for ease of readability (we also need to * NULL-check modifier) */ const gboolean canonical_permissions = self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY || (modifier && (modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_CANONICAL_PERMISSIONS)); const gboolean devino_canonical = modifier && (modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_DEVINO_CANONICAL); /* We currently only honor the CONSUME flag in the dfd_iter case to avoid even * more complexity in this function, and it'd mostly only be useful when * operating on local filesystems anyways. */ const gboolean delete_after_commit = dfd_iter && modifier && (modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_CONSUME); /* See if we have a devino hit; this is used below in a few places. */ const char *loose_checksum = NULL; if (dfd_iter != NULL) { guint32 dev = g_file_info_get_attribute_uint32 (child_info, "unix::device"); guint64 inode = g_file_info_get_attribute_uint64 (child_info, "unix::inode"); loose_checksum = devino_cache_lookup (self, modifier, dev, inode); if (loose_checksum && devino_canonical) { /* Go directly to checksum, do not pass Go, do not collect $200. * In this mode the app is required to break hardlinks for any * files it wants to modify. */ if (!ostree_mutable_tree_replace_file (mtree, name, loose_checksum, error)) return FALSE; if (delete_after_commit) { if (!glnx_shutil_rm_rf_at (dfd_iter->fd, name, cancellable, error)) return FALSE; } g_mutex_lock (&self->txn_lock); self->txn.stats.devino_cache_hits++; g_mutex_unlock (&self->txn_lock); return TRUE; /* Early return */ } } /* Build the full path which we need for callbacks */ g_ptr_array_add (path, (char*)name); g_autofree char *child_relpath = ptrarray_path_join (path); /* For bare-user repos we'll reload our file info from the object * (specifically the ostreemeta xattr), if it was checked out that way (via * hardlink). The on-disk state is not normally what we want to commit. * Basically we're making sure that we pick up "real" uid/gid and any xattrs * there. */ g_autoptr(GVariant) source_xattrs = NULL; g_autoptr(GFileInfo) source_child_info = NULL; if (loose_checksum && self->mode == OSTREE_REPO_MODE_BARE_USER) { if (!ostree_repo_load_file (self, loose_checksum, NULL, &source_child_info, &source_xattrs, cancellable, error)) return FALSE; child_info = source_child_info; } /* Call the filter */ g_autoptr(GFileInfo) modified_info = NULL; OstreeRepoCommitFilterResult filter_result = _ostree_repo_commit_modifier_apply (self, modifier, child_relpath, child_info, &modified_info); const gboolean child_info_was_modified = !_ostree_gfileinfo_equal (child_info, modified_info); if (filter_result != OSTREE_REPO_COMMIT_FILTER_ALLOW) { g_ptr_array_remove_index (path, path->len - 1); if (delete_after_commit) { g_assert (dfd_iter); if (!glnx_shutil_rm_rf_at (dfd_iter->fd, name, cancellable, error)) return FALSE; } /* Note: early return */ return TRUE; } switch (file_type) { case G_FILE_TYPE_SYMBOLIC_LINK: case G_FILE_TYPE_REGULAR: break; default: return glnx_throw (error, "Unsupported file type for file: '%s'", child_relpath); } g_autoptr(GFile) child = NULL; if (dir_enum != NULL) child = g_file_enumerator_get_child (dir_enum, child_info); /* Our filters have passed, etc.; now we prepare to write the content object */ glnx_autofd int file_input_fd = -1; /* Open the file now, since it's better for reading xattrs * rather than using the /proc/self/fd links. * * TODO: Do this lazily, since for e.g. bare-user-only repos * we don't have xattrs and don't need to open every file * for things that have devino cache hits. */ if (file_type == G_FILE_TYPE_REGULAR && dfd_iter != NULL) { if (!glnx_openat_rdonly (dfd_iter->fd, name, FALSE, &file_input_fd, error)) return FALSE; } g_autoptr(GVariant) xattrs = NULL; gboolean xattrs_were_modified; if (dir_enum != NULL) { if (!get_final_xattrs (self, modifier, child_relpath, child_info, child, -1, name, source_xattrs, &xattrs, &xattrs_were_modified, cancellable, error)) return FALSE; } else { /* These contortions are basically so we use glnx_fd_get_all_xattrs() * for regfiles, and glnx_dfd_name_get_all_xattrs() for symlinks. */ int xattr_fd_arg = (file_input_fd != -1) ? file_input_fd : dfd_iter->fd; const char *xattr_path_arg = (file_input_fd != -1) ? NULL : name; if (!get_final_xattrs (self, modifier, child_relpath, child_info, child, xattr_fd_arg, xattr_path_arg, source_xattrs, &xattrs, &xattrs_were_modified, cancellable, error)) return FALSE; } /* Used below to see whether we can do a fast path commit */ const gboolean modified_file_meta = child_info_was_modified || xattrs_were_modified; /* A big prerequisite list of conditions for whether or not we can * "adopt", i.e. just checksum and rename() into place */ const gboolean can_adopt_basic = file_type == G_FILE_TYPE_REGULAR && dfd_iter != NULL && delete_after_commit && ((writeflags & WRITE_DIR_CONTENT_FLAGS_CAN_ADOPT) > 0); gboolean can_adopt = can_adopt_basic; /* If basic prerquisites are met, check repo mode specific ones */ if (can_adopt) { /* For bare repos, we could actually chown/reset the xattrs, but let's * do the basic optimizations here first. */ if (self->mode == OSTREE_REPO_MODE_BARE) can_adopt = !modified_file_meta; else if (self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY) can_adopt = canonical_permissions; else /* This covers bare-user and archive. See comments in adopt_and_commit_regfile() * for notes on adding bare-user later here. */ can_adopt = FALSE; } gboolean did_adopt = FALSE; /* The very fast path - we have a devino cache hit, nothing to write */ if (loose_checksum && !modified_file_meta) { if (!ostree_mutable_tree_replace_file (mtree, name, loose_checksum, error)) return FALSE; g_mutex_lock (&self->txn_lock); self->txn.stats.devino_cache_hits++; g_mutex_unlock (&self->txn_lock); } /* Next fast path - we can "adopt" the file */ else if (can_adopt) { char checksum[OSTREE_SHA256_STRING_LEN+1]; if (!adopt_and_commit_regfile (self, dfd_iter->fd, name, modified_info, xattrs, checksum, cancellable, error)) return FALSE; if (!ostree_mutable_tree_replace_file (mtree, name, checksum, error)) return FALSE; did_adopt = TRUE; } else { g_autoptr(GInputStream) file_input = NULL; if (file_type == G_FILE_TYPE_REGULAR) { if (dir_enum != NULL) { g_assert (child != NULL); file_input = (GInputStream*)g_file_read (child, cancellable, error); if (!file_input) return FALSE; } else { /* We already opened the fd above */ file_input = g_unix_input_stream_new (file_input_fd, FALSE); } } g_autofree guchar *child_file_csum = NULL; if (!write_content_object (self, NULL, file_input, modified_info, xattrs, &child_file_csum, cancellable, error)) return FALSE; char tmp_checksum[OSTREE_SHA256_STRING_LEN+1]; ostree_checksum_inplace_from_bytes (child_file_csum, tmp_checksum); if (!ostree_mutable_tree_replace_file (mtree, name, tmp_checksum, error)) return FALSE; } /* Process delete_after_commit. In the adoption case though, we already * took ownership of the file above, usually via a renameat(). */ if (delete_after_commit && !did_adopt) { if (!glnx_unlinkat (dfd_iter->fd, name, 0, error)) return FALSE; } g_ptr_array_remove_index (path, path->len - 1); return TRUE; } /* Handles the dirmeta for the given GFile dir and then calls * write_{dir_entry,content}_to_mtree_internal() for each directory entry. */ static gboolean write_directory_to_mtree_internal (OstreeRepo *self, GFile *dir, OstreeMutableTree *mtree, OstreeRepoCommitModifier *modifier, GPtrArray *path, GCancellable *cancellable, GError **error) { OstreeRepoCommitFilterResult filter_result; OstreeRepoFile *repo_dir = NULL; if (dir) g_debug ("Examining: %s", gs_file_get_path_cached (dir)); /* If the directory is already in the repository, we can try to * reuse checksums to skip checksumming. */ if (dir && OSTREE_IS_REPO_FILE (dir) && modifier == NULL) repo_dir = (OstreeRepoFile *) dir; if (repo_dir) { if (!ostree_repo_file_ensure_resolved (repo_dir, error)) return FALSE; /* ostree_mutable_tree_fill_from_dirtree returns FALSE if mtree isn't * empty: in which case we're responsible for merging the trees. */ if (ostree_mutable_tree_fill_empty_from_dirtree (mtree, ostree_repo_file_get_repo (repo_dir), ostree_repo_file_tree_get_contents_checksum (repo_dir), ostree_repo_file_get_checksum (repo_dir))) return TRUE; ostree_mutable_tree_set_metadata_checksum (mtree, ostree_repo_file_tree_get_metadata_checksum (repo_dir)); filter_result = OSTREE_REPO_COMMIT_FILTER_ALLOW; } else { g_autoptr(GVariant) xattrs = NULL; g_autoptr(GFileInfo) child_info = g_file_query_info (dir, OSTREE_GIO_FAST_QUERYINFO, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, error); if (!child_info) return FALSE; g_autofree char *relpath = NULL; if (modifier != NULL) relpath = ptrarray_path_join (path); g_autoptr(GFileInfo) modified_info = NULL; filter_result = _ostree_repo_commit_modifier_apply (self, modifier, relpath, child_info, &modified_info); if (filter_result == OSTREE_REPO_COMMIT_FILTER_ALLOW) { if (!get_final_xattrs (self, modifier, relpath, child_info, dir, -1, NULL, NULL, &xattrs, NULL, cancellable, error)) return FALSE; g_autofree guchar *child_file_csum = NULL; if (!_ostree_repo_write_directory_meta (self, modified_info, xattrs, &child_file_csum, cancellable, error)) return FALSE; g_autofree char *tmp_checksum = ostree_checksum_from_bytes (child_file_csum); ostree_mutable_tree_set_metadata_checksum (mtree, tmp_checksum); } } if (filter_result == OSTREE_REPO_COMMIT_FILTER_ALLOW) { g_autoptr(GFileEnumerator) dir_enum = NULL; dir_enum = g_file_enumerate_children ((GFile*)dir, OSTREE_GIO_FAST_QUERYINFO, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, error); if (!dir_enum) return FALSE; while (TRUE) { GFileInfo *child_info; if (!g_file_enumerator_iterate (dir_enum, &child_info, NULL, cancellable, error)) return FALSE; if (child_info == NULL) break; if (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY) { if (!write_dir_entry_to_mtree_internal (self, repo_dir, dir_enum, NULL, WRITE_DIR_CONTENT_FLAGS_NONE, child_info, mtree, modifier, path, cancellable, error)) return FALSE; } else { if (!write_content_to_mtree_internal (self, repo_dir, dir_enum, NULL, WRITE_DIR_CONTENT_FLAGS_NONE, child_info, mtree, modifier, path, cancellable, error)) return FALSE; } } } return TRUE; } /* Handles the dirmeta for the dir described by src_dfd_iter and then calls * write_{dir_entry,content}_to_mtree_internal() for each directory entry. */ static gboolean write_dfd_iter_to_mtree_internal (OstreeRepo *self, GLnxDirFdIterator *src_dfd_iter, OstreeMutableTree *mtree, OstreeRepoCommitModifier *modifier, GPtrArray *path, GCancellable *cancellable, GError **error) { g_autoptr(GFileInfo) modified_info = NULL; g_autoptr(GVariant) xattrs = NULL; g_autofree guchar *child_file_csum = NULL; g_autofree char *relpath = NULL; OstreeRepoCommitFilterResult filter_result; struct stat dir_stbuf; if (!glnx_fstat (src_dfd_iter->fd, &dir_stbuf, error)) return FALSE; { g_autoptr(GFileInfo) child_info = _ostree_stbuf_to_gfileinfo (&dir_stbuf); if (modifier != NULL) { relpath = ptrarray_path_join (path); filter_result = _ostree_repo_commit_modifier_apply (self, modifier, relpath, child_info, &modified_info); } else { filter_result = OSTREE_REPO_COMMIT_FILTER_ALLOW; modified_info = g_object_ref (child_info); } } if (filter_result == OSTREE_REPO_COMMIT_FILTER_ALLOW) { if (!get_final_xattrs (self, modifier, relpath, modified_info, NULL, src_dfd_iter->fd, NULL, NULL, &xattrs, NULL, cancellable, error)) return FALSE; if (!_ostree_repo_write_directory_meta (self, modified_info, xattrs, &child_file_csum, cancellable, error)) return FALSE; g_autofree char *tmp_checksum = ostree_checksum_from_bytes (child_file_csum); ostree_mutable_tree_set_metadata_checksum (mtree, tmp_checksum); } if (filter_result != OSTREE_REPO_COMMIT_FILTER_ALLOW) { /* Note - early return */ return TRUE; } /* See if this dir is on the same device; if so we can adopt (if enabled) */ WriteDirContentFlags flags = 0; if (dir_stbuf.st_dev == self->device) flags |= WRITE_DIR_CONTENT_FLAGS_CAN_ADOPT; while (TRUE) { struct dirent *dent; if (!glnx_dirfd_iterator_next_dent (src_dfd_iter, &dent, cancellable, error)) return FALSE; if (dent == NULL) break; struct stat stbuf; if (!glnx_fstatat (src_dfd_iter->fd, dent->d_name, &stbuf, AT_SYMLINK_NOFOLLOW, error)) return FALSE; g_autoptr(GFileInfo) child_info = _ostree_stbuf_to_gfileinfo (&stbuf); g_file_info_set_name (child_info, dent->d_name); if (S_ISDIR (stbuf.st_mode)) { if (!write_dir_entry_to_mtree_internal (self, NULL, NULL, src_dfd_iter, flags, child_info, mtree, modifier, path, cancellable, error)) return FALSE; /* We handled the dir, move onto the next */ continue; } if (S_ISREG (stbuf.st_mode)) ; else if (S_ISLNK (stbuf.st_mode)) { if (!ot_readlinkat_gfile_info (src_dfd_iter->fd, dent->d_name, child_info, cancellable, error)) return FALSE; } else { return glnx_throw (error, "Not a regular file or symlink: %s", dent->d_name); } /* Write a content object, we handled directories above */ if (!write_content_to_mtree_internal (self, NULL, NULL, src_dfd_iter, flags, child_info, mtree, modifier, path, cancellable, error)) return FALSE; } return TRUE; } /** * ostree_repo_write_directory_to_mtree: * @self: Repo * @dir: Path to a directory * @mtree: Overlay directory contents into this tree * @modifier: (allow-none): Optional modifier * @cancellable: Cancellable * @error: Error * * Store objects for @dir and all children into the repository @self, * overlaying the resulting filesystem hierarchy into @mtree. */ gboolean ostree_repo_write_directory_to_mtree (OstreeRepo *self, GFile *dir, OstreeMutableTree *mtree, OstreeRepoCommitModifier *modifier, GCancellable *cancellable, GError **error) { /* Short cut local files */ if (g_file_is_native (dir)) { if (!ostree_repo_write_dfd_to_mtree (self, AT_FDCWD, gs_file_get_path_cached (dir), mtree, modifier, cancellable, error)) return FALSE; } else { _ostree_repo_setup_generate_sizes (self, modifier); g_autoptr(GPtrArray) path = g_ptr_array_new (); if (!write_directory_to_mtree_internal (self, dir, mtree, modifier, path, cancellable, error)) return FALSE; } return TRUE; } /** * ostree_repo_write_dfd_to_mtree: * @self: Repo * @dfd: Directory file descriptor * @path: Path * @mtree: Overlay directory contents into this tree * @modifier: (allow-none): Optional modifier * @cancellable: Cancellable * @error: Error * * Store as objects all contents of the directory referred to by @dfd * and @path all children into the repository @self, overlaying the * resulting filesystem hierarchy into @mtree. */ gboolean ostree_repo_write_dfd_to_mtree (OstreeRepo *self, int dfd, const char *path, OstreeMutableTree *mtree, OstreeRepoCommitModifier *modifier, GCancellable *cancellable, GError **error) { _ostree_repo_setup_generate_sizes (self, modifier); g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; if (!glnx_dirfd_iterator_init_at (dfd, path, FALSE, &dfd_iter, error)) return FALSE; g_autoptr(GPtrArray) pathbuilder = g_ptr_array_new (); if (!write_dfd_iter_to_mtree_internal (self, &dfd_iter, mtree, modifier, pathbuilder, cancellable, error)) return FALSE; /* And now finally remove the toplevel; see also the handling for this flag in * the write_dfd_iter_to_mtree_internal() function. As a special case we don't * try to remove `.` (since we'd get EINVAL); that's what's used in * rpm-ostree. */ const gboolean delete_after_commit = modifier && (modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_CONSUME); if (delete_after_commit && !g_str_equal (path, ".")) { if (!glnx_unlinkat (dfd, path, AT_REMOVEDIR, error)) return FALSE; } return TRUE; } /** * ostree_repo_write_mtree: * @self: Repo * @mtree: Mutable tree * @out_file: (out): An #OstreeRepoFile representing @mtree's root. * @cancellable: Cancellable * @error: Error * * Write all metadata objects for @mtree to repo; the resulting * @out_file points to the %OSTREE_OBJECT_TYPE_DIR_TREE object that * the @mtree represented. */ gboolean ostree_repo_write_mtree (OstreeRepo *self, OstreeMutableTree *mtree, GFile **out_file, GCancellable *cancellable, GError **error) { const char *contents_checksum, *metadata_checksum; g_autoptr(GFile) ret_file = NULL; if (!ostree_mutable_tree_check_error (mtree, error)) return glnx_prefix_error (error, "mtree"); metadata_checksum = ostree_mutable_tree_get_metadata_checksum (mtree); if (!metadata_checksum) return glnx_throw (error, "Can't commit an empty tree"); contents_checksum = ostree_mutable_tree_get_contents_checksum (mtree); if (contents_checksum) { ret_file = G_FILE (_ostree_repo_file_new_root (self, contents_checksum, metadata_checksum)); } else { g_autoptr(GHashTable) dir_metadata_checksums = NULL; g_autoptr(GHashTable) dir_contents_checksums = NULL; g_autoptr(GVariant) serialized_tree = NULL; g_autofree guchar *contents_csum = NULL; char contents_checksum_buf[OSTREE_SHA256_STRING_LEN+1]; dir_contents_checksums = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_free); dir_metadata_checksums = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_free); GLNX_HASH_TABLE_FOREACH_KV (ostree_mutable_tree_get_subdirs (mtree), const char*, name, OstreeMutableTree*, child_dir) { g_autoptr(GFile) child_file = NULL; if (!ostree_repo_write_mtree (self, child_dir, &child_file, cancellable, error)) return FALSE; g_hash_table_replace (dir_contents_checksums, g_strdup (name), g_strdup (ostree_repo_file_tree_get_contents_checksum (OSTREE_REPO_FILE (child_file)))); g_hash_table_replace (dir_metadata_checksums, g_strdup (name), g_strdup (ostree_repo_file_tree_get_metadata_checksum (OSTREE_REPO_FILE (child_file)))); } serialized_tree = create_tree_variant_from_hashes (ostree_mutable_tree_get_files (mtree), dir_contents_checksums, dir_metadata_checksums); if (!ostree_repo_write_metadata (self, OSTREE_OBJECT_TYPE_DIR_TREE, NULL, serialized_tree, &contents_csum, cancellable, error)) return FALSE; ostree_checksum_inplace_from_bytes (contents_csum, contents_checksum_buf); ostree_mutable_tree_set_contents_checksum (mtree, contents_checksum_buf); ret_file = G_FILE (_ostree_repo_file_new_root (self, contents_checksum_buf, metadata_checksum)); } if (out_file) *out_file = g_steal_pointer (&ret_file); return TRUE; } /** * ostree_repo_commit_modifier_new: * @flags: Control options for filter * @commit_filter: (allow-none): Function that can inspect individual files * @user_data: (allow-none): User data * @destroy_notify: A #GDestroyNotify * * Returns: (transfer full): A new commit modifier. */ OstreeRepoCommitModifier * ostree_repo_commit_modifier_new (OstreeRepoCommitModifierFlags flags, OstreeRepoCommitFilter commit_filter, gpointer user_data, GDestroyNotify destroy_notify) { OstreeRepoCommitModifier *modifier = g_new0 (OstreeRepoCommitModifier, 1); modifier->refcount = 1; modifier->flags = flags; modifier->filter = commit_filter; modifier->user_data = user_data; modifier->destroy_notify = destroy_notify; return modifier; } OstreeRepoCommitModifier * ostree_repo_commit_modifier_ref (OstreeRepoCommitModifier *modifier) { gint refcount = g_atomic_int_add (&modifier->refcount, 1); g_assert (refcount > 0); return modifier; } void ostree_repo_commit_modifier_unref (OstreeRepoCommitModifier *modifier) { if (!modifier) return; if (!g_atomic_int_dec_and_test (&modifier->refcount)) return; if (modifier->destroy_notify) modifier->destroy_notify (modifier->user_data); if (modifier->xattr_destroy) modifier->xattr_destroy (modifier->xattr_user_data); g_clear_pointer (&modifier->devino_cache, (GDestroyNotify)g_hash_table_unref); g_clear_object (&modifier->sepolicy); g_free (modifier); return; } /** * ostree_repo_commit_modifier_set_xattr_callback: * @modifier: An #OstreeRepoCommitModifier * @callback: Function to be invoked, should return extended attributes for path * @destroy: Destroy notification * @user_data: Data for @callback: * * If set, this function should return extended attributes to use for * the given path. This is useful for things like ACLs and SELinux, * where a build system can label the files as it's committing to the * repository. */ void ostree_repo_commit_modifier_set_xattr_callback (OstreeRepoCommitModifier *modifier, OstreeRepoCommitModifierXattrCallback callback, GDestroyNotify destroy, gpointer user_data) { modifier->xattr_callback = callback; modifier->xattr_destroy = destroy; modifier->xattr_user_data = user_data; } /** * ostree_repo_commit_modifier_set_sepolicy: * @modifier: An #OstreeRepoCommitModifier * @sepolicy: (allow-none): Policy to use for labeling * * If @policy is non-%NULL, use it to look up labels to use for * "security.selinux" extended attributes. * * Note that any policy specified this way operates in addition to any * extended attributes provided via * ostree_repo_commit_modifier_set_xattr_callback(). However if both * specify a value for "security.selinux", then the one from the * policy wins. */ void ostree_repo_commit_modifier_set_sepolicy (OstreeRepoCommitModifier *modifier, OstreeSePolicy *sepolicy) { g_clear_object (&modifier->sepolicy); modifier->sepolicy = sepolicy ? g_object_ref (sepolicy) : NULL; } /** * ostree_repo_commit_modifier_set_sepolicy_from_commit: * @modifier: Commit modifier * @repo: OSTree repo containing @rev * @rev: Find SELinux policy from this base commit * @cancellable: * @error: * * In many cases, one wants to create a "derived" commit from base commit. * SELinux policy labels are part of that base commit. This API allows * one to easily set up SELinux labeling from a base commit. * * Since: 2020.4 */ gboolean ostree_repo_commit_modifier_set_sepolicy_from_commit (OstreeRepoCommitModifier *modifier, OstreeRepo *repo, const char *rev, GCancellable *cancellable, GError **error) { g_autoptr(OstreeSePolicy) policy = ostree_sepolicy_new_from_commit (repo, rev, cancellable, error); if (!policy) return FALSE; ostree_repo_commit_modifier_set_sepolicy (modifier, policy); return TRUE; } /** * ostree_repo_commit_modifier_set_devino_cache: * @modifier: Modifier * @cache: A hash table caching device,inode to checksums * * See the documentation for * `ostree_repo_devino_cache_new()`. This function can * then be used for later calls to * `ostree_repo_write_directory_to_mtree()` to optimize commits. * * Note if your process has multiple writers, you should use separate * `OSTreeRepo` instances if you want to also use this API. * * This function will add a reference to @cache without copying - you * should avoid further mutation of the cache. * * Since: 2017.13 */ void ostree_repo_commit_modifier_set_devino_cache (OstreeRepoCommitModifier *modifier, OstreeRepoDevInoCache *cache) { modifier->devino_cache = g_hash_table_ref ((GHashTable*)cache); } OstreeRepoDevInoCache * ostree_repo_devino_cache_ref (OstreeRepoDevInoCache *cache) { g_hash_table_ref ((GHashTable*)cache); return cache; } void ostree_repo_devino_cache_unref (OstreeRepoDevInoCache *cache) { g_hash_table_unref ((GHashTable*)cache); } G_DEFINE_BOXED_TYPE(OstreeRepoDevInoCache, ostree_repo_devino_cache, ostree_repo_devino_cache_ref, ostree_repo_devino_cache_unref); G_DEFINE_BOXED_TYPE(OstreeRepoCommitModifier, ostree_repo_commit_modifier, ostree_repo_commit_modifier_ref, ostree_repo_commit_modifier_unref); /* Special case between bare-user and bare-user-only, * mostly for https://github.com/flatpak/flatpak/issues/845 * see below for any more comments. */ static gboolean import_is_bareuser_only_conversion (OstreeRepo *src_repo, OstreeRepo *dest_repo, OstreeObjectType objtype) { return src_repo->mode == OSTREE_REPO_MODE_BARE_USER && dest_repo->mode == OSTREE_REPO_MODE_BARE_USER_ONLY && objtype == OSTREE_OBJECT_TYPE_FILE; } /* Returns TRUE if we can potentially just call link() to copy an object; * if untrusted the repos must be owned by the same uid. */ static gboolean import_via_reflink_is_possible (OstreeRepo *src_repo, OstreeRepo *dest_repo, OstreeObjectType objtype, gboolean trusted) { /* Untrusted pulls require matching ownership */ if (!trusted && (src_repo->owner_uid != dest_repo->owner_uid)) return FALSE; /* Equal modes are always compatible, and metadata * is identical between all modes. */ if (src_repo->mode == dest_repo->mode || OSTREE_OBJECT_TYPE_IS_META (objtype)) return TRUE; /* And now a special case between bare-user and bare-user-only, * mostly for https://github.com/flatpak/flatpak/issues/845 */ if (import_is_bareuser_only_conversion (src_repo, dest_repo, objtype)) return TRUE; return FALSE; } /* Copy the detached metadata for commit @checksum from @source repo * to @self. */ static gboolean copy_detached_metadata (OstreeRepo *self, OstreeRepo *source, const char *checksum, GCancellable *cancellable, GError **error) { g_autoptr(GVariant) detached_meta = NULL; if (!ostree_repo_read_commit_detached_metadata (source, checksum, &detached_meta, cancellable, error)) return FALSE; if (detached_meta) { if (!ostree_repo_write_commit_detached_metadata (self, checksum, detached_meta, cancellable, error)) return FALSE; } return TRUE; } /* Try to import an object via reflink or just linkat(); returns a value in * @out_was_supported if we were able to do it or not. In this path * we're not verifying the checksum. */ static gboolean import_one_object_direct (OstreeRepo *dest_repo, OstreeRepo *src_repo, const char *checksum, OstreeObjectType objtype, gboolean *out_was_supported, GCancellable *cancellable, GError **error) { const char *errprefix = glnx_strjoina ("Importing ", checksum, ".", ostree_object_type_to_string (objtype)); GLNX_AUTO_PREFIX_ERROR (errprefix, error); char loose_path_buf[_OSTREE_LOOSE_PATH_MAX]; _ostree_loose_path (loose_path_buf, checksum, objtype, dest_repo->mode); /* hardlinks require the owner to match and to be on the same device */ const gboolean can_hardlink = src_repo->owner_uid == dest_repo->owner_uid && src_repo->device == dest_repo->device; /* Find our target dfd */ int dest_dfd; if (dest_repo->commit_stagedir.initialized) dest_dfd = dest_repo->commit_stagedir.fd; else dest_dfd = dest_repo->objects_dir_fd; if (!_ostree_repo_ensure_loose_objdir_at (dest_dfd, loose_path_buf, cancellable, error)) return FALSE; gboolean did_hardlink = FALSE; if (can_hardlink) { if (linkat (src_repo->objects_dir_fd, loose_path_buf, dest_dfd, loose_path_buf, 0) != 0) { if (errno == EEXIST) did_hardlink = TRUE; else if (errno == EMLINK || errno == EXDEV || errno == EPERM) { /* EMLINK, EXDEV and EPERM shouldn't be fatal; we just can't do * the optimization of hardlinking instead of copying. Fall * through below. */ } else return glnx_throw_errno_prefix (error, "linkat"); } else did_hardlink = TRUE; } /* If we weren't able to hardlink, fall back to a copy (which might be * reflinked). */ if (!did_hardlink) { struct stat stbuf; if (!glnx_fstatat (src_repo->objects_dir_fd, loose_path_buf, &stbuf, AT_SYMLINK_NOFOLLOW, error)) return FALSE; /* Let's punt for symlinks right now, it's more complicated */ if (!S_ISREG (stbuf.st_mode)) { *out_was_supported = FALSE; return TRUE; } /* This is yet another variation of glnx_file_copy_at() * that basically just optionally does chown(). Perhaps * in the future we should add flags for those things? */ glnx_autofd int src_fd = -1; if (!glnx_openat_rdonly (src_repo->objects_dir_fd, loose_path_buf, FALSE, &src_fd, error)) return FALSE; /* Open a tmpfile for dest */ g_auto(GLnxTmpfile) tmp_dest = { 0, }; if (!glnx_open_tmpfile_linkable_at (dest_dfd, ".", O_WRONLY | O_CLOEXEC, &tmp_dest, error)) return FALSE; if (glnx_regfile_copy_bytes (src_fd, tmp_dest.fd, (off_t) -1) < 0) return glnx_throw_errno_prefix (error, "regfile copy"); /* Only chown for true bare repos */ if (dest_repo->mode == OSTREE_REPO_MODE_BARE) { if (fchown (tmp_dest.fd, stbuf.st_uid, stbuf.st_gid) != 0) return glnx_throw_errno_prefix (error, "fchown"); } /* Don't want to copy xattrs for archive repos, nor for * bare-user-only. We also only do this for content * objects. */ const gboolean src_is_bare_or_bare_user = G_IN_SET (src_repo->mode, OSTREE_REPO_MODE_BARE, OSTREE_REPO_MODE_BARE_USER); if (src_is_bare_or_bare_user && !OSTREE_OBJECT_TYPE_IS_META(objtype)) { if (src_repo->mode == OSTREE_REPO_MODE_BARE) { g_autoptr(GVariant) xattrs = NULL; if (!glnx_fd_get_all_xattrs (src_fd, &xattrs, cancellable, error)) return FALSE; if (!glnx_fd_set_all_xattrs (tmp_dest.fd, xattrs, cancellable, error)) return FALSE; } else if (dest_repo->mode == OSTREE_REPO_MODE_BARE_USER_ONLY) { /* Nothing; this is the "bareuser-only conversion case", * we don't need to set any xattrs in the dest repo. */ } else { /* And this case must be bare-user → bare-user */ g_assert (src_repo->mode == OSTREE_REPO_MODE_BARE_USER); g_assert (src_repo->mode == dest_repo->mode); /* bare-user; we just want ostree.usermeta */ g_autoptr(GBytes) bytes = glnx_fgetxattr_bytes (src_fd, "user.ostreemeta", error); if (bytes == NULL) return FALSE; if (TEMP_FAILURE_RETRY (fsetxattr (tmp_dest.fd, "user.ostreemeta", (char*)g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), 0)) != 0) return glnx_throw_errno_prefix (error, "fsetxattr"); } } if (fchmod (tmp_dest.fd, stbuf.st_mode & ~S_IFMT) != 0) return glnx_throw_errno_prefix (error, "fchmod"); /* For archive repos, we just let the timestamps be object creation. * Otherwise, copy the ostree timestamp value. */ if (_ostree_repo_mode_is_bare (dest_repo->mode)) { struct timespec ts[2]; ts[0] = stbuf.st_atim; ts[1] = stbuf.st_mtim; (void) futimens (tmp_dest.fd, ts); } if (!_ostree_repo_commit_tmpf_final (dest_repo, checksum, objtype, &tmp_dest, cancellable, error)) return FALSE; } if (objtype == OSTREE_OBJECT_TYPE_COMMIT) { if (!copy_detached_metadata (dest_repo, src_repo, checksum, cancellable, error)) return FALSE; } else if (objtype == OSTREE_OBJECT_TYPE_FILE) { if (!_import_payload_link (dest_repo, src_repo, checksum, cancellable, error)) return FALSE; } *out_was_supported = TRUE; return TRUE; } /* A version of ostree_repo_import_object_from_with_trust() * with flags; may make this public API later. */ gboolean _ostree_repo_import_object (OstreeRepo *self, OstreeRepo *source, OstreeObjectType objtype, const char *checksum, OstreeRepoImportFlags flags, GCancellable *cancellable, GError **error) { const gboolean trusted = (flags & _OSTREE_REPO_IMPORT_FLAGS_TRUSTED) > 0; /* Implements OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES which was designed for flatpak */ const gboolean verify_bareuseronly = (flags & _OSTREE_REPO_IMPORT_FLAGS_VERIFY_BAREUSERONLY) > 0; /* A special case between bare-user and bare-user-only, * mostly for https://github.com/flatpak/flatpak/issues/845 */ const gboolean is_bareuseronly_conversion = import_is_bareuser_only_conversion (source, self, objtype); gboolean try_direct = TRUE; /* If we need to do bareuseronly verification, or we're potentially doing a * bareuseronly conversion, let's verify those first so we don't complicate * the rest of the code below. */ if ((verify_bareuseronly || is_bareuseronly_conversion) && !OSTREE_OBJECT_TYPE_IS_META (objtype)) { g_autoptr(GFileInfo) src_finfo = NULL; if (!ostree_repo_load_file (source, checksum, NULL, &src_finfo, NULL, cancellable, error)) return FALSE; if (verify_bareuseronly) { if (!_ostree_validate_bareuseronly_mode_finfo (src_finfo, checksum, error)) return FALSE; } if (is_bareuseronly_conversion) { switch (g_file_info_get_file_type (src_finfo)) { case G_FILE_TYPE_REGULAR: /* This is OK, we'll try a hardlink */ break; case G_FILE_TYPE_SYMBOLIC_LINK: /* Symlinks in bare-user are regular files, we can't * hardlink them to another repo mode. */ try_direct = FALSE; break; default: g_assert_not_reached (); break; } } } /* First, let's see if we can import via reflink/hardlink. */ if (try_direct && import_via_reflink_is_possible (source, self, objtype, trusted)) { /* For local repositories, if the untrusted flag is set, we verify the * checksum first. This assumes then that the files are immutable - the * above check verified that the owner uids match. */ if (!trusted) { if (!ostree_repo_fsck_object (source, objtype, checksum, cancellable, error)) return FALSE; } gboolean direct_was_supported = FALSE; if (!import_one_object_direct (self, source, checksum, objtype, &direct_was_supported, cancellable, error)) return FALSE; /* If direct import succeeded, we're done! */ if (direct_was_supported) return TRUE; } /* The more expensive copy path; involves parsing the object. For * example the input might be an archive repo and the destination bare, * or vice versa. Or we may simply need to verify the checksum. */ /* First, do we have the object already? */ gboolean has_object; if (!ostree_repo_has_object (self, objtype, checksum, &has_object, cancellable, error)) return FALSE; /* If we have it, we're done */ if (has_object) { if (objtype == OSTREE_OBJECT_TYPE_FILE) { if (!_import_payload_link (self, source, checksum, cancellable, error)) return FALSE; } return TRUE; } if (OSTREE_OBJECT_TYPE_IS_META (objtype)) { /* Metadata object */ g_autoptr(GVariant) variant = NULL; if (objtype == OSTREE_OBJECT_TYPE_COMMIT) { /* FIXME - cleanup detached metadata if copy below fails */ if (!copy_detached_metadata (self, source, checksum, cancellable, error)) return FALSE; } if (!ostree_repo_load_variant (source, objtype, checksum, &variant, error)) return FALSE; /* Note this one also now verifies structure in the !trusted case */ g_autofree guchar *real_csum = NULL; if (!ostree_repo_write_metadata (self, objtype, checksum, variant, trusted ? NULL : &real_csum, cancellable, error)) return FALSE; } else { /* Content object */ guint64 length; g_autoptr(GInputStream) object_stream = NULL; if (!ostree_repo_load_object_stream (source, objtype, checksum, &object_stream, &length, cancellable, error)) return FALSE; g_autofree guchar *real_csum = NULL; if (!ostree_repo_write_content (self, checksum, object_stream, length, trusted ? NULL : &real_csum, cancellable, error)) return FALSE; } return TRUE; } static OstreeRepoTransactionStats * ostree_repo_transaction_stats_copy (OstreeRepoTransactionStats *stats) { return g_memdup (stats, sizeof (OstreeRepoTransactionStats)); } static void ostree_repo_transaction_stats_free (OstreeRepoTransactionStats *stats) { return g_free (stats); } G_DEFINE_BOXED_TYPE(OstreeRepoTransactionStats, ostree_repo_transaction_stats, ostree_repo_transaction_stats_copy, ostree_repo_transaction_stats_free);