When dealing with an encrypted or compressed file, we gather together sufficient pages from the pagecache to constitute a logical crypto/compression block, allocate a bounce buffer and then ask the filesystem to encrypt/compress between the buffers. The bounce buffer is then passed to the filesystem to upload. The network filesystem must set a flag to indicate what service is desired and when the logical blocksize will be. The netfs library iterates through each block to be processed, providing a pair of scatterlists to describe the start and end buffers. Note that it should be possible in future to encrypt/compress DIO writes also by this same mechanism. A mock-up block-encryption function for afs is included for illustration. Signed-off-by: David Howells <dhowells@xxxxxxxxxx> --- fs/afs/file.c | 1 fs/afs/inode.c | 6 ++ fs/afs/internal.h | 5 ++ fs/afs/super.c | 7 ++ fs/afs/write.c | 49 +++++++++++++++ fs/netfs/Makefile | 3 + fs/netfs/internal.h | 5 ++ fs/netfs/write_back.c | 6 ++ fs/netfs/write_prep.c | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ include/linux/netfs.h | 6 ++ 10 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 fs/netfs/write_prep.c diff --git a/fs/afs/file.c b/fs/afs/file.c index 22030d5191cd..8a6be8d2b426 100644 --- a/fs/afs/file.c +++ b/fs/afs/file.c @@ -404,6 +404,7 @@ const struct netfs_request_ops afs_req_ops = { .update_i_size = afs_update_i_size, .init_wreq = afs_init_wreq, .add_write_streams = afs_add_write_streams, + .encrypt_block = afs_encrypt_block, }; int afs_write_inode(struct inode *inode, struct writeback_control *wbc) diff --git a/fs/afs/inode.c b/fs/afs/inode.c index a6ae031461c7..7cad099c3bb1 100644 --- a/fs/afs/inode.c +++ b/fs/afs/inode.c @@ -452,10 +452,16 @@ static void afs_get_inode_cache(struct afs_vnode *vnode) static void afs_set_netfs_context(struct afs_vnode *vnode) { struct netfs_i_context *ctx = netfs_i_context(&vnode->vfs_inode); + struct afs_super_info *as = AFS_FS_S(vnode->vfs_inode.i_sb); netfs_i_context_init(&vnode->vfs_inode, &afs_req_ops); ctx->n_wstreams = 1; ctx->bsize = PAGE_SIZE; + if (as->fscrypt) { + kdebug("ENCRYPT!"); + ctx->crypto_bsize = ilog2(4096); + __set_bit(NETFS_ICTX_ENCRYPTED, &ctx->flags); + } } /* diff --git a/fs/afs/internal.h b/fs/afs/internal.h index 32a36b96cc9b..b5f7c3659a0a 100644 --- a/fs/afs/internal.h +++ b/fs/afs/internal.h @@ -51,6 +51,7 @@ struct afs_fs_context { bool autocell; /* T if set auto mount operation */ bool dyn_root; /* T if dynamic root */ bool no_cell; /* T if the source is "none" (for dynroot) */ + bool fscrypt; /* T if content encryption is engaged */ enum afs_flock_mode flock_mode; /* Partial file-locking emulation mode */ afs_voltype_t type; /* type of volume requested */ unsigned int volnamesz; /* size of volume name */ @@ -230,6 +231,7 @@ struct afs_super_info { struct afs_volume *volume; /* volume record */ enum afs_flock_mode flock_mode:8; /* File locking emulation mode */ bool dyn_root; /* True if dynamic root */ + bool fscrypt; /* T if content encryption is engaged */ }; static inline struct afs_super_info *AFS_FS_S(struct super_block *sb) @@ -1518,6 +1520,9 @@ extern void afs_prune_wb_keys(struct afs_vnode *); extern int afs_launder_page(struct page *); extern ssize_t afs_file_direct_write(struct kiocb *, struct iov_iter *); extern void afs_add_write_streams(struct netfs_write_request *); +extern bool afs_encrypt_block(struct netfs_write_request *, loff_t, size_t, + struct scatterlist *, unsigned int, + struct scatterlist *, unsigned int); /* * xattr.c diff --git a/fs/afs/super.c b/fs/afs/super.c index 29c1178beb72..53f35ec7b17b 100644 --- a/fs/afs/super.c +++ b/fs/afs/super.c @@ -71,6 +71,7 @@ enum afs_param { Opt_autocell, Opt_dyn, Opt_flock, + Opt_fscrypt, Opt_source, }; @@ -86,6 +87,7 @@ static const struct fs_parameter_spec afs_fs_parameters[] = { fsparam_flag ("autocell", Opt_autocell), fsparam_flag ("dyn", Opt_dyn), fsparam_enum ("flock", Opt_flock, afs_param_flock), + fsparam_flag ("fscrypt", Opt_fscrypt), fsparam_string("source", Opt_source), {} }; @@ -342,6 +344,10 @@ static int afs_parse_param(struct fs_context *fc, struct fs_parameter *param) ctx->flock_mode = result.uint_32; break; + case Opt_fscrypt: + ctx->fscrypt = true; + break; + default: return -EINVAL; } @@ -516,6 +522,7 @@ static struct afs_super_info *afs_alloc_sbi(struct fs_context *fc) as->cell = afs_use_cell(ctx->cell, afs_cell_trace_use_sbi); as->volume = afs_get_volume(ctx->volume, afs_volume_trace_get_alloc_sbi); + as->fscrypt = ctx->fscrypt; } } return as; diff --git a/fs/afs/write.c b/fs/afs/write.c index 0668389f3466..d2b7cb1a4668 100644 --- a/fs/afs/write.c +++ b/fs/afs/write.c @@ -13,6 +13,7 @@ #include <linux/pagevec.h> #include <linux/netfs.h> #include <linux/fscache.h> +#include <crypto/skcipher.h> #include <trace/events/netfs.h> #include "internal.h" @@ -293,6 +294,54 @@ void afs_add_write_streams(struct netfs_write_request *wreq) afs_upload_to_server_worker); } +/* + * Encrypt part of a write for fscrypt. + */ +bool afs_encrypt_block(struct netfs_write_request *wreq, loff_t pos, size_t len, + struct scatterlist *source_sg, unsigned int n_source, + struct scatterlist *dest_sg, unsigned int n_dest) +{ + struct crypto_sync_skcipher *ci; + struct crypto_skcipher *tfm; + struct skcipher_request *req; + u8 session_key[8], iv[8]; + int ret; + + kenter("%llx", pos); + + ci = crypto_alloc_sync_skcipher("pcbc(fcrypt)", 0, 0); + if (IS_ERR(ci)) { + _debug("no cipher"); + ret = PTR_ERR(ci); + goto error; + } + tfm= &ci->base; + + ret = crypto_sync_skcipher_setkey(ci, session_key, sizeof(session_key)); + if (ret < 0) + goto error_ci; + + ret = -ENOMEM; + req = skcipher_request_alloc(tfm, GFP_NOFS); + if (!req) + goto error_ci; + + memset(iv, 0, sizeof(iv)); + skcipher_request_set_sync_tfm(req, ci); + skcipher_request_set_callback(req, 0, NULL, NULL); + skcipher_request_set_crypt(req, source_sg, dest_sg, len, iv); + ret = crypto_skcipher_encrypt(req); + + skcipher_request_free(req); +error_ci: + crypto_free_sync_skcipher(ci); +error: + if (ret < 0) + wreq->error = ret; + kleave(" = %d", ret); + return ret == 0; +} + /* * Extend the region to be written back to include subsequent contiguously * dirty pages if possible, but don't sleep while doing so. diff --git a/fs/netfs/Makefile b/fs/netfs/Makefile index a201fd7b22cf..a7c3a9173ac0 100644 --- a/fs/netfs/Makefile +++ b/fs/netfs/Makefile @@ -4,7 +4,8 @@ netfs-y := \ objects.o \ read_helper.o \ write_back.o \ - write_helper.o + write_helper.o \ + write_prep.o # dio_helper.o netfs-$(CONFIG_NETFS_STATS) += stats.o diff --git a/fs/netfs/internal.h b/fs/netfs/internal.h index 6fdf9e5663f7..381ca64062eb 100644 --- a/fs/netfs/internal.h +++ b/fs/netfs/internal.h @@ -65,6 +65,11 @@ void netfs_flush_region(struct netfs_i_context *ctx, struct netfs_dirty_region *region, enum netfs_dirty_trace why); +/* + * write_prep.c + */ +bool netfs_prepare_wreq(struct netfs_write_request *wreq); + /* * stats.c */ diff --git a/fs/netfs/write_back.c b/fs/netfs/write_back.c index 15cc0e1b9acf..7363c3324602 100644 --- a/fs/netfs/write_back.c +++ b/fs/netfs/write_back.c @@ -254,7 +254,9 @@ static void netfs_writeback(struct netfs_write_request *wreq) kenter(""); - /* TODO: Encrypt or compress the region as appropriate */ + if (test_bit(NETFS_ICTX_ENCRYPTED, &ctx->flags) && + !netfs_prepare_wreq(wreq)) + goto out; /* ->outstanding > 0 carries a ref */ netfs_get_write_request(wreq, netfs_wreq_trace_get_for_outstanding); @@ -262,6 +264,8 @@ static void netfs_writeback(struct netfs_write_request *wreq) if (test_bit(NETFS_WREQ_WRITE_TO_CACHE, &wreq->flags)) netfs_set_up_write_to_cache(wreq); ctx->ops->add_write_streams(wreq); + +out: if (atomic_dec_and_test(&wreq->outstanding)) netfs_write_completed(wreq, false); } diff --git a/fs/netfs/write_prep.c b/fs/netfs/write_prep.c new file mode 100644 index 000000000000..f0a9dfd92a18 --- /dev/null +++ b/fs/netfs/write_prep.c @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Network filesystem high-level write support. + * + * Copyright (C) 2021 Red Hat, Inc. All Rights Reserved. + * Written by David Howells (dhowells@xxxxxxxxxx) + */ + +#include <linux/fs.h> +#include <linux/mm.h> +#include <linux/pagemap.h> +#include <linux/slab.h> +#include "internal.h" + +/* + * Allocate a bunch of pages and add them into the xarray buffer starting at + * the given index. + */ +static int netfs_alloc_buffer(struct xarray *xa, pgoff_t index, unsigned int nr_pages) +{ + struct page *page; + unsigned int n; + int ret; + LIST_HEAD(list); + + kenter(""); + + n = alloc_pages_bulk_list(GFP_NOIO, nr_pages, &list); + if (n < nr_pages) { + ret = -ENOMEM; + } + + while ((page = list_first_entry_or_null(&list, struct page, lru))) { + list_del(&page->lru); + ret = xa_insert(xa, index++, page, GFP_NOIO); + if (ret < 0) + break; + } + + while ((page = list_first_entry_or_null(&list, struct page, lru))) { + list_del(&page->lru); + __free_page(page); + } + return ret; +} + +/* + * Populate a scatterlist from pages in an xarray. + */ +static int netfs_xarray_to_sglist(struct xarray *xa, loff_t pos, size_t len, + struct scatterlist *sg, unsigned int n_sg) +{ + struct scatterlist *p = sg; + struct page *head = NULL; + size_t seg, offset, skip = 0; + loff_t start = pos; + pgoff_t index = start >> PAGE_SHIFT; + int j; + + XA_STATE(xas, xa, index); + + sg_init_table(sg, n_sg); + + rcu_read_lock(); + + xas_for_each(&xas, head, ULONG_MAX) { + kdebug("LOAD %lx %px", head->index, head); + if (xas_retry(&xas, head)) + continue; + if (WARN_ON(xa_is_value(head)) || WARN_ON(PageHuge(head))) + break; + for (j = (head->index < index) ? index - head->index : 0; + j < thp_nr_pages(head); j++ + ) { + offset = (pos + skip) & ~PAGE_MASK; + seg = min(len, PAGE_SIZE - offset); + + kdebug("[%zx] %lx %zx @%zx", p - sg, (head + j)->index, seg, offset); + sg_set_page(p++, head + j, seg, offset); + + len -= seg; + skip += seg; + if (len == 0) + break; + } + if (len == 0) + break; + } + + rcu_read_unlock(); + if (len > 0) { + WARN_ON(len > 0); + return -EIO; + } + + sg_mark_end(p - 1); + kleave(" = %zd", p - sg); + return p - sg; +} + +/* + * Perform content encryption on the data to be written before we write it to + * the server and the cache. + */ +static bool netfs_prepare_encrypt(struct netfs_write_request *wreq) +{ + struct netfs_i_context *ctx = netfs_i_context(wreq->inode); + struct scatterlist source_sg[16], dest_sg[16]; + unsigned int bsize = 1 << ctx->crypto_bsize, n_source, n_dest; + loff_t pos; + size_t n; + int ret; + + ret = netfs_alloc_buffer(&wreq->buffer, wreq->first, wreq->last - wreq->first + 1); + if (ret < 0) + goto error; + + pos = round_down(wreq->start, bsize); + n = round_up(wreq->start + wreq->len, bsize) - pos; + for (; n > 0; n -= bsize, pos += bsize) { + ret = netfs_xarray_to_sglist(&wreq->mapping->i_pages, pos, bsize, + source_sg, ARRAY_SIZE(source_sg)); + if (ret < 0) + goto error; + n_source = ret; + + ret = netfs_xarray_to_sglist(&wreq->buffer, pos, bsize, + dest_sg, ARRAY_SIZE(dest_sg)); + if (ret < 0) + goto error; + n_dest = ret; + + ret = ctx->ops->encrypt_block(wreq, pos, bsize, + source_sg, n_source, dest_sg, n_dest); + if (ret < 0) + goto error; + } + + iov_iter_xarray(&wreq->source, WRITE, &wreq->buffer, wreq->start, wreq->len); + kleave(" = t"); + return true; + +error: + wreq->error = ret; + kleave(" = f [%d]", ret); + return false; +} + +/* + * Prepare a write request for writing. All the pages in the bounding box have + * had a ref taken on them and those covering the dirty region have been marked + * as being written back and their dirty bits provisionally cleared. + */ +bool netfs_prepare_wreq(struct netfs_write_request *wreq) +{ + struct netfs_i_context *ctx = netfs_i_context(wreq->inode); + + if (test_bit(NETFS_ICTX_ENCRYPTED, &ctx->flags)) + return netfs_prepare_encrypt(wreq); + return true; +} diff --git a/include/linux/netfs.h b/include/linux/netfs.h index 9d50c2933863..6acf3fb170c3 100644 --- a/include/linux/netfs.h +++ b/include/linux/netfs.h @@ -19,6 +19,7 @@ #include <linux/pagemap.h> #include <linux/uio.h> +struct scatterlist; enum netfs_wreq_trace; /* @@ -177,12 +178,14 @@ struct netfs_i_context { #endif unsigned long flags; #define NETFS_ICTX_NEW_CONTENT 0 /* Set if file has new content (create/trunc-0) */ +#define NETFS_ICTX_ENCRYPTED 1 /* The file contents are encrypted */ spinlock_t lock; unsigned int rsize; /* Maximum read size */ unsigned int wsize; /* Maximum write size */ unsigned int bsize; /* Min block size for bounding box */ unsigned int inval_counter; /* Number of invalidations made */ unsigned char n_wstreams; /* Number of write streams to allocate */ + unsigned char crypto_bsize; /* log2 of crypto block size */ }; /* @@ -358,6 +361,9 @@ struct netfs_request_ops { void (*init_wreq)(struct netfs_write_request *wreq); void (*add_write_streams)(struct netfs_write_request *wreq); void (*invalidate_cache)(struct netfs_write_request *wreq); + bool (*encrypt_block)(struct netfs_write_request *wreq, loff_t pos, size_t len, + struct scatterlist *source_sg, unsigned int n_source, + struct scatterlist *dest_sg, unsigned int n_dest); }; /* -- Linux-cachefs mailing list Linux-cachefs@xxxxxxxxxx https://listman.redhat.com/mailman/listinfo/linux-cachefs