// SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
/*
 *
 * (C) COPYRIGHT 2022-2023 ARM Limited. All rights reserved.
 *
 * This program is free software and is provided to you under the terms of the
 * GNU General Public License version 2 as published by the Free Software
 * Foundation, and any use by you of this program is subject to the terms
 * of such GNU license.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, you can access it online at
 * http://www.gnu.org/licenses/gpl-2.0.html.
 *
 */

#include <mali_kbase.h>

#if IS_ENABLED(CONFIG_DEBUG_FS)

/**
 * kbasep_fault_occurred - Check if fault occurred.
 *
 * @kbdev:  Device pointer
 *
 * Return: true if a fault occurred.
 */
static bool kbasep_fault_occurred(struct kbase_device *kbdev)
{
	unsigned long flags;
	bool ret;

	spin_lock_irqsave(&kbdev->csf.dof.lock, flags);
	ret = (kbdev->csf.dof.error_code != DF_NO_ERROR);
	spin_unlock_irqrestore(&kbdev->csf.dof.lock, flags);

	return ret;
}

void kbase_debug_csf_fault_wait_completion(struct kbase_device *kbdev)
{
	if (likely(!kbase_debug_csf_fault_dump_enabled(kbdev))) {
		dev_dbg(kbdev->dev, "No userspace client for dumping exists");
		return;
	}

	wait_event(kbdev->csf.dof.dump_wait_wq, kbase_debug_csf_fault_dump_complete(kbdev));
}
KBASE_EXPORT_TEST_API(kbase_debug_csf_fault_wait_completion);

/**
 * kbase_debug_csf_fault_wakeup - Wake up a waiting user space client.
 *
 * @kbdev:   Kbase device
 */
static void kbase_debug_csf_fault_wakeup(struct kbase_device *kbdev)
{
	wake_up_interruptible(&kbdev->csf.dof.fault_wait_wq);
}

bool kbase_debug_csf_fault_notify(struct kbase_device *kbdev, struct kbase_context *kctx,
				  enum dumpfault_error_type error)
{
	unsigned long flags;

	if (likely(!kbase_debug_csf_fault_dump_enabled(kbdev)))
		return false;

	if (WARN_ON(error == DF_NO_ERROR))
		return false;

	if (kctx && kbase_ctx_flag(kctx, KCTX_DYING)) {
		dev_info(kbdev->dev, "kctx %d_%d is dying when error %d is reported", kctx->tgid,
			 kctx->id, error);
		kctx = NULL;
	}

	spin_lock_irqsave(&kbdev->csf.dof.lock, flags);

	/* Only one fault at a time can be processed */
	if (kbdev->csf.dof.error_code) {
		dev_info(kbdev->dev, "skip this fault as there's a pending fault");
		goto unlock;
	}

	kbdev->csf.dof.kctx_tgid = kctx ? (unsigned int)kctx->tgid : 0U;
	kbdev->csf.dof.kctx_id = kctx ? kctx->id : 0U;
	kbdev->csf.dof.error_code = error;
	kbase_debug_csf_fault_wakeup(kbdev);

unlock:
	spin_unlock_irqrestore(&kbdev->csf.dof.lock, flags);
	return true;
}

static ssize_t debug_csf_fault_read(struct file *file, char __user *buffer, size_t size,
				    loff_t *f_pos)
{
#define BUF_SIZE 64
	struct kbase_device *kbdev;
	unsigned long flags;
	int count;
	char buf[BUF_SIZE];
	u32 tgid, ctx_id;
	enum dumpfault_error_type error_code;

	if (unlikely(!file)) {
		pr_warn("%s: file is NULL", __func__);
		return -EINVAL;
	}

	kbdev = file->private_data;
	if (unlikely(!buffer)) {
		dev_warn(kbdev->dev, "%s: buffer is NULL", __func__);
		return -EINVAL;
	}

	if (unlikely(*f_pos < 0)) {
		dev_warn(kbdev->dev, "%s: f_pos is negative", __func__);
		return -EINVAL;
	}

	if (size < sizeof(buf)) {
		dev_warn(kbdev->dev, "%s: buffer is too small", __func__);
		return -EINVAL;
	}

	if (wait_event_interruptible(kbdev->csf.dof.fault_wait_wq, kbasep_fault_occurred(kbdev)))
		return -ERESTARTSYS;

	spin_lock_irqsave(&kbdev->csf.dof.lock, flags);
	tgid = kbdev->csf.dof.kctx_tgid;
	ctx_id = kbdev->csf.dof.kctx_id;
	error_code = kbdev->csf.dof.error_code;
	BUILD_BUG_ON(sizeof(buf) < (sizeof(tgid) + sizeof(ctx_id) + sizeof(error_code)));
	count = scnprintf(buf, sizeof(buf), "%u_%u_%u\n", tgid, ctx_id, error_code);
	spin_unlock_irqrestore(&kbdev->csf.dof.lock, flags);

	dev_info(kbdev->dev, "debug csf fault info read");
	return simple_read_from_buffer(buffer, size, f_pos, buf, (size_t)count);
}

