Skip to content

Commit

Permalink
wip: support for quoting devices
Browse files Browse the repository at this point in the history
This generalizes the whiteout support to handle block/chardev and FIFOs.

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed Oct 2, 2024
1 parent fdfeb0b commit dd9b870
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 9 deletions.
9 changes: 9 additions & 0 deletions man/ostree-commit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ License along with this library. If not, see <https://www.gnu.org/licenses/>.
</para></listitem>
</varlistentry>

<varlistentry>
<term><option>--quote-devices</option></term>
<listitem><para>
By default, ostree rejects block and character devices. This option instead "quotes" them
as regular files. In order to be processed back into block and character devices,
the corresponding <literal>--unquote-devices</literal> must be passed to <literal>ostree checkout</literal>.
</para></listitem>
</varlistentry>

<varlistentry>
<term><option>--no-xattrs</option></term>
<listitem><para>
Expand Down
17 changes: 17 additions & 0 deletions src/libostree/ostree-core-private.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ G_BEGIN_DECLS
*/
#define _OSTREE_ZLIB_FILE_HEADER_GVARIANT_FORMAT G_VARIANT_TYPE ("(tuuuusa(ayay))")

// ostree doesn't have native support for devices. Whiteouts in overlayfs
// are a 0:0 character device, and in some cases people are copying docker/podman
// style overlayfs container storage directly into ostree commits. This
// adds special support for "quoting" the whiteout so it just appears as a regular
// file in the ostree commit, but can be converted back into a character device
// on checkout.
#define OSTREE_QUOTED_OVERLAYFS_WHITEOUT_PREFIX ".ostree-wh."
// Filename prefix to signify a character or block device. This
// is not supported natively by ostree (because there is no reason
// to ship devices in images). But because OCI supports it, and in
// some cases one wants to map OCI to ostree, we have support for
// "quoting" them.
#define OSTREE_QUOTED_DEVICE_PREFIX ".ostree-quoted-device."

GBytes *_ostree_file_header_new (GFileInfo *file_info, GVariant *xattrs);

GBytes *_ostree_zlib_file_header_new (GFileInfo *file_info, GVariant *xattrs);
Expand All @@ -92,6 +106,9 @@ gboolean _ostree_stbuf_equal (struct stat *stbuf_a, struct stat *stbuf_b);
GFileInfo *_ostree_mode_uidgid_to_gfileinfo (mode_t mode, uid_t uid, gid_t gid);
gboolean _ostree_validate_structureof_xattrs (GVariant *xattrs, GError **error);

gboolean _ostree_parse_quoted_device (const char *name, guint32 src_mode, const char **out_name, guint32 *out_mode,
dev_t *out_dev, GError **error);

static inline void
_ostree_checksum_inplace_from_bytes_v (GVariant *csum_v, char *buf)
{
Expand Down
65 changes: 65 additions & 0 deletions src/libostree/ostree-core.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sysmacros.h>

/* Generic ABI checks */
G_STATIC_ASSERT (OSTREE_REPO_MODE_BARE == 0);
Expand Down Expand Up @@ -2331,6 +2332,70 @@ ostree_validate_structureof_dirmeta (GVariant *dirmeta, GError **error)
return TRUE;
}

gboolean
_ostree_parse_quoted_device (const char *name, guint32 src_mode, const char **out_name, guint32 *out_mode, dev_t *out_dev,
GError **error)
{
// Ensure we start with the quoted device prefix
const char *s = name;
const char *p = strchr (s, '.');
if (!p)
return glnx_throw (error, "Invalid quoted device: %s", name);
if (strncmp (s, OSTREE_QUOTED_DEVICE_PREFIX, p - name) != 0)
return glnx_throw (error, "Invalid quoted device: %s", name);
s += strlen (OSTREE_QUOTED_DEVICE_PREFIX);
g_assert (out_name);
*out_name = s;

// The input mode is the same as the source, but without the format bits
guint32 ret_mode = (src_mode & ~S_IFMT);

// Parse the mode
s++;
switch (*s)
{
case 'b':
ret_mode |= S_IFBLK;
break;
case 'c':
ret_mode |= S_IFCHR;
break;
case 'p':
ret_mode |= S_IFIFO;
break;
default:
return glnx_throw (error, "Invalid quoted device: %s", name);
}
s++;
if (*s != '.')
return glnx_throw (error, "Invalid quoted device: %s", name);
s++;
s = strchr (s, '.');
if (!s)
return glnx_throw (error, "Invalid quoted device: %s", name);
s++;
char *endptr;
unsigned int major, minor;
major = (unsigned int)g_ascii_strtoull (s, &endptr, 10);
if (errno == ERANGE)
return glnx_throw (error, "Invalid quoted device: %s", name);
s = endptr;
if (*s != '.')
return glnx_throw (error, "Invalid quoted device: %s", name);
s++;
minor = (unsigned int)g_ascii_strtoull (s, &endptr, 10);
if (errno == ERANGE)
return glnx_throw (error, "Invalid quoted device: %s", name);
g_assert (endptr);
if (*endptr != '\0')
return glnx_throw (error, "Invalid quoted device: %s", name);
g_assert (ret_mode);
*out_mode = ret_mode;
g_assert (out_dev);
*out_dev = makedev (major, minor);
return TRUE;
}

