Skip to content

Commit

Permalink
Add API+CLI to get fsverity digests efficiently
Browse files Browse the repository at this point in the history
- Add an API that first tries to ask the kernel for the fsverity
  digest, and returns it if present, but otherwise does a fallback
  to in-memory computation.  The primary use case here
  is systems which want to canonically index files by their fsverity
  digest (as opposed to e.g. sha256).  If the filesystem in the
  future starts supporting fsverity (as e.g. XFS is slated to do)
  then this allows an efficient in-place upgrade.
- Expose the measurement in the CLI.  `fsverity-utils` isn't
  installed everywhere, and also today composefs makes hard decisions
  (like using sha256) that that project doesn't do.

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed May 27, 2024
1 parent 650aa59 commit ef85111
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 4 deletions.
3 changes: 3 additions & 0 deletions libcomposefs/lcfs-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ typedef int errint_t;
*/
#define LCFS_BUILD_INLINE_FILE_SIZE_LIMIT 64

/* What may be returned by the kernel for digests */
#define MAX_DIGEST_SIZE 64

#define OVERLAY_XATTR_USER_PREFIX "user."
#define OVERLAY_XATTR_TRUSTED_PREFIX "trusted."
#define OVERLAY_XATTR_PARTIAL_PREFIX "overlay."
Expand Down
2 changes: 0 additions & 2 deletions libcomposefs/lcfs-mount.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,6 @@ static int syscall_mount_setattr(int dfd, const char *path, unsigned int flags,
}
#endif

#define MAX_DIGEST_SIZE 64

