diff options
Diffstat (limited to 'common/rescue.c')
-rw-r--r-- | common/rescue.c | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/common/rescue.c b/common/rescue.c new file mode 100644 index 0000000..7a85ebc --- /dev/null +++ b/common/rescue.c @@ -0,0 +1,351 @@ + /* + Unix SMB/CIFS implementation. + + trivial database library, rescue attempt code. + + Copyright (C) Rusty Russell 2012 + + ** NOTE! The following LGPL license applies to the tdb + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ +#include "tdb_private.h" +#include <assert.h> + + +struct found { + tdb_off_t head; /* 0 -> invalid. */ + struct tdb_record rec; + TDB_DATA key; + bool in_hash; + bool in_free; +}; + +struct found_table { + /* As an ordered array (by head offset). */ + struct found *arr; + unsigned int num, max; +}; + +static bool looks_like_valid_record(struct tdb_context *tdb, + tdb_off_t off, + const struct tdb_record *rec, + TDB_DATA *key) +{ + unsigned int hval; + + if (rec->magic != TDB_MAGIC) + return false; + + if (rec->key_len + rec->data_len > rec->rec_len) + return false; + + if (rec->rec_len % TDB_ALIGNMENT) + return false; + + /* Next pointer must make some sense. */ + if (rec->next > 0 && rec->next < TDB_DATA_START(tdb->hash_size)) + return false; + + if (tdb_oob(tdb, rec->next, sizeof(*rec), 1)) + return false; + + key->dsize = rec->key_len; + key->dptr = tdb_alloc_read(tdb, off + sizeof(*rec), key->dsize); + if (!key->dptr) + return false; + + hval = tdb->hash_fn(key); + if (hval != rec->full_hash) { + free(key->dptr); + return false; + } + + /* Caller frees up key->dptr */ + return true; +} + +static bool add_to_table(struct found_table *found, + tdb_off_t off, + struct tdb_record *rec, + TDB_DATA key) +{ + if (found->num + 1 > found->max) { + struct found *new; + found->max = (found->max ? found->max * 2 : 128); + new = realloc(found->arr, found->max * sizeof(found->arr[0])); + if (!new) + return false; + found->arr = new; + } + + found->arr[found->num].head = off; + found->arr[found->num].rec = *rec; + found->arr[found->num].key = key; + found->arr[found->num].in_hash = false; + found->arr[found->num].in_free = false; + + found->num++; + return true; +} + +static bool walk_record(struct tdb_context *tdb, + const struct found *f, + void (*walk)(TDB_DATA, TDB_DATA, void *private_data), + void *private_data) +{ + TDB_DATA data; + + data.dsize = f->rec.data_len; + data.dptr = tdb_alloc_read(tdb, + f->head + sizeof(f->rec) + f->rec.key_len, + data.dsize); + if (!data.dptr) { + if (tdb->ecode == TDB_ERR_OOM) + return false; + /* I/O errors are expected. */ + return true; + } + + walk(f->key, data, private_data); + free(data.dptr); + return true; +} + +/* First entry which has offset >= this one. */ +static unsigned int find_entry(struct found_table *found, tdb_off_t off) +{ + unsigned int start = 0, end = found->num; + + while (start < end) { + /* We can't overflow here. */ + unsigned int mid = (start + end) / 2; + + if (off < found->arr[mid].head) { + end = mid; + } else if (off > found->arr[mid].head) { + start = mid + 1; + } else { + return mid; + } + } + + assert(start == end); + return end; +} + +static void found_in_hashchain(struct found_table *found, tdb_off_t head) +{ + unsigned int match; + + match = find_entry(found, head); + if (match < found->num && found->arr[match].head == head) { + found->arr[match].in_hash = true; + } +} + +static void mark_free_area(struct found_table *found, tdb_off_t head, + tdb_len_t len) +{ + unsigned int match; + + match = find_entry(found, head); + /* Mark everything within this free entry. */ + while (match < found->num) { + if (found->arr[match].head >= head + len) { + break; + } + found->arr[match].in_free = true; + match++; + } +} + +static int cmp_key(const void *a, const void *b) +{ + const struct found *fa = a, *fb = b; + + if (fa->key.dsize < fb->key.dsize) { + return -1; + } else if (fa->key.dsize > fb->key.dsize) { + return 1; + } + return memcmp(fa->key.dptr, fb->key.dptr, fa->key.dsize); +} + +static bool key_eq(TDB_DATA a, TDB_DATA b) +{ + return a.dsize == b.dsize + && memcmp(a.dptr, b.dptr, a.dsize) == 0; +} + +static void free_table(struct found_table *found) +{ + unsigned int i; + + for (i = 0; i < found->num; i++) { + free(found->arr[i].key.dptr); + } + free(found->arr); +} + +static void logging_suppressed(struct tdb_context *tdb, + enum tdb_debug_level level, const char *fmt, ...) +{ +} + +_PUBLIC_ int tdb_rescue(struct tdb_context *tdb, + void (*walk)(TDB_DATA, TDB_DATA, void *private_data), + void *private_data) +{ + struct found_table found = { NULL, 0, 0 }; + tdb_off_t h, off, i; + tdb_log_func oldlog = tdb->log.log_fn; + struct tdb_record rec; + TDB_DATA key; + bool locked; + + /* Read-only databases use no locking at all: it's best-effort. + * We may have a write lock already, so skip that case too. */ + if (tdb->read_only || tdb->allrecord_lock.count != 0) { + locked = false; + } else { + if (tdb_lockall_read(tdb) == -1) + return -1; + locked = true; + } + + /* Make sure we know true size of the underlying file. */ + tdb_oob(tdb, tdb->map_size, 1, 1); + + /* Suppress logging, since we anticipate errors. */ + tdb->log.log_fn = logging_suppressed; + + /* Now walk entire db looking for records. */ + for (off = TDB_DATA_START(tdb->hash_size); + off < tdb->map_size; + off += TDB_ALIGNMENT) { + if (tdb->methods->tdb_read(tdb, off, &rec, sizeof(rec), + DOCONV()) == -1) + continue; + + if (looks_like_valid_record(tdb, off, &rec, &key)) { + if (!add_to_table(&found, off, &rec, key)) { + goto oom; + } + } + } + + /* Walk hash chains to positive vet. */ + for (h = 0; h < 1+tdb->hash_size; h++) { + bool slow_chase = false; + tdb_off_t slow_off = FREELIST_TOP + h*sizeof(tdb_off_t); + + if (tdb_ofs_read(tdb, FREELIST_TOP + h*sizeof(tdb_off_t), + &off) == -1) + continue; + + while (off && off != slow_off) { + if (tdb->methods->tdb_read(tdb, off, &rec, sizeof(rec), + DOCONV()) != 0) { + break; + } + + /* 0 is the free list, rest are hash chains. */ + if (h == 0) { + /* Don't mark garbage as free. */ + if (rec.magic != TDB_FREE_MAGIC) { + break; + } + mark_free_area(&found, off, + sizeof(rec) + rec.rec_len); + } else { + found_in_hashchain(&found, off); + } + + off = rec.next; + + /* Loop detection using second pointer at half-speed */ + if (slow_chase) { + /* First entry happens to be next ptr */ + tdb_ofs_read(tdb, slow_off, &slow_off); + } + slow_chase = !slow_chase; + } + } + + /* Recovery area: must be marked as free, since it often has old + * records in there! */ + if (tdb_ofs_read(tdb, TDB_RECOVERY_HEAD, &off) == 0 && off != 0) { + if (tdb->methods->tdb_read(tdb, off, &rec, sizeof(rec), + DOCONV()) == 0) { + mark_free_area(&found, off, sizeof(rec) + rec.rec_len); + } + } + + /* Now sort by key! */ + if (found.arr != NULL) { + qsort(found.arr, found.num, sizeof(found.arr[0]), cmp_key); + } + + for (i = 0; (found.arr != NULL) && i < found.num; ) { + unsigned int num, num_in_hash = 0; + + /* How many are identical? */ + for (num = 0; num < found.num - i; num++) { + if (!key_eq(found.arr[i].key, found.arr[i+num].key)) { + break; + } + if (found.arr[i+num].in_hash) { + if (!walk_record(tdb, &found.arr[i+num], + walk, private_data)) + goto oom; + num_in_hash++; + } + } + assert(num); + + /* If none were in the hash, we print any not in free list. */ + if (num_in_hash == 0) { + unsigned int j; + + for (j = i; j < i + num; j++) { + if (!found.arr[j].in_free) { + if (!walk_record(tdb, &found.arr[j], + walk, private_data)) + goto oom; + } + } + } + + i += num; + } + + tdb->log.log_fn = oldlog; + if (locked) { + tdb_unlockall_read(tdb); + } + return 0; + +oom: + tdb->log.log_fn = oldlog; + tdb->ecode = TDB_ERR_OOM; + TDB_LOG((tdb, TDB_DEBUG_ERROR, "tdb_rescue: failed allocating\n")); + free_table(&found); + if (locked) { + tdb_unlockall_read(tdb); + } + return -1; +} |