root/drivers/nvdimm/badrange.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright(c) 2017 Intel Corporation. All rights reserved.
 */
#include <linux/libnvdimm.h>
#include <linux/badblocks.h>
#include <linux/export.h>
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/device.h>
#include <linux/ctype.h>
#include <linux/ndctl.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/io.h>
#include "nd-core.h"
#include "nd.h"

void badrange_init(struct badrange *badrange)
{
        INIT_LIST_HEAD(&badrange->list);
        spin_lock_init(&badrange->lock);
}
EXPORT_SYMBOL_GPL(badrange_init);

static void append_badrange_entry(struct badrange *badrange,
                struct badrange_entry *bre, u64 addr, u64 length)
{
        lockdep_assert_held(&badrange->lock);
        bre->start = addr;
        bre->length = length;
        list_add_tail(&bre->list, &badrange->list);
}

static int alloc_and_append_badrange_entry(struct badrange *badrange,
                u64 addr, u64 length, gfp_t flags)
{
        struct badrange_entry *bre;

        bre = kzalloc_obj(*bre, flags);
        if (!bre)
                return -ENOMEM;

        append_badrange_entry(badrange, bre, addr, length);
        return 0;
}

static int add_badrange(struct badrange *badrange, u64 addr, u64 length)
{
        struct badrange_entry *bre, *bre_new;

        spin_unlock(&badrange->lock);
        bre_new = kzalloc_obj(*bre_new);
        spin_lock(&badrange->lock);

        if (list_empty(&badrange->list)) {
                if (!bre_new)
                        return -ENOMEM;
                append_badrange_entry(badrange, bre_new, addr, length);
                return 0;
        }

        /*
         * There is a chance this is a duplicate, check for those first.
         * This will be the common case as ARS_STATUS returns all known
         * errors in the SPA space, and we can't query it per region
         */
        list_for_each_entry(bre, &badrange->list, list)
                if (bre->start == addr) {
                        /* If length has changed, update this list entry */
                        if (bre->length != length)
                                bre->length = length;
                        kfree(bre_new);
                        return 0;
                }

        /*
         * If not a duplicate or a simple length update, add the entry as is,
         * as any overlapping ranges will get resolved when the list is consumed
         * and converted to badblocks
         */
        if (!bre_new)
                return -ENOMEM;
        append_badrange_entry(badrange, bre_new, addr, length);

        return 0;
}

int badrange_add(struct badrange *badrange, u64 addr, u64 length)
{
        int rc;

        spin_lock(&badrange->lock);
        rc = add_badrange(badrange, addr, length);
        spin_unlock(&badrange->lock);

        return rc;
}
EXPORT_SYMBOL_GPL(badrange_add);

void badrange_forget(struct badrange *badrange, phys_addr_t start,
                unsigned int len)
{
        struct list_head *badrange_list = &badrange->list;
        u64 clr_end = start + len - 1;
        struct badrange_entry *bre, *next;

        spin_lock(&badrange->lock);

        /*
         * [start, clr_end] is the badrange interval being cleared.
         * [bre->start, bre_end] is the badrange_list entry we're comparing
         * the above interval against. The badrange list entry may need
         * to be modified (update either start or length), deleted, or
         * split into two based on the overlap characteristics
         */

        list_for_each_entry_safe(bre, next, badrange_list, list) {
                u64 bre_end = bre->start + bre->length - 1;

                /* Skip intervals with no intersection */
                if (bre_end < start)
                        continue;
                if (bre->start >  clr_end)
                        continue;
                /* Delete completely overlapped badrange entries */
                if ((bre->start >= start) && (bre_end <= clr_end)) {
                        list_del(&bre->list);
                        kfree(bre);
                        continue;
                }
                /* Adjust start point of partially cleared entries */
                if ((start <= bre->start) && (clr_end > bre->start)) {
                        bre->length -= clr_end - bre->start + 1;
                        bre->start = clr_end + 1;
                        continue;
                }
                /* Adjust bre->length for partial clearing at the tail end */
                if ((bre->start < start) && (bre_end <= clr_end)) {
                        /* bre->start remains the same */
                        bre->length = start - bre->start;
                        continue;
                }
                /*
                 * If clearing in the middle of an entry, we split it into
                 * two by modifying the current entry to represent one half of
                 * the split, and adding a new entry for the second half.
                 */
                if ((bre->start < start) && (bre_end > clr_end)) {
                        u64 new_start = clr_end + 1;
                        u64 new_len = bre_end - new_start + 1;

                        /* Add new entry covering the right half */
                        alloc_and_append_badrange_entry(badrange, new_start,
                                        new_len, GFP_NOWAIT);
                        /* Adjust this entry to cover the left half */
                        bre->length = start - bre->start;
                        continue;
                }
        }
        spin_unlock(&badrange->lock);
}
EXPORT_SYMBOL_GPL(badrange_forget);