struct lcfs_mount_state_s {
const char *image_path;
const char *mountpoint;
Expand Down
44 changes: 44 additions & 0 deletions libcomposefs/lcfs-writer.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@
#include "lcfs-writer.h"
#include "lcfs-utils.h"
#include "lcfs-fsverity.h"
#include "lcfs-mount.h"
#include "lcfs-erofs.h"
#include "hash.h"

#include <errno.h>
#include <string.h>
#include <linux/fsverity.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/xattr.h>
Expand Down Expand Up @@ -521,12 +524,53 @@ int lcfs_compute_fsverity_from_content(uint8_t *digest, void *file, lcfs_read_cb
return 0;
}

// Given a file descriptor, perform an in-memory computation of its fsverity
// digest. Note that the computation starts from the current offset position of
// the file.
int lcfs_compute_fsverity_from_fd(uint8_t *digest, int fd)
{
int _fd = fd;
return lcfs_compute_fsverity_from_content(digest, &_fd, fsverity_read_cb);
}

// Given a file descriptor, first query the kernel for its fsverity digest. If
// it is not available in the kernel, perform an in-memory computation. The file
// position will always be reset to zero if needed.
int lcfs_fd_get_fsverity(uint8_t *digest, int fd)
{
struct {
struct fsverity_digest fsv;
char buf[MAX_DIGEST_SIZE];
} buf;

// First, ask the kernel if the file already has fsverity; if so we just return
// that.
buf.fsv.digest_size = MAX_DIGEST_SIZE;
int res = ioctl(fd, FS_IOC_MEASURE_VERITY, &buf.fsv);
if (res == -1) {
// Under this condition, the file didn't have fsverity enabled or the
// kernel doesn't support it at all. We need to compute it in the current process.
if (errno == ENODATA || errno == EOPNOTSUPP || errno == ENOTTY) {
// For consistency ensure we start from the beginning. We could
// avoid this by using pread() in the future.
if (lseek(fd, 0, SEEK_SET) < 0)
return -errno;
return lcfs_compute_fsverity_from_fd(digest, fd);
}
// In this case, we found an unexpected error
return -errno;
}
// The file has fsverity enabled, but with an unexpected different algorithm (e.g. sha512).
// This is going to be a weird corner case. For now, we error out.
if (buf.fsv.digest_size != LCFS_DIGEST_SIZE) {
return -EWRONGVERITY;
}

memcpy(digest, buf.buf, LCFS_DIGEST_SIZE);

return 0;
}

int lcfs_compute_fsverity_from_data(uint8_t *digest, uint8_t *data, size_t data_len)
{
FsVerityContext *ctx;
Expand Down
1 change: 1 addition & 0 deletions libcomposefs/lcfs-writer.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ LCFS_EXTERN int lcfs_compute_fsverity_from_content(uint8_t *digest, void *file,
LCFS_EXTERN int lcfs_compute_fsverity_from_fd(uint8_t *digest, int fd);
LCFS_EXTERN int lcfs_compute_fsverity_from_data(uint8_t *digest, uint8_t *data,
size_t data_len);
LCFS_EXTERN int lcfs_fd_get_fsverity(uint8_t *digest, int fd);

LCFS_EXTERN int lcfs_node_set_from_content(struct lcfs_node_s *node, int dirfd,
const char *fname, int buildflags);
Expand Down
3 changes: 3 additions & 0 deletions man/composefs-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ several sub-commands:
accepted as input to mkcomposefs if the --from-file
option is used.

**measure-file**
: Interpret the provided paths as generic files, and print their fsverity digest.

# OPTIONS

The provided *IMAGE* argument must be a composefs file. Multiple images
Expand Down
7 changes: 7 additions & 0 deletions tests/test-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ check_fsverity () {
return 0
}

assert_streq () {
if test "$1" != "$2"; then
echo "assertion failed: $1 = $2" 1>&2
return 1
fi
}

[[ -v can_whiteout ]] || can_whiteout=$(check_whiteout)
[[ -v has_fuse ]] || has_fuse=$(if check_fuse; then echo y; else echo n; fi)
[[ -v has_fsck ]] || has_fsck=$(check_erofs_fsck)
Expand Down
20 changes: 19 additions & 1 deletion tests/test-units.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,25 @@ function test_mount_digest () {
fi
}

TESTS="test_inline test_objects test_mount_digest"
function test_composefs_info_measure_files () {
local dir=$1
cd $dir

echo hello world > test.txt
echo foo bar baz > test2.txt
composefs-info measure-file test.txt test2.txt > out.txt
assert_streq "$(head -1 out.txt)" "37061ef2ac4c21bec68489b56138c5780306a4ad7fe6676236ecdf2c9027cd92"
assert_streq "$(tail -1 out.txt)" "91e7d88cb7bc9cf6d8db3b0ecf89af4abf204bef5b3ade5113d5b62ef374e70b"

if [ $has_fsverity = y ]; then
fsverity enable --hash-alg=256 test.txt
digest=$(composefs-info measure-file test.txt)
assert_streq "$digest" "37061ef2ac4c21bec68489b56138c5780306a4ad7fe6676236ecdf2c9027cd92"
fi
cd -
}

TESTS="test_inline test_objects test_mount_digest test_composefs_info_measure_files"
res=0
for i in $TESTS; do
testdir=$(mktemp -d $workdir/$i.XXXXXX)
Expand Down
37 changes: 36 additions & 1 deletion tools/composefs-info.c
Original file line number Diff line number Diff line change
Expand Up @@ -361,12 +361,45 @@ static void print_objects_handler_end(void *_data)
static void usage(const char *argv0)
{
fprintf(stderr,
"usage: %s [--basedir=path] [ls|objects|dump|missing-objects] IMAGES...\n",
"usage: %s [--basedir=path] [ls|objects|dump|missing-objects|measure-file] IMAGES...\n",
argv0);
}

#define OPT_BASEDIR 100

// Most of the rest of this code operates on composefs superblocks. This function
// just prints the fsverity digest of the provided files.
static int measure_files(const char *bin, int argc, char **argv)
{
if (argc <= 2) {
fprintf(stderr, "No files specified\n");
usage(bin);
exit(1);
}

for (int i = 2; i < argc; i++) {
const char *path = argv[i];
uint8_t digest[LCFS_DIGEST_SIZE];

cleanup_fd int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
err(EXIT_FAILURE, "Failed to open '%s'", path);
}

int r = lcfs_fd_get_fsverity(digest, fd);
if (r != 0) {
const char *errmsg = strerror(errno);
fprintf(stderr, "failed to measure '%s': %s", path, errmsg);
exit(1);
}
char digest_str[LCFS_DIGEST_SIZE * 2 + 1] = { 0 };
digest_to_string(digest, digest_str);
printf("%s\n", digest_str);
}

return 0;
}

int main(int argc, char **argv)
{
const char *bin = argv[0];
Expand Down Expand Up @@ -420,6 +453,8 @@ int main(int argc, char **argv)
handler = print_missing_objects_handler;
handler_init = print_objects_handler_init;
handler_end = print_objects_handler_end;
} else if (strcmp(command, "measure-file") == 0) {
return measure_files(bin, argc, argv);
} else {
errx(EXIT_FAILURE, "Unknown command '%s'\n", command);
}
Expand Down

0 comments on commit ef85111

Please sign in to comment.