linux-hardened/fs/afs/dir_edit.c
David Howells 63a4681ff3 afs: Locally edit directory data for mkdir/create/unlink/...
Locally edit the contents of an AFS directory upon a successful inode
operation that modifies that directory (such as mkdir, create and unlink)
so that we can avoid the current practice of re-downloading the directory
after each change.

This is viable provided that the directory version number we get back from
the modifying RPC op is exactly incremented by 1 from what we had
previously.  The data in the directory contents is in a defined format that
we have to parse locally to perform lookups and readdir, so modifying isn't
a problem.

If the edit fails, we just clear the VALID flag on the directory and it
will be reloaded next time it is needed.

Signed-off-by: David Howells <dhowells@redhat.com>
2018-04-09 21:54:48 +01:00

505 lines
13 KiB
C

/* AFS filesystem directory editing
*
* Copyright (C) 2018 Red Hat, Inc. All Rights Reserved.
* Written by David Howells (dhowells@redhat.com)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public Licence
* as published by the Free Software Foundation; either version
* 2 of the Licence, or (at your option) any later version.
*/
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/namei.h>
#include <linux/pagemap.h>
#include <linux/iversion.h>
#include "internal.h"
#include "xdr_fs.h"
/*
* Find a number of contiguous clear bits in a directory block bitmask.
*
* There are 64 slots, which means we can load the entire bitmap into a
* variable. The first bit doesn't count as it corresponds to the block header
* slot. nr_slots is between 1 and 9.
*/
static int afs_find_contig_bits(union afs_xdr_dir_block *block, unsigned int nr_slots)
{
u64 bitmap;
u32 mask;
int bit, n;
bitmap = (u64)block->hdr.bitmap[0] << 0 * 8;
bitmap |= (u64)block->hdr.bitmap[1] << 1 * 8;
bitmap |= (u64)block->hdr.bitmap[2] << 2 * 8;
bitmap |= (u64)block->hdr.bitmap[3] << 3 * 8;
bitmap |= (u64)block->hdr.bitmap[4] << 4 * 8;
bitmap |= (u64)block->hdr.bitmap[5] << 5 * 8;
bitmap |= (u64)block->hdr.bitmap[6] << 6 * 8;
bitmap |= (u64)block->hdr.bitmap[7] << 7 * 8;
bitmap >>= 1; /* The first entry is metadata */
bit = 1;
mask = (1 << nr_slots) - 1;
do {
if (sizeof(unsigned long) == 8)
n = ffz(bitmap);
else
n = ((u32)bitmap) != 0 ?
ffz((u32)bitmap) :
ffz((u32)(bitmap >> 32)) + 32;
bitmap >>= n;
bit += n;
if ((bitmap & mask) == 0) {
if (bit > 64 - nr_slots)
return -1;
return bit;
}
n = __ffs(bitmap);
bitmap >>= n;
bit += n;
} while (bitmap);
return -1;
}
/*
* Set a number of contiguous bits in the directory block bitmap.
*/
static void afs_set_contig_bits(union afs_xdr_dir_block *block,
int bit, unsigned int nr_slots)
{
u64 mask, before, after;
mask = (1 << nr_slots) - 1;
mask <<= bit;
before = *(u64 *)block->hdr.bitmap;
block->hdr.bitmap[0] |= (u8)(mask >> 0 * 8);
block->hdr.bitmap[1] |= (u8)(mask >> 1 * 8);
block->hdr.bitmap[2] |= (u8)(mask >> 2 * 8);
block->hdr.bitmap[3] |= (u8)(mask >> 3 * 8);
block->hdr.bitmap[4] |= (u8)(mask >> 4 * 8);
block->hdr.bitmap[5] |= (u8)(mask >> 5 * 8);
block->hdr.bitmap[6] |= (u8)(mask >> 6 * 8);
block->hdr.bitmap[7] |= (u8)(mask >> 7 * 8);
after = *(u64 *)block->hdr.bitmap;
}
/*
* Clear a number of contiguous bits in the directory block bitmap.
*/
static void afs_clear_contig_bits(union afs_xdr_dir_block *block,
int bit, unsigned int nr_slots)
{
u64 mask, before, after;
mask = (1 << nr_slots) - 1;
mask <<= bit;
before = *(u64 *)block->hdr.bitmap;
block->hdr.bitmap[0] &= ~(u8)(mask >> 0 * 8);
block->hdr.bitmap[1] &= ~(u8)(mask >> 1 * 8);
block->hdr.bitmap[2] &= ~(u8)(mask >> 2 * 8);
block->hdr.bitmap[3] &= ~(u8)(mask >> 3 * 8);
block->hdr.bitmap[4] &= ~(u8)(mask >> 4 * 8);
block->hdr.bitmap[5] &= ~(u8)(mask >> 5 * 8);
block->hdr.bitmap[6] &= ~(u8)(mask >> 6 * 8);
block->hdr.bitmap[7] &= ~(u8)(mask >> 7 * 8);
after = *(u64 *)block->hdr.bitmap;
}
/*
* Scan a directory block looking for a dirent of the right name.
*/
static int afs_dir_scan_block(union afs_xdr_dir_block *block, struct qstr *name,
unsigned int blocknum)
{
union afs_xdr_dirent *de;
u64 bitmap;
int d, len, n;
_enter("");
bitmap = (u64)block->hdr.bitmap[0] << 0 * 8;
bitmap |= (u64)block->hdr.bitmap[1] << 1 * 8;
bitmap |= (u64)block->hdr.bitmap[2] << 2 * 8;
bitmap |= (u64)block->hdr.bitmap[3] << 3 * 8;
bitmap |= (u64)block->hdr.bitmap[4] << 4 * 8;
bitmap |= (u64)block->hdr.bitmap[5] << 5 * 8;
bitmap |= (u64)block->hdr.bitmap[6] << 6 * 8;
bitmap |= (u64)block->hdr.bitmap[7] << 7 * 8;
for (d = (blocknum == 0 ? AFS_DIR_RESV_BLOCKS0 : AFS_DIR_RESV_BLOCKS);
d < AFS_DIR_SLOTS_PER_BLOCK;
d++) {
if (!((bitmap >> d) & 1))
continue;
de = &block->dirents[d];
if (de->u.valid != 1)
continue;
/* The block was NUL-terminated by afs_dir_check_page(). */
len = strlen(de->u.name);
if (len == name->len &&
memcmp(de->u.name, name->name, name->len) == 0)
return d;
n = round_up(12 + len + 1 + 4, AFS_DIR_DIRENT_SIZE);
n /= AFS_DIR_DIRENT_SIZE;
d += n - 1;
}
return -1;
}
/*
* Initialise a new directory block. Note that block 0 is special and contains
* some extra metadata.
*/
static void afs_edit_init_block(union afs_xdr_dir_block *meta,
union afs_xdr_dir_block *block, int block_num)
{
memset(block, 0, sizeof(*block));
block->hdr.npages = htons(1);
block->hdr.magic = AFS_DIR_MAGIC;
block->hdr.bitmap[0] = 1;
if (block_num == 0) {
block->hdr.bitmap[0] = 0xff;
block->hdr.bitmap[1] = 0x1f;
memset(block->meta.alloc_ctrs,
AFS_DIR_SLOTS_PER_BLOCK,
sizeof(block->meta.alloc_ctrs));
meta->meta.alloc_ctrs[0] =
AFS_DIR_SLOTS_PER_BLOCK - AFS_DIR_RESV_BLOCKS0;
}
if (block_num < AFS_DIR_BLOCKS_WITH_CTR)
meta->meta.alloc_ctrs[block_num] =
AFS_DIR_SLOTS_PER_BLOCK - AFS_DIR_RESV_BLOCKS;
}
/*
* Edit a directory's file data to add a new directory entry. Doing this after
* create, mkdir, symlink, link or rename if the data version number is
* incremented by exactly one avoids the need to re-download the entire
* directory contents.
*
* The caller must hold the inode locked.
*/
void afs_edit_dir_add(struct afs_vnode *vnode,
struct qstr *name, struct afs_fid *new_fid,
enum afs_edit_dir_reason why)
{
union afs_xdr_dir_block *meta, *block;
struct afs_xdr_dir_page *meta_page, *dir_page;
union afs_xdr_dirent *de;
struct page *page0, *page;
unsigned int need_slots, nr_blocks, b;
pgoff_t index;
loff_t i_size;
gfp_t gfp;
int slot;
_enter(",,{%d,%s},", name->len, name->name);
i_size = i_size_read(&vnode->vfs_inode);
if (i_size > AFS_DIR_BLOCK_SIZE * AFS_DIR_MAX_BLOCKS ||
(i_size & (AFS_DIR_BLOCK_SIZE - 1))) {
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
return;
}
gfp = vnode->vfs_inode.i_mapping->gfp_mask;
page0 = find_or_create_page(vnode->vfs_inode.i_mapping, 0, gfp);
if (!page0) {
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
_leave(" [fgp]");
return;
}
/* Work out how many slots we're going to need. */
need_slots = round_up(12 + name->len + 1 + 4, AFS_DIR_DIRENT_SIZE);
need_slots /= AFS_DIR_DIRENT_SIZE;
meta_page = kmap(page0);
meta = &meta_page->blocks[0];
if (i_size == 0)
goto new_directory;
nr_blocks = i_size / AFS_DIR_BLOCK_SIZE;
/* Find a block that has sufficient slots available. Each VM page
* contains two or more directory blocks.
*/
for (b = 0; b < nr_blocks + 1; b++) {
/* If the directory extended into a new page, then we need to
* tack a new page on the end.
*/
index = b / AFS_DIR_BLOCKS_PER_PAGE;
if (index == 0) {
page = page0;
dir_page = meta_page;
} else {
if (nr_blocks >= AFS_DIR_MAX_BLOCKS)
goto error;
gfp = vnode->vfs_inode.i_mapping->gfp_mask;
page = find_or_create_page(vnode->vfs_inode.i_mapping,
index, gfp);
if (!page)
goto error;
if (!PagePrivate(page)) {
set_page_private(page, 1);
SetPagePrivate(page);
}
dir_page = kmap(page);
}
/* Abandon the edit if we got a callback break. */
if (!test_bit(AFS_VNODE_DIR_VALID, &vnode->flags))
goto invalidated;
block = &dir_page->blocks[b % AFS_DIR_BLOCKS_PER_PAGE];
_debug("block %u: %2u %3u %u",
b,
(b < AFS_DIR_BLOCKS_WITH_CTR) ? meta->meta.alloc_ctrs[b] : 99,
ntohs(block->hdr.npages),
ntohs(block->hdr.magic));
/* Initialise the block if necessary. */
if (b == nr_blocks) {
_debug("init %u", b);
afs_edit_init_block(meta, block, b);
i_size_write(&vnode->vfs_inode, (b + 1) * AFS_DIR_BLOCK_SIZE);
}
/* Only lower dir pages have a counter in the header. */
if (b >= AFS_DIR_BLOCKS_WITH_CTR ||
meta->meta.alloc_ctrs[b] >= need_slots) {
/* We need to try and find one or more consecutive
* slots to hold the entry.
*/
slot = afs_find_contig_bits(block, need_slots);
if (slot >= 0) {
_debug("slot %u", slot);
goto found_space;
}
}
if (page != page0) {
unlock_page(page);
kunmap(page);
put_page(page);
}
}
/* There are no spare slots of sufficient size, yet the operation
* succeeded. Download the directory again.
*/
trace_afs_edit_dir(vnode, why, afs_edit_dir_create_nospc, 0, 0, 0, 0, name->name);
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
goto out_unmap;
new_directory:
afs_edit_init_block(meta, meta, 0);
i_size = AFS_DIR_BLOCK_SIZE;
i_size_write(&vnode->vfs_inode, i_size);
slot = AFS_DIR_RESV_BLOCKS0;
page = page0;
block = meta;
nr_blocks = 1;
b = 0;
found_space:
/* Set the dirent slot. */
trace_afs_edit_dir(vnode, why, afs_edit_dir_create, b, slot,
new_fid->vnode, new_fid->unique, name->name);
de = &block->dirents[slot];
de->u.valid = 1;
de->u.unused[0] = 0;
de->u.hash_next = 0; // TODO: Really need to maintain this
de->u.vnode = htonl(new_fid->vnode);
de->u.unique = htonl(new_fid->unique);
memcpy(de->u.name, name->name, name->len + 1);
de->u.name[name->len] = 0;
/* Adjust the bitmap. */
afs_set_contig_bits(block, slot, need_slots);
if (page != page0) {
unlock_page(page);
kunmap(page);
put_page(page);
}
/* Adjust the allocation counter. */
if (b < AFS_DIR_BLOCKS_WITH_CTR)
meta->meta.alloc_ctrs[b] -= need_slots;
inode_inc_iversion_raw(&vnode->vfs_inode);
afs_stat_v(vnode, n_dir_cr);
_debug("Insert %s in %u[%u]", name->name, b, slot);
out_unmap:
unlock_page(page0);
kunmap(page0);
put_page(page0);
_leave("");
return;
invalidated:
trace_afs_edit_dir(vnode, why, afs_edit_dir_create_inval, 0, 0, 0, 0, name->name);
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
if (page != page0) {
kunmap(page);
put_page(page);
}
goto out_unmap;
error:
trace_afs_edit_dir(vnode, why, afs_edit_dir_create_error, 0, 0, 0, 0, name->name);
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
goto out_unmap;
}
/*
* Edit a directory's file data to remove a new directory entry. Doing this
* after unlink, rmdir or rename if the data version number is incremented by
* exactly one avoids the need to re-download the entire directory contents.
*
* The caller must hold the inode locked.
*/
void afs_edit_dir_remove(struct afs_vnode *vnode,
struct qstr *name, enum afs_edit_dir_reason why)
{
struct afs_xdr_dir_page *meta_page, *dir_page;
union afs_xdr_dir_block *meta, *block;
union afs_xdr_dirent *de;
struct page *page0, *page;
unsigned int need_slots, nr_blocks, b;
pgoff_t index;
loff_t i_size;
int slot;
_enter(",,{%d,%s},", name->len, name->name);
i_size = i_size_read(&vnode->vfs_inode);
if (i_size < AFS_DIR_BLOCK_SIZE ||
i_size > AFS_DIR_BLOCK_SIZE * AFS_DIR_MAX_BLOCKS ||
(i_size & (AFS_DIR_BLOCK_SIZE - 1))) {
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
return;
}
nr_blocks = i_size / AFS_DIR_BLOCK_SIZE;
page0 = find_lock_page(vnode->vfs_inode.i_mapping, 0);
if (!page0) {
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
_leave(" [fgp]");
return;
}
/* Work out how many slots we're going to discard. */
need_slots = round_up(12 + name->len + 1 + 4, AFS_DIR_DIRENT_SIZE);
need_slots /= AFS_DIR_DIRENT_SIZE;
meta_page = kmap(page0);
meta = &meta_page->blocks[0];
/* Find a page that has sufficient slots available. Each VM page
* contains two or more directory blocks.
*/
for (b = 0; b < nr_blocks; b++) {
index = b / AFS_DIR_BLOCKS_PER_PAGE;
if (index != 0) {
page = find_lock_page(vnode->vfs_inode.i_mapping, index);
if (!page)
goto error;
dir_page = kmap(page);
} else {
page = page0;
dir_page = meta_page;
}
/* Abandon the edit if we got a callback break. */
if (!test_bit(AFS_VNODE_DIR_VALID, &vnode->flags))
goto invalidated;
block = &dir_page->blocks[b % AFS_DIR_BLOCKS_PER_PAGE];
if (b > AFS_DIR_BLOCKS_WITH_CTR ||
meta->meta.alloc_ctrs[b] <= AFS_DIR_SLOTS_PER_BLOCK - 1 - need_slots) {
slot = afs_dir_scan_block(block, name, b);
if (slot >= 0)
goto found_dirent;
}
if (page != page0) {
unlock_page(page);
kunmap(page);
put_page(page);
}
}
/* Didn't find the dirent to clobber. Download the directory again. */
trace_afs_edit_dir(vnode, why, afs_edit_dir_delete_noent,
0, 0, 0, 0, name->name);
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
goto out_unmap;
found_dirent:
de = &block->dirents[slot];
trace_afs_edit_dir(vnode, why, afs_edit_dir_delete, b, slot,
ntohl(de->u.vnode), ntohl(de->u.unique),
name->name);
memset(de, 0, sizeof(*de) * need_slots);
/* Adjust the bitmap. */
afs_clear_contig_bits(block, slot, need_slots);
if (page != page0) {
unlock_page(page);
kunmap(page);
put_page(page);
}
/* Adjust the allocation counter. */
if (b < AFS_DIR_BLOCKS_WITH_CTR)
meta->meta.alloc_ctrs[b] += need_slots;
inode_set_iversion_raw(&vnode->vfs_inode, vnode->status.data_version);
afs_stat_v(vnode, n_dir_rm);
_debug("Remove %s from %u[%u]", name->name, b, slot);
out_unmap:
unlock_page(page0);
kunmap(page0);
put_page(page0);
_leave("");
return;
invalidated:
trace_afs_edit_dir(vnode, why, afs_edit_dir_delete_inval,
0, 0, 0, 0, name->name);
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
if (page != page0) {
unlock_page(page);
kunmap(page);
put_page(page);
}
goto out_unmap;
error:
trace_afs_edit_dir(vnode, why, afs_edit_dir_delete_error,
0, 0, 0, 0, name->name);
clear_bit(AFS_VNODE_DIR_VALID, &vnode->flags);
goto out_unmap;
}