static void set_badblock(struct badblocks *bb, sector_t s, int num)
{
        dev_dbg(bb->dev, "Found a bad range (0x%llx, 0x%llx)\n",
                        (u64) s * 512, (u64) num * 512);
        /* this isn't an error as the hardware will still throw an exception */
        if (!badblocks_set(bb, s, num, 1))
                dev_info_once(bb->dev, "%s: failed for sector %llx\n",
                                __func__, (u64) s);
}

/**
 * __add_badblock_range() - Convert a physical address range to bad sectors
 * @bb:         badblocks instance to populate
 * @ns_offset:  namespace offset where the error range begins (in bytes)
 * @len:        number of bytes of badrange to be added
 *
 * This assumes that the range provided with (ns_offset, len) is within
 * the bounds of physical addresses for this namespace, i.e. lies in the
 * interval [ns_start, ns_start + ns_size)
 */
static void __add_badblock_range(struct badblocks *bb, u64 ns_offset, u64 len)
{
        const unsigned int sector_size = 512;
        sector_t start_sector, end_sector;
        u64 num_sectors;
        u32 rem;

        start_sector = div_u64(ns_offset, sector_size);
        end_sector = div_u64_rem(ns_offset + len, sector_size, &rem);
        if (rem)
                end_sector++;
        num_sectors = end_sector - start_sector;

        if (unlikely(num_sectors > (u64)INT_MAX)) {
                u64 remaining = num_sectors;
                sector_t s = start_sector;

                while (remaining) {
                        int done = min_t(u64, remaining, INT_MAX);

                        set_badblock(bb, s, done);
                        remaining -= done;
                        s += done;
                }
        } else
                set_badblock(bb, start_sector, num_sectors);
}

static void badblocks_populate(struct badrange *badrange,
                struct badblocks *bb, const struct range *range)
{
        struct badrange_entry *bre;

        if (list_empty(&badrange->list))
                return;

        list_for_each_entry(bre, &badrange->list, list) {
                u64 bre_end = bre->start + bre->length - 1;

                /* Discard intervals with no intersection */
                if (bre_end < range->start)
                        continue;
                if (bre->start > range->end)
                        continue;
                /* Deal with any overlap after start of the namespace */
                if (bre->start >= range->start) {
                        u64 start = bre->start;
                        u64 len;

                        if (bre_end <= range->end)
                                len = bre->length;
                        else
                                len = range->start + range_len(range)
                                        - bre->start;
                        __add_badblock_range(bb, start - range->start, len);
                        continue;
                }
                /*
                 * Deal with overlap for badrange starting before
                 * the namespace.
                 */
                if (bre->start < range->start) {
                        u64 len;

                        if (bre_end < range->end)
                                len = bre->start + bre->length - range->start;
                        else
                                len = range_len(range);
                        __add_badblock_range(bb, 0, len);
                }
        }
}

/**
 * nvdimm_badblocks_populate() - Convert a list of badranges to badblocks
 * @nd_region: parent region of the range to interrogate
 * @bb: badblocks instance to populate
 * @range: resource range to consider
 *
 * The badrange list generated during bus initialization may contain
 * multiple, possibly overlapping physical address ranges.  Compare each
 * of these ranges to the resource range currently being initialized,
 * and add badblocks entries for all matching sub-ranges
 */
void nvdimm_badblocks_populate(struct nd_region *nd_region,
                struct badblocks *bb, const struct range *range)
{
        struct nvdimm_bus *nvdimm_bus;

        if (!is_memory(&nd_region->dev)) {
                dev_WARN_ONCE(&nd_region->dev, 1,
                                "%s only valid for pmem regions\n", __func__);
                return;
        }
        nvdimm_bus = walk_to_nvdimm_bus(&nd_region->dev);

        guard(nvdimm_bus)(&nvdimm_bus->dev);
        badblocks_populate(&nvdimm_bus->badrange, bb, range);
}
EXPORT_SYMBOL_GPL(nvdimm_badblocks_populate);