static int debug_csf_fault_open(struct inode *in, struct file *file)
{
	struct kbase_device *kbdev;

	if (unlikely(!in)) {
		pr_warn("%s: inode is NULL", __func__);
		return -EINVAL;
	}

	kbdev = in->i_private;
	if (unlikely(!file)) {
		dev_warn(kbdev->dev, "%s: file is NULL", __func__);
		return -EINVAL;
	}

	if (atomic_cmpxchg(&kbdev->csf.dof.enabled, 0, 1) == 1) {
		dev_warn(kbdev->dev, "Only one client is allowed for dump on fault");
		return -EBUSY;
	}

	dev_info(kbdev->dev, "debug csf fault file open");

	return simple_open(in, file);
}

static ssize_t debug_csf_fault_write(struct file *file, const char __user *ubuf, size_t count,
				     loff_t *ppos)
{
	struct kbase_device *kbdev;
	unsigned long flags;

	CSTD_UNUSED(ubuf);
	CSTD_UNUSED(ppos);

	if (unlikely(!file)) {
		pr_warn("%s: file is NULL", __func__);
		return -EINVAL;
	}

	kbdev = file->private_data;
	spin_lock_irqsave(&kbdev->csf.dof.lock, flags);
	kbdev->csf.dof.error_code = DF_NO_ERROR;
	kbdev->csf.dof.kctx_tgid = 0;
	kbdev->csf.dof.kctx_id = 0;
	dev_info(kbdev->dev, "debug csf fault dump complete");
	spin_unlock_irqrestore(&kbdev->csf.dof.lock, flags);

	/* User space finished the dump.
	 * Wake up blocked kernel threads to proceed.
	 */
	wake_up(&kbdev->csf.dof.dump_wait_wq);

	return (ssize_t)count;
}

static int debug_csf_fault_release(struct inode *in, struct file *file)
{
	struct kbase_device *kbdev;
	unsigned long flags;

	CSTD_UNUSED(file);

	if (unlikely(!in)) {
		pr_warn("%s: inode is NULL", __func__);
		return -EINVAL;
	}

	kbdev = in->i_private;
	spin_lock_irqsave(&kbdev->csf.dof.lock, flags);
	kbdev->csf.dof.kctx_tgid = 0;
	kbdev->csf.dof.kctx_id = 0;
	kbdev->csf.dof.error_code = DF_NO_ERROR;
	spin_unlock_irqrestore(&kbdev->csf.dof.lock, flags);

	atomic_set(&kbdev->csf.dof.enabled, 0);
	dev_info(kbdev->dev, "debug csf fault file close");

	/* User space closed the debugfs file.
	 * Wake up blocked kernel threads to resume.
	 */
	wake_up(&kbdev->csf.dof.dump_wait_wq);

	return 0;
}

static const struct file_operations kbasep_debug_csf_fault_fops = {
	.owner = THIS_MODULE,
	.open = debug_csf_fault_open,
	.read = debug_csf_fault_read,
	.write = debug_csf_fault_write,
	.llseek = default_llseek,
	.release = debug_csf_fault_release,
};

void kbase_debug_csf_fault_debugfs_init(struct kbase_device *kbdev)
{
	const char *fname = "csf_fault";

	if (unlikely(!kbdev)) {
		pr_warn("%s: kbdev is NULL", __func__);
		return;
	}

	debugfs_create_file(fname, 0600, kbdev->mali_debugfs_directory, kbdev,
			    &kbasep_debug_csf_fault_fops);
}

int kbase_debug_csf_fault_init(struct kbase_device *kbdev)
{
	if (unlikely(!kbdev)) {
		pr_warn("%s: kbdev is NULL", __func__);
		return -EINVAL;
	}

	init_waitqueue_head(&(kbdev->csf.dof.fault_wait_wq));
	init_waitqueue_head(&(kbdev->csf.dof.dump_wait_wq));
	spin_lock_init(&kbdev->csf.dof.lock);
	kbdev->csf.dof.kctx_tgid = 0;
	kbdev->csf.dof.kctx_id = 0;
	kbdev->csf.dof.error_code = DF_NO_ERROR;
	atomic_set(&kbdev->csf.dof.enabled, 0);

	return 0;
}

void kbase_debug_csf_fault_term(struct kbase_device *kbdev)
{
	CSTD_UNUSED(kbdev);
}
#endif /* CONFIG_DEBUG_FS */