/**
* ostree_commit_get_parent:
* @commit_variant: Variant of type %OSTREE_OBJECT_TYPE_COMMIT
Expand Down
24 changes: 16 additions & 8 deletions src/libostree/ostree-repo-checkout.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,6 @@
#define WHITEOUT_PREFIX ".wh."
#define OPAQUE_WHITEOUT_NAME ".wh..wh..opq"

// ostree doesn't have native support for devices. Whiteouts in overlayfs
// are a 0:0 character device, and in some cases people are copying docker/podman
// style overlayfs container storage directly into ostree commits. This
// adds special support for "quoting" the whiteout so it just appears as a regular
// file in the ostree commit, but can be converted back into a character device
// on checkout.
#define OSTREE_QUOTED_OVERLAYFS_WHITEOUT_PREFIX ".ostree-wh."

/* Per-checkout call state/caching */
typedef struct
{
Expand Down Expand Up @@ -716,6 +708,9 @@ checkout_one_file_at (OstreeRepo *repo, OstreeRepoCheckoutAtOptions *options, Ch
const gboolean is_unreadable = (!is_symlink && (source_mode & S_IRUSR) == 0);
const gboolean is_whiteout = (!is_symlink && options->process_whiteouts
&& g_str_has_prefix (destination_name, WHITEOUT_PREFIX));
const gboolean is_quoted_device
= (!is_symlink && options->unquote_devices
&& g_str_has_prefix (destination_name, OSTREE_QUOTED_DEVICE_PREFIX));
const gboolean is_overlayfs_whiteout
= (!is_symlink
&& g_str_has_prefix (destination_name, OSTREE_QUOTED_OVERLAYFS_WHITEOUT_PREFIX));
Expand All @@ -740,6 +735,16 @@ checkout_one_file_at (OstreeRepo *repo, OstreeRepoCheckoutAtOptions *options, Ch

need_copy = FALSE;
}
else if (is_quoted_device)
{
const char *devname;
dev_t dev;
guint32 mode;
if (!_ostree_parse_quoted_device (destination_name, source_mode, &devname, &mode, &dev, error))
return FALSE;
if (mknodat (destination_dfd, devname, (mode_t)mode, dev) < 0)
return glnx_throw_errno_prefix (error, "mknodat");
}
else if (is_overlayfs_whiteout && options->process_passthrough_whiteouts)
{
const char *name = destination_name + (sizeof (OSTREE_QUOTED_OVERLAYFS_WHITEOUT_PREFIX) - 1);
Expand Down Expand Up @@ -1437,6 +1442,9 @@ canonicalize_options (OstreeRepo *self, OstreeRepoCheckoutAtOptions *options)
/* Force USER mode for BARE_USER_ONLY always - nothing else makes sense */
if (ostree_repo_get_mode (self) == OSTREE_REPO_MODE_BARE_USER_ONLY)
options->mode = OSTREE_REPO_CHECKOUT_MODE_USER;

if (options->unquote_devices)
options->process_whiteouts = TRUE;
}

/**
Expand Down
204 changes: 204 additions & 0 deletions src/libostree/ostree-repo-commit.c
Original file line number Diff line number Diff line change
Expand Up @@ -3450,6 +3450,202 @@ write_dir_entry_to_mtree_internal (OstreeRepo *self, OstreeRepoFile *repo_dir,
return TRUE;
}

static gboolean
write_quoted_device (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);

/* 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);
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;
}

guint32 src_mode = g_file_info_get_attribute_uint32 (src_info, "unix::mode")';'
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;
}

/* Given either a dir_enum or a dfd_iter, writes a non-dir (regfile/symlink) to
* the mtree.
*/
Expand Down Expand Up @@ -3889,6 +4085,14 @@ write_dfd_iter_to_mtree_internal (OstreeRepo *self, GLnxDirFdIterator *src_dfd_i
error))
return FALSE;
}
else if (modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_QUOTE_DEVICES)
{
if (!write_quoted_device (self, NULL, NULL, src_dfd_iter, flags, child_info, mtree,
modifier, path, cancellable, error))
return FALSE;
// Note we skip over the code below
continue;
}
else
{
return glnx_throw (error, "Not a regular file or symlink: %s", dent->d_name);
Expand Down
Loading

0 comments on commit dd9b870

Please sign in to comment.