Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: support for quoting devices #3318

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading