mirror of
https://github.com/torvalds/linux.git
synced 2025-12-07 20:06:24 +00:00
The allocation of a cell's anonymous key is done in a background thread
along with other cell setup such as doing a DNS upcall. In the reported
bug, this is triggered by afs_parse_source() parsing the device name given
to mount() and calling afs_lookup_cell() with the name of the cell.
The normal key lookup then tries to use the key description on the
anonymous authentication key as the reference for request_key() - but it
may not yet be set and so an oops can happen.
This has been made more likely to happen by the fix for dynamic lookup
failure.
Fix this by firstly allocating a reference name and attaching it to the
afs_cell record when the record is created. It can share the memory
allocation with the cell name (unfortunately it can't just overlap the cell
name by prepending it with "afs@" as the cell name already has a '.'
prepended for other purposes). This reference name is then passed to
request_key().
Secondly, the anon key is now allocated on demand at the point a key is
requested in afs_request_key() if it is not already allocated. A mutex is
used to prevent multiple allocation for a cell.
Thirdly, make afs_request_key_rcu() return NULL if the anonymous key isn't
yet allocated (if we need it) and then the caller can return -ECHILD to
drop out of RCU-mode and afs_request_key() can be called.
Note that the anonymous key is kind of necessary to make the key lookup
cache work as that doesn't currently cache a negative lookup, but it's
probably worth some investigation to see if NULL can be used instead.
Fixes: 330e2c5148 ("afs: Fix dynamic lookup to fail on cell lookup failure")
Reported-by: syzbot+41c68824eefb67cdf00c@syzkaller.appspotmail.com
Signed-off-by: David Howells <dhowells@redhat.com>
Link: https://patch.msgid.link/800328.1764325145@warthog.procyon.org.uk
cc: Marc Dionne <marc.dionne@auristor.com>
cc: linux-afs@lists.infradead.org
cc: linux-fsdevel@vger.kernel.org
Signed-off-by: Christian Brauner <brauner@kernel.org>
925 lines
23 KiB
C
925 lines
23 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/* AFS cell and server record management
|
|
*
|
|
* Copyright (C) 2002, 2017 Red Hat, Inc. All Rights Reserved.
|
|
* Written by David Howells (dhowells@redhat.com)
|
|
*/
|
|
|
|
#include <linux/slab.h>
|
|
#include <linux/key.h>
|
|
#include <linux/ctype.h>
|
|
#include <linux/dns_resolver.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/inet.h>
|
|
#include <linux/namei.h>
|
|
#include <keys/rxrpc-type.h>
|
|
#include "internal.h"
|
|
|
|
static unsigned __read_mostly afs_cell_gc_delay = 10;
|
|
static unsigned __read_mostly afs_cell_min_ttl = 10 * 60;
|
|
static unsigned __read_mostly afs_cell_max_ttl = 24 * 60 * 60;
|
|
static atomic_t cell_debug_id;
|
|
|
|
static void afs_cell_timer(struct timer_list *timer);
|
|
static void afs_destroy_cell_work(struct work_struct *work);
|
|
static void afs_manage_cell_work(struct work_struct *work);
|
|
|
|
static void afs_dec_cells_outstanding(struct afs_net *net)
|
|
{
|
|
if (atomic_dec_and_test(&net->cells_outstanding))
|
|
wake_up_var(&net->cells_outstanding);
|
|
}
|
|
|
|
static void afs_set_cell_state(struct afs_cell *cell, enum afs_cell_state state)
|
|
{
|
|
smp_store_release(&cell->state, state); /* Commit cell changes before state */
|
|
smp_wmb(); /* Set cell state before task state */
|
|
wake_up_var(&cell->state);
|
|
}
|
|
|
|
/*
|
|
* Look up and get an activation reference on a cell record. The caller must
|
|
* hold net->cells_lock at least read-locked.
|
|
*/
|
|
static struct afs_cell *afs_find_cell_locked(struct afs_net *net,
|
|
const char *name, unsigned int namesz,
|
|
enum afs_cell_trace reason)
|
|
{
|
|
struct afs_cell *cell = NULL;
|
|
struct rb_node *p;
|
|
int n;
|
|
|
|
_enter("%*.*s", namesz, namesz, name);
|
|
|
|
if (name && namesz == 0)
|
|
return ERR_PTR(-EINVAL);
|
|
if (namesz > AFS_MAXCELLNAME)
|
|
return ERR_PTR(-ENAMETOOLONG);
|
|
|
|
if (!name) {
|
|
cell = rcu_dereference_protected(net->ws_cell,
|
|
lockdep_is_held(&net->cells_lock));
|
|
if (!cell)
|
|
return ERR_PTR(-EDESTADDRREQ);
|
|
goto found;
|
|
}
|
|
|
|
p = net->cells.rb_node;
|
|
while (p) {
|
|
cell = rb_entry(p, struct afs_cell, net_node);
|
|
|
|
n = strncasecmp(cell->name, name,
|
|
min_t(size_t, cell->name_len, namesz));
|
|
if (n == 0)
|
|
n = cell->name_len - namesz;
|
|
if (n < 0)
|
|
p = p->rb_left;
|
|
else if (n > 0)
|
|
p = p->rb_right;
|
|
else
|
|
goto found;
|
|
}
|
|
|
|
return ERR_PTR(-ENOENT);
|
|
|
|
found:
|
|
return afs_use_cell(cell, reason);
|
|
}
|
|
|
|
/*
|
|
* Look up and get an activation reference on a cell record.
|
|
*/
|
|
struct afs_cell *afs_find_cell(struct afs_net *net,
|
|
const char *name, unsigned int namesz,
|
|
enum afs_cell_trace reason)
|
|
{
|
|
struct afs_cell *cell;
|
|
|
|
down_read(&net->cells_lock);
|
|
cell = afs_find_cell_locked(net, name, namesz, reason);
|
|
up_read(&net->cells_lock);
|
|
return cell;
|
|
}
|
|
|
|
/*
|
|
* Set up a cell record and fill in its name, VL server address list and
|
|
* allocate an anonymous key
|
|
*/
|
|
static struct afs_cell *afs_alloc_cell(struct afs_net *net,
|
|
const char *name, unsigned int namelen,
|
|
const char *addresses)
|
|
{
|
|
struct afs_vlserver_list *vllist = NULL;
|
|
struct afs_cell *cell;
|
|
int i, ret;
|
|
|
|
ASSERT(name);
|
|
if (namelen == 0)
|
|
return ERR_PTR(-EINVAL);
|
|
if (namelen > AFS_MAXCELLNAME) {
|
|
_leave(" = -ENAMETOOLONG");
|
|
return ERR_PTR(-ENAMETOOLONG);
|
|
}
|
|
|
|
/* Prohibit cell names that contain unprintable chars, '/' and '@' or
|
|
* that begin with a dot. This also precludes "@cell".
|
|
*/
|
|
if (name[0] == '.')
|
|
return ERR_PTR(-EINVAL);
|
|
for (i = 0; i < namelen; i++) {
|
|
char ch = name[i];
|
|
if (!isprint(ch) || ch == '/' || ch == '@')
|
|
return ERR_PTR(-EINVAL);
|
|
}
|
|
|
|
_enter("%*.*s,%s", namelen, namelen, name, addresses);
|
|
|
|
cell = kzalloc(sizeof(struct afs_cell), GFP_KERNEL);
|
|
if (!cell) {
|
|
_leave(" = -ENOMEM");
|
|
return ERR_PTR(-ENOMEM);
|
|
}
|
|
|
|
/* Allocate the cell name and the key name in one go. */
|
|
cell->name = kmalloc(1 + namelen + 1 +
|
|
4 + namelen + 1, GFP_KERNEL);
|
|
if (!cell->name) {
|
|
kfree(cell);
|
|
return ERR_PTR(-ENOMEM);
|
|
}
|
|
|
|
cell->name[0] = '.';
|
|
cell->name++;
|
|
cell->name_len = namelen;
|
|
for (i = 0; i < namelen; i++)
|
|
cell->name[i] = tolower(name[i]);
|
|
cell->name[i++] = 0;
|
|
|
|
cell->key_desc = cell->name + i;
|
|
memcpy(cell->key_desc, "afs@", 4);
|
|
memcpy(cell->key_desc + 4, cell->name, cell->name_len + 1);
|
|
|
|
cell->net = net;
|
|
refcount_set(&cell->ref, 1);
|
|
atomic_set(&cell->active, 0);
|
|
INIT_WORK(&cell->destroyer, afs_destroy_cell_work);
|
|
INIT_WORK(&cell->manager, afs_manage_cell_work);
|
|
timer_setup(&cell->management_timer, afs_cell_timer, 0);
|
|
init_rwsem(&cell->vs_lock);
|
|
cell->volumes = RB_ROOT;
|
|
INIT_HLIST_HEAD(&cell->proc_volumes);
|
|
seqlock_init(&cell->volume_lock);
|
|
cell->fs_servers = RB_ROOT;
|
|
init_rwsem(&cell->fs_lock);
|
|
rwlock_init(&cell->vl_servers_lock);
|
|
cell->flags = (1 << AFS_CELL_FL_CHECK_ALIAS);
|
|
|
|
/* Provide a VL server list, filling it in if we were given a list of
|
|
* addresses to use.
|
|
*/
|
|
if (addresses) {
|
|
vllist = afs_parse_text_addrs(net,
|
|
addresses, strlen(addresses), ':',
|
|
VL_SERVICE, AFS_VL_PORT);
|
|
if (IS_ERR(vllist)) {
|
|
ret = PTR_ERR(vllist);
|
|
vllist = NULL;
|
|
goto parse_failed;
|
|
}
|
|
|
|
vllist->source = DNS_RECORD_FROM_CONFIG;
|
|
vllist->status = DNS_LOOKUP_NOT_DONE;
|
|
cell->dns_expiry = TIME64_MAX;
|
|
} else {
|
|
ret = -ENOMEM;
|
|
vllist = afs_alloc_vlserver_list(0);
|
|
if (!vllist)
|
|
goto error;
|
|
vllist->source = DNS_RECORD_UNAVAILABLE;
|
|
vllist->status = DNS_LOOKUP_NOT_DONE;
|
|
cell->dns_expiry = ktime_get_real_seconds();
|
|
}
|
|
|
|
rcu_assign_pointer(cell->vl_servers, vllist);
|
|
|
|
cell->dns_source = vllist->source;
|
|
cell->dns_status = vllist->status;
|
|
smp_store_release(&cell->dns_lookup_count, 1); /* vs source/status */
|
|
atomic_inc(&net->cells_outstanding);
|
|
ret = idr_alloc_cyclic(&net->cells_dyn_ino, cell,
|
|
2, INT_MAX / 2, GFP_KERNEL);
|
|
if (ret < 0)
|
|
goto error;
|
|
cell->dynroot_ino = ret;
|
|
cell->debug_id = atomic_inc_return(&cell_debug_id);
|
|
|
|
trace_afs_cell(cell->debug_id, 1, 0, afs_cell_trace_alloc);
|
|
|
|
_leave(" = %p", cell);
|
|
return cell;
|
|
|
|
parse_failed:
|
|
if (ret == -EINVAL)
|
|
printk(KERN_ERR "kAFS: bad VL server IP address\n");
|
|
error:
|
|
afs_put_vlserverlist(cell->net, vllist);
|
|
kfree(cell->name - 1);
|
|
kfree(cell);
|
|
_leave(" = %d", ret);
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
/*
|
|
* afs_lookup_cell - Look up or create a cell record.
|
|
* @net: The network namespace
|
|
* @name: The name of the cell.
|
|
* @namesz: The strlen of the cell name.
|
|
* @vllist: A colon/comma separated list of numeric IP addresses or NULL.
|
|
* @reason: The reason we're doing the lookup
|
|
* @trace: The reason to be logged if the lookup is successful.
|
|
*
|
|
* Look up a cell record by name and query the DNS for VL server addresses if
|
|
* needed. Note that that actual DNS query is punted off to the manager thread
|
|
* so that this function can return immediately if interrupted whilst allowing
|
|
* cell records to be shared even if not yet fully constructed.
|
|
*/
|
|
struct afs_cell *afs_lookup_cell(struct afs_net *net,
|
|
const char *name, unsigned int namesz,
|
|
const char *vllist,
|
|
enum afs_lookup_cell_for reason,
|
|
enum afs_cell_trace trace)
|
|
{
|
|
struct afs_cell *cell, *candidate, *cursor;
|
|
struct rb_node *parent, **pp;
|
|
enum afs_cell_state state;
|
|
int ret, n;
|
|
|
|
_enter("%s,%s,%u", name, vllist, reason);
|
|
|
|
if (reason != AFS_LOOKUP_CELL_PRELOAD) {
|
|
cell = afs_find_cell(net, name, namesz, trace);
|
|
if (!IS_ERR(cell)) {
|
|
if (reason == AFS_LOOKUP_CELL_DYNROOT)
|
|
goto no_wait;
|
|
if (cell->state == AFS_CELL_SETTING_UP ||
|
|
cell->state == AFS_CELL_UNLOOKED)
|
|
goto lookup_cell;
|
|
goto wait_for_cell;
|
|
}
|
|
}
|
|
|
|
/* Assume we're probably going to create a cell and preallocate and
|
|
* mostly set up a candidate record. We can then use this to stash the
|
|
* name, the net namespace and VL server addresses.
|
|
*
|
|
* We also want to do this before we hold any locks as it may involve
|
|
* upcalling to userspace to make DNS queries.
|
|
*/
|
|
candidate = afs_alloc_cell(net, name, namesz, vllist);
|
|
if (IS_ERR(candidate)) {
|
|
_leave(" = %ld", PTR_ERR(candidate));
|
|
return candidate;
|
|
}
|
|
|
|
/* Find the insertion point and check to see if someone else added a
|
|
* cell whilst we were allocating.
|
|
*/
|
|
down_write(&net->cells_lock);
|
|
|
|
pp = &net->cells.rb_node;
|
|
parent = NULL;
|
|
while (*pp) {
|
|
parent = *pp;
|
|
cursor = rb_entry(parent, struct afs_cell, net_node);
|
|
|
|
n = strncasecmp(cursor->name, name,
|
|
min_t(size_t, cursor->name_len, namesz));
|
|
if (n == 0)
|
|
n = cursor->name_len - namesz;
|
|
if (n < 0)
|
|
pp = &(*pp)->rb_left;
|
|
else if (n > 0)
|
|
pp = &(*pp)->rb_right;
|
|
else
|
|
goto cell_already_exists;
|
|
}
|
|
|
|
cell = candidate;
|
|
candidate = NULL;
|
|
afs_use_cell(cell, trace);
|
|
rb_link_node_rcu(&cell->net_node, parent, pp);
|
|
rb_insert_color(&cell->net_node, &net->cells);
|
|
up_write(&net->cells_lock);
|
|
|
|
lookup_cell:
|
|
if (reason != AFS_LOOKUP_CELL_PRELOAD &&
|
|
reason != AFS_LOOKUP_CELL_ROOTCELL) {
|
|
set_bit(AFS_CELL_FL_DO_LOOKUP, &cell->flags);
|
|
afs_queue_cell(cell, afs_cell_trace_queue_new);
|
|
}
|
|
|
|
wait_for_cell:
|
|
state = smp_load_acquire(&cell->state); /* vs error */
|
|
switch (state) {
|
|
case AFS_CELL_ACTIVE:
|
|
case AFS_CELL_DEAD:
|
|
break;
|
|
case AFS_CELL_UNLOOKED:
|
|
default:
|
|
if (reason == AFS_LOOKUP_CELL_PRELOAD ||
|
|
reason == AFS_LOOKUP_CELL_ROOTCELL)
|
|
break;
|
|
_debug("wait_for_cell");
|
|
afs_see_cell(cell, afs_cell_trace_wait);
|
|
wait_var_event(&cell->state,
|
|
({
|
|
state = smp_load_acquire(&cell->state); /* vs error */
|
|
state == AFS_CELL_ACTIVE || state == AFS_CELL_DEAD;
|
|
}));
|
|
_debug("waited_for_cell %d %d", cell->state, cell->error);
|
|
}
|
|
|
|
no_wait:
|
|
/* Check the state obtained from the wait check. */
|
|
state = smp_load_acquire(&cell->state); /* vs error */
|
|
if (state == AFS_CELL_DEAD) {
|
|
ret = cell->error;
|
|
goto error;
|
|
}
|
|
if (state == AFS_CELL_ACTIVE) {
|
|
switch (cell->dns_status) {
|
|
case DNS_LOOKUP_NOT_DONE:
|
|
if (cell->dns_source == DNS_RECORD_FROM_CONFIG) {
|
|
ret = 0;
|
|
break;
|
|
}
|
|
fallthrough;
|
|
default:
|
|
ret = -EIO;
|
|
goto error;
|
|
case DNS_LOOKUP_GOOD:
|
|
case DNS_LOOKUP_GOOD_WITH_BAD:
|
|
ret = 0;
|
|
break;
|
|
case DNS_LOOKUP_GOT_NOT_FOUND:
|
|
ret = -ENOENT;
|
|
goto error;
|
|
case DNS_LOOKUP_BAD:
|
|
ret = -EREMOTEIO;
|
|
goto error;
|
|
case DNS_LOOKUP_GOT_LOCAL_FAILURE:
|
|
case DNS_LOOKUP_GOT_TEMP_FAILURE:
|
|
case DNS_LOOKUP_GOT_NS_FAILURE:
|
|
ret = -EDESTADDRREQ;
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
_leave(" = %p [cell]", cell);
|
|
return cell;
|
|
|
|
cell_already_exists:
|
|
_debug("cell exists");
|
|
cell = cursor;
|
|
if (reason == AFS_LOOKUP_CELL_PRELOAD) {
|
|
ret = -EEXIST;
|
|
} else {
|
|
afs_use_cell(cursor, trace);
|
|
ret = 0;
|
|
}
|
|
up_write(&net->cells_lock);
|
|
if (candidate)
|
|
afs_put_cell(candidate, afs_cell_trace_put_candidate);
|
|
if (ret == 0)
|
|
goto wait_for_cell;
|
|
goto error_noput;
|
|
error:
|
|
afs_unuse_cell(cell, afs_cell_trace_unuse_lookup_error);
|
|
error_noput:
|
|
_leave(" = %d [error]", ret);
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
/*
|
|
* set the root cell information
|
|
* - can be called with a module parameter string
|
|
* - can be called from a write to /proc/fs/afs/rootcell
|
|
*/
|
|
int afs_cell_init(struct afs_net *net, const char *rootcell)
|
|
{
|
|
struct afs_cell *old_root, *new_root;
|
|
const char *cp, *vllist;
|
|
size_t len;
|
|
|
|
_enter("");
|
|
|
|
if (!rootcell) {
|
|
/* module is loaded with no parameters, or built statically.
|
|
* - in the future we might initialize cell DB here.
|
|
*/
|
|
_leave(" = 0 [no root]");
|
|
return 0;
|
|
}
|
|
|
|
cp = strchr(rootcell, ':');
|
|
if (!cp) {
|
|
_debug("kAFS: no VL server IP addresses specified");
|
|
vllist = NULL;
|
|
len = strlen(rootcell);
|
|
} else {
|
|
vllist = cp + 1;
|
|
len = cp - rootcell;
|
|
}
|
|
|
|
if (len == 0 || !rootcell[0] || rootcell[0] == '.' || rootcell[len - 1] == '.')
|
|
return -EINVAL;
|
|
if (memchr(rootcell, '/', len))
|
|
return -EINVAL;
|
|
cp = strstr(rootcell, "..");
|
|
if (cp && cp < rootcell + len)
|
|
return -EINVAL;
|
|
|
|
/* allocate a cell record for the root/workstation cell */
|
|
new_root = afs_lookup_cell(net, rootcell, len, vllist,
|
|
AFS_LOOKUP_CELL_ROOTCELL,
|
|
afs_cell_trace_use_lookup_ws);
|
|
if (IS_ERR(new_root)) {
|
|
_leave(" = %ld", PTR_ERR(new_root));
|
|
return PTR_ERR(new_root);
|
|
}
|
|
|
|
if (!test_and_set_bit(AFS_CELL_FL_NO_GC, &new_root->flags))
|
|
afs_use_cell(new_root, afs_cell_trace_use_pin);
|
|
|
|
/* install the new cell */
|
|
down_write(&net->cells_lock);
|
|
old_root = rcu_replace_pointer(net->ws_cell, new_root,
|
|
lockdep_is_held(&net->cells_lock));
|
|
up_write(&net->cells_lock);
|
|
|
|
afs_unuse_cell(old_root, afs_cell_trace_unuse_ws);
|
|
_leave(" = 0");
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Update a cell's VL server address list from the DNS.
|
|
*/
|
|
static int afs_update_cell(struct afs_cell *cell)
|
|
{
|
|
struct afs_vlserver_list *vllist, *old = NULL, *p;
|
|
unsigned int min_ttl = READ_ONCE(afs_cell_min_ttl);
|
|
unsigned int max_ttl = READ_ONCE(afs_cell_max_ttl);
|
|
time64_t now, expiry = 0;
|
|
int ret = 0;
|
|
|
|
_enter("%s", cell->name);
|
|
|
|
vllist = afs_dns_query(cell, &expiry);
|
|
if (IS_ERR(vllist)) {
|
|
ret = PTR_ERR(vllist);
|
|
|
|
_debug("%s: fail %d", cell->name, ret);
|
|
if (ret == -ENOMEM)
|
|
goto out_wake;
|
|
|
|
vllist = afs_alloc_vlserver_list(0);
|
|
if (!vllist) {
|
|
if (ret >= 0)
|
|
ret = -ENOMEM;
|
|
goto out_wake;
|
|
}
|
|
|
|
switch (ret) {
|
|
case -ENODATA:
|
|
case -EDESTADDRREQ:
|
|
vllist->status = DNS_LOOKUP_GOT_NOT_FOUND;
|
|
break;
|
|
case -EAGAIN:
|
|
case -ECONNREFUSED:
|
|
vllist->status = DNS_LOOKUP_GOT_TEMP_FAILURE;
|
|
break;
|
|
default:
|
|
vllist->status = DNS_LOOKUP_GOT_LOCAL_FAILURE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
_debug("%s: got list %d %d", cell->name, vllist->source, vllist->status);
|
|
cell->dns_status = vllist->status;
|
|
|
|
now = ktime_get_real_seconds();
|
|
if (min_ttl > max_ttl)
|
|
max_ttl = min_ttl;
|
|
if (expiry < now + min_ttl)
|
|
expiry = now + min_ttl;
|
|
else if (expiry > now + max_ttl)
|
|
expiry = now + max_ttl;
|
|
|
|
_debug("%s: status %d", cell->name, vllist->status);
|
|
if (vllist->source == DNS_RECORD_UNAVAILABLE) {
|
|
switch (vllist->status) {
|
|
case DNS_LOOKUP_GOT_NOT_FOUND:
|
|
/* The DNS said that the cell does not exist or there
|
|
* weren't any addresses to be had.
|
|
*/
|
|
cell->dns_expiry = expiry;
|
|
break;
|
|
|
|
case DNS_LOOKUP_BAD:
|
|
case DNS_LOOKUP_GOT_LOCAL_FAILURE:
|
|
case DNS_LOOKUP_GOT_TEMP_FAILURE:
|
|
case DNS_LOOKUP_GOT_NS_FAILURE:
|
|
default:
|
|
cell->dns_expiry = now + 10;
|
|
break;
|
|
}
|
|
} else {
|
|
cell->dns_expiry = expiry;
|
|
}
|
|
|
|
/* Replace the VL server list if the new record has servers or the old
|
|
* record doesn't.
|
|
*/
|
|
write_lock(&cell->vl_servers_lock);
|
|
p = rcu_dereference_protected(cell->vl_servers, true);
|
|
if (vllist->nr_servers > 0 || p->nr_servers == 0) {
|
|
rcu_assign_pointer(cell->vl_servers, vllist);
|
|
cell->dns_source = vllist->source;
|
|
old = p;
|
|
}
|
|
write_unlock(&cell->vl_servers_lock);
|
|
afs_put_vlserverlist(cell->net, old);
|
|
|
|
out_wake:
|
|
smp_store_release(&cell->dns_lookup_count,
|
|
cell->dns_lookup_count + 1); /* vs source/status */
|
|
wake_up_var(&cell->dns_lookup_count);
|
|
_leave(" = %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Destroy a cell record
|
|
*/
|
|
static void afs_cell_destroy(struct rcu_head *rcu)
|
|
{
|
|
struct afs_cell *cell = container_of(rcu, struct afs_cell, rcu);
|
|
struct afs_net *net = cell->net;
|
|
int r;
|
|
|
|
_enter("%p{%s}", cell, cell->name);
|
|
|
|
r = refcount_read(&cell->ref);
|
|
ASSERTCMP(r, ==, 0);
|
|
trace_afs_cell(cell->debug_id, r, atomic_read(&cell->active), afs_cell_trace_free);
|
|
|
|
afs_put_vlserverlist(net, rcu_access_pointer(cell->vl_servers));
|
|
afs_unuse_cell(cell->alias_of, afs_cell_trace_unuse_alias);
|
|
key_put(cell->anonymous_key);
|
|
idr_remove(&net->cells_dyn_ino, cell->dynroot_ino);
|
|
kfree(cell->name - 1);
|
|
kfree(cell);
|
|
|
|
afs_dec_cells_outstanding(net);
|
|
_leave(" [destroyed]");
|
|
}
|
|
|
|
static void afs_destroy_cell_work(struct work_struct *work)
|
|
{
|
|
struct afs_cell *cell = container_of(work, struct afs_cell, destroyer);
|
|
|
|
afs_see_cell(cell, afs_cell_trace_destroy);
|
|
timer_delete_sync(&cell->management_timer);
|
|
cancel_work_sync(&cell->manager);
|
|
call_rcu(&cell->rcu, afs_cell_destroy);
|
|
}
|
|
|
|
/*
|
|
* Get a reference on a cell record.
|
|
*/
|
|
struct afs_cell *afs_get_cell(struct afs_cell *cell, enum afs_cell_trace reason)
|
|
{
|
|
int r;
|
|
|
|
__refcount_inc(&cell->ref, &r);
|
|
trace_afs_cell(cell->debug_id, r + 1, atomic_read(&cell->active), reason);
|
|
return cell;
|
|
}
|
|
|
|
/*
|
|
* Drop a reference on a cell record.
|
|
*/
|
|
void afs_put_cell(struct afs_cell *cell, enum afs_cell_trace reason)
|
|
{
|
|
if (cell) {
|
|
unsigned int debug_id = cell->debug_id;
|
|
unsigned int a;
|
|
bool zero;
|
|
int r;
|
|
|
|
a = atomic_read(&cell->active);
|
|
zero = __refcount_dec_and_test(&cell->ref, &r);
|
|
trace_afs_cell(debug_id, r - 1, a, reason);
|
|
if (zero) {
|
|
a = atomic_read(&cell->active);
|
|
WARN(a != 0, "Cell active count %u > 0\n", a);
|
|
WARN_ON(!queue_work(afs_wq, &cell->destroyer));
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Note a cell becoming more active.
|
|
*/
|
|
struct afs_cell *afs_use_cell(struct afs_cell *cell, enum afs_cell_trace reason)
|
|
{
|
|
int r, a;
|
|
|
|
__refcount_inc(&cell->ref, &r);
|
|
a = atomic_inc_return(&cell->active);
|
|
trace_afs_cell(cell->debug_id, r + 1, a, reason);
|
|
return cell;
|
|
}
|
|
|
|
/*
|
|
* Record a cell becoming less active. When the active counter reaches 1, it
|
|
* is scheduled for destruction, but may get reactivated.
|
|
*/
|
|
void afs_unuse_cell(struct afs_cell *cell, enum afs_cell_trace reason)
|
|
{
|
|
unsigned int debug_id;
|
|
time64_t now, expire_delay;
|
|
bool zero;
|
|
int r, a;
|
|
|
|
if (!cell)
|
|
return;
|
|
|
|
_enter("%s", cell->name);
|
|
|
|
now = ktime_get_real_seconds();
|
|
cell->last_inactive = now;
|
|
expire_delay = 0;
|
|
if (cell->vl_servers->nr_servers)
|
|
expire_delay = afs_cell_gc_delay;
|
|
|
|
debug_id = cell->debug_id;
|
|
a = atomic_dec_return(&cell->active);
|
|
if (!a)
|
|
/* 'cell' may now be garbage collected. */
|
|
afs_set_cell_timer(cell, expire_delay);
|
|
|
|
zero = __refcount_dec_and_test(&cell->ref, &r);
|
|
trace_afs_cell(debug_id, r - 1, a, reason);
|
|
if (zero)
|
|
WARN_ON(!queue_work(afs_wq, &cell->destroyer));
|
|
}
|
|
|
|
/*
|
|
* Note that a cell has been seen.
|
|
*/
|
|
void afs_see_cell(struct afs_cell *cell, enum afs_cell_trace reason)
|
|
{
|
|
int r, a;
|
|
|
|
r = refcount_read(&cell->ref);
|
|
a = atomic_read(&cell->active);
|
|
trace_afs_cell(cell->debug_id, r, a, reason);
|
|
}
|
|
|
|
/*
|
|
* Queue a cell for management, giving the workqueue a ref to hold.
|
|
*/
|
|
void afs_queue_cell(struct afs_cell *cell, enum afs_cell_trace reason)
|
|
{
|
|
queue_work(afs_wq, &cell->manager);
|
|
}
|
|
|
|
/*
|
|
* Cell-specific management timer.
|
|
*/
|
|
static void afs_cell_timer(struct timer_list *timer)
|
|
{
|
|
struct afs_cell *cell = container_of(timer, struct afs_cell, management_timer);
|
|
|
|
afs_see_cell(cell, afs_cell_trace_see_mgmt_timer);
|
|
if (refcount_read(&cell->ref) > 0 && cell->net->live)
|
|
queue_work(afs_wq, &cell->manager);
|
|
}
|
|
|
|
/*
|
|
* Set/reduce the cell timer.
|
|
*/
|
|
void afs_set_cell_timer(struct afs_cell *cell, unsigned int delay_secs)
|
|
{
|
|
timer_reduce(&cell->management_timer, jiffies + delay_secs * HZ);
|
|
}
|
|
|
|
/*
|
|
* Activate a cell.
|
|
*/
|
|
static int afs_activate_cell(struct afs_net *net, struct afs_cell *cell)
|
|
{
|
|
struct hlist_node **p;
|
|
struct afs_cell *pcell;
|
|
int ret;
|
|
|
|
ret = afs_proc_cell_setup(cell);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
mutex_lock(&net->proc_cells_lock);
|
|
for (p = &net->proc_cells.first; *p; p = &(*p)->next) {
|
|
pcell = hlist_entry(*p, struct afs_cell, proc_link);
|
|
if (strcmp(cell->name, pcell->name) < 0)
|
|
break;
|
|
}
|
|
|
|
cell->proc_link.pprev = p;
|
|
cell->proc_link.next = *p;
|
|
rcu_assign_pointer(*p, &cell->proc_link.next);
|
|
if (cell->proc_link.next)
|
|
cell->proc_link.next->pprev = &cell->proc_link.next;
|
|
|
|
mutex_unlock(&net->proc_cells_lock);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Deactivate a cell.
|
|
*/
|
|
static void afs_deactivate_cell(struct afs_net *net, struct afs_cell *cell)
|
|
{
|
|
_enter("%s", cell->name);
|
|
|
|
afs_proc_cell_remove(cell);
|
|
|
|
mutex_lock(&net->proc_cells_lock);
|
|
if (!hlist_unhashed(&cell->proc_link))
|
|
hlist_del_rcu(&cell->proc_link);
|
|
mutex_unlock(&net->proc_cells_lock);
|
|
|
|
_leave("");
|
|
}
|
|
|
|
static bool afs_has_cell_expired(struct afs_cell *cell, time64_t *_next_manage)
|
|
{
|
|
const struct afs_vlserver_list *vllist;
|
|
time64_t expire_at = cell->last_inactive;
|
|
time64_t now = ktime_get_real_seconds();
|
|
|
|
if (atomic_read(&cell->active))
|
|
return false;
|
|
if (!cell->net->live)
|
|
return true;
|
|
|
|
vllist = rcu_dereference_protected(cell->vl_servers, true);
|
|
if (vllist && vllist->nr_servers > 0)
|
|
expire_at += afs_cell_gc_delay;
|
|
|
|
if (expire_at <= now)
|
|
return true;
|
|
if (expire_at < *_next_manage)
|
|
*_next_manage = expire_at;
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Manage a cell record, initialising and destroying it, maintaining its DNS
|
|
* records.
|
|
*/
|
|
static bool afs_manage_cell(struct afs_cell *cell)
|
|
{
|
|
struct afs_net *net = cell->net;
|
|
time64_t next_manage = TIME64_MAX;
|
|
int ret;
|
|
|
|
_enter("%s", cell->name);
|
|
|
|
_debug("state %u", cell->state);
|
|
switch (cell->state) {
|
|
case AFS_CELL_SETTING_UP:
|
|
goto set_up_cell;
|
|
case AFS_CELL_UNLOOKED:
|
|
case AFS_CELL_ACTIVE:
|
|
goto cell_is_active;
|
|
case AFS_CELL_REMOVING:
|
|
WARN_ON_ONCE(1);
|
|
return false;
|
|
case AFS_CELL_DEAD:
|
|
return false;
|
|
default:
|
|
_debug("bad state %u", cell->state);
|
|
WARN_ON_ONCE(1); /* Unhandled state */
|
|
return false;
|
|
}
|
|
|
|
set_up_cell:
|
|
ret = afs_activate_cell(net, cell);
|
|
if (ret < 0) {
|
|
cell->error = ret;
|
|
goto remove_cell;
|
|
}
|
|
|
|
afs_set_cell_state(cell, AFS_CELL_UNLOOKED);
|
|
|
|
cell_is_active:
|
|
if (afs_has_cell_expired(cell, &next_manage))
|
|
goto remove_cell;
|
|
|
|
if (test_and_clear_bit(AFS_CELL_FL_DO_LOOKUP, &cell->flags)) {
|
|
ret = afs_update_cell(cell);
|
|
if (ret < 0)
|
|
cell->error = ret;
|
|
if (cell->state == AFS_CELL_UNLOOKED)
|
|
afs_set_cell_state(cell, AFS_CELL_ACTIVE);
|
|
}
|
|
|
|
if (next_manage < TIME64_MAX && cell->net->live) {
|
|
time64_t now = ktime_get_real_seconds();
|
|
|
|
if (next_manage - now <= 0)
|
|
afs_queue_cell(cell, afs_cell_trace_queue_again);
|
|
else
|
|
afs_set_cell_timer(cell, next_manage - now);
|
|
}
|
|
_leave(" [done %u]", cell->state);
|
|
return false;
|
|
|
|
remove_cell:
|
|
down_write(&net->cells_lock);
|
|
|
|
if (atomic_read(&cell->active)) {
|
|
up_write(&net->cells_lock);
|
|
goto cell_is_active;
|
|
}
|
|
|
|
/* Make sure that the expiring server records are going to see the fact
|
|
* that the cell is caput.
|
|
*/
|
|
afs_set_cell_state(cell, AFS_CELL_REMOVING);
|
|
|
|
afs_deactivate_cell(net, cell);
|
|
afs_purge_servers(cell);
|
|
|
|
rb_erase(&cell->net_node, &net->cells);
|
|
afs_see_cell(cell, afs_cell_trace_unuse_delete);
|
|
up_write(&net->cells_lock);
|
|
|
|
/* The root volume is pinning the cell */
|
|
afs_put_volume(cell->root_volume, afs_volume_trace_put_cell_root);
|
|
cell->root_volume = NULL;
|
|
|
|
afs_set_cell_state(cell, AFS_CELL_DEAD);
|
|
return true;
|
|
}
|
|
|
|
static void afs_manage_cell_work(struct work_struct *work)
|
|
{
|
|
struct afs_cell *cell = container_of(work, struct afs_cell, manager);
|
|
bool final_put;
|
|
|
|
afs_see_cell(cell, afs_cell_trace_manage);
|
|
final_put = afs_manage_cell(cell);
|
|
afs_see_cell(cell, afs_cell_trace_managed);
|
|
if (final_put)
|
|
afs_put_cell(cell, afs_cell_trace_put_final);
|
|
}
|
|
|
|
/*
|
|
* Purge in-memory cell database.
|
|
*/
|
|
void afs_cell_purge(struct afs_net *net)
|
|
{
|
|
struct afs_cell *ws;
|
|
struct rb_node *cursor;
|
|
|
|
_enter("");
|
|
|
|
down_write(&net->cells_lock);
|
|
ws = rcu_replace_pointer(net->ws_cell, NULL,
|
|
lockdep_is_held(&net->cells_lock));
|
|
up_write(&net->cells_lock);
|
|
afs_unuse_cell(ws, afs_cell_trace_unuse_ws);
|
|
|
|
_debug("kick cells");
|
|
down_read(&net->cells_lock);
|
|
for (cursor = rb_first(&net->cells); cursor; cursor = rb_next(cursor)) {
|
|
struct afs_cell *cell = rb_entry(cursor, struct afs_cell, net_node);
|
|
|
|
afs_see_cell(cell, afs_cell_trace_purge);
|
|
|
|
if (test_and_clear_bit(AFS_CELL_FL_NO_GC, &cell->flags))
|
|
afs_unuse_cell(cell, afs_cell_trace_unuse_pin);
|
|
|
|
afs_queue_cell(cell, afs_cell_trace_queue_purge);
|
|
}
|
|
up_read(&net->cells_lock);
|
|
|
|
_debug("wait");
|
|
wait_var_event(&net->cells_outstanding,
|
|
!atomic_read(&net->cells_outstanding));
|
|
_leave("");
|
|
}
|