#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/random.h>
#include <linux/cpuhotplug.h>
#include <linux/reboot.h>
#include <asm/mshyperv.h>
#include <linux/acpi.h>
#include "mshv_eventfd.h"
#include "mshv.h"
static int synic_cpuhp_online;
static struct hv_synic_pages __percpu *synic_pages;
static int mshv_sint_vector = -1;
static int mshv_sint_irq = -1;
static u32 synic_event_ring_get_queued_port(u32 sint_index)
{
struct hv_synic_event_ring_page **event_ring_page;
volatile struct hv_synic_event_ring *ring;
struct hv_synic_pages *spages;
u8 **synic_eventring_tail;
u32 message;
u8 tail;
spages = this_cpu_ptr(synic_pages);
event_ring_page = &spages->synic_event_ring_page;
synic_eventring_tail = (u8 **)this_cpu_ptr(hv_synic_eventring_tail);
if (unlikely(!*synic_eventring_tail)) {
pr_debug("Missing synic event ring tail!\n");
return 0;
}
tail = (*synic_eventring_tail)[sint_index];
if (unlikely(!*event_ring_page)) {
pr_debug("Missing synic event ring page!\n");
return 0;
}
ring = &(*event_ring_page)->sint_event_ring[sint_index];
message = ring->data[tail];
if (!message) {
if (ring->ring_full) {
ring->ring_full = 0;
hv_call_notify_port_ring_empty(sint_index);
message = ring->data[tail];
}
if (!message) {
ring->signal_masked = 0;
mb();
message = ring->data[tail];
if (!message)
return 0;
}
ring->signal_masked = 1;
}
ring->data[tail] = 0;
if (++tail == HV_SYNIC_EVENT_RING_MESSAGE_COUNT)
tail = 0;
(*synic_eventring_tail)[sint_index] = tail;
return message;
}
static bool
mshv_doorbell_isr(struct hv_message *msg)
{
struct hv_notification_message_payload *notification;
u32 port;
if (msg->header.message_type != HVMSG_SYNIC_SINT_INTERCEPT)
return false;
notification = (struct hv_notification_message_payload *)msg->u.payload;
if (notification->sint_index != HV_SYNIC_DOORBELL_SINT_INDEX)
return false;
while ((port = synic_event_ring_get_queued_port(HV_SYNIC_DOORBELL_SINT_INDEX))) {
struct port_table_info ptinfo = { 0 };
if (mshv_portid_lookup(port, &ptinfo)) {
pr_debug("Failed to get port info from port_table!\n");
continue;
}
if (ptinfo.hv_port_type != HV_PORT_TYPE_DOORBELL) {
pr_debug("Not a doorbell port!, port: %d, port_type: %d\n",
port, ptinfo.hv_port_type);
continue;
}
ptinfo.hv_port_doorbell.doorbell_cb(port,
ptinfo.hv_port_doorbell.data);
}
return true;
}
static bool mshv_async_call_completion_isr(struct hv_message *msg)
{
bool handled = false;
struct hv_async_completion_message_payload *async_msg;
struct mshv_partition *partition;
u64 partition_id;
if (msg->header.message_type != HVMSG_ASYNC_CALL_COMPLETION)
goto out;
async_msg =
(struct hv_async_completion_message_payload *)msg->u.payload;
partition_id = async_msg->partition_id;
rcu_read_lock();
partition = mshv_partition_find(partition_id);
if (unlikely(!partition)) {
pr_debug("failed to find partition %llu\n", partition_id);
goto unlock_out;
}
partition->async_hypercall_status = async_msg->status;
complete(&partition->async_hypercall);
handled = true;
unlock_out:
rcu_read_unlock();
out:
return handled;
}
static void kick_vp(struct mshv_vp *vp)
{
atomic64_inc(&vp->run.vp_signaled_count);
vp->run.kicked_by_hv = 1;
wake_up(&vp->run.vp_suspend_queue);
}
static void
handle_bitset_message(const struct hv_vp_signal_bitset_scheduler_message *msg)
{
int bank_idx, vps_signaled = 0, bank_mask_size;
struct mshv_partition *partition;
const struct hv_vpset *vpset;
const u64 *bank_contents;
u64 partition_id = msg->partition_id;
if (msg->vp_bitset.bitset.format != HV_GENERIC_SET_SPARSE_4K) {
pr_debug("scheduler message format is not HV_GENERIC_SET_SPARSE_4K");
return;
}
if (msg->vp_count == 0) {
pr_debug("scheduler message with no VP specified");
return;
}
rcu_read_lock();
partition = mshv_partition_find(partition_id);
if (unlikely(!partition)) {
pr_debug("failed to find partition %llu\n", partition_id);
goto unlock_out;
}
vpset = &msg->vp_bitset.bitset;
bank_idx = -1;
bank_contents = vpset->bank_contents;
bank_mask_size = sizeof(vpset->valid_bank_mask) * BITS_PER_BYTE;
while (true) {
int vp_bank_idx = -1;
int vp_bank_size = sizeof(*bank_contents) * BITS_PER_BYTE;
int vp_index;
bank_idx = find_next_bit((unsigned long *)&vpset->valid_bank_mask,
bank_mask_size, bank_idx + 1);
if (bank_idx == bank_mask_size)
break;
while (true) {
struct mshv_vp *vp;
vp_bank_idx = find_next_bit((unsigned long *)bank_contents,
vp_bank_size, vp_bank_idx + 1);
if (vp_bank_idx == vp_bank_size)
break;
vp_index = (bank_idx * vp_bank_size) + vp_bank_idx;
if (unlikely(vp_index >= MSHV_MAX_VPS)) {
pr_debug("VP index %u out of bounds\n",
vp_index);
goto unlock_out;
}
vp = partition->pt_vp_array[vp_index];
if (unlikely(!vp)) {
pr_debug("failed to find VP %u\n", vp_index);
goto unlock_out;
}
kick_vp(vp);
vps_signaled++;
}
bank_contents++;
}
unlock_out:
rcu_read_unlock();
if (vps_signaled != msg->vp_count)
pr_debug("asked to signal %u VPs but only did %u\n",
msg->vp_count, vps_signaled);
}
static void
handle_pair_message(const struct hv_vp_signal_pair_scheduler_message *msg)
{
struct mshv_partition *partition = NULL;
struct mshv_vp *vp;
int idx;
rcu_read_lock();
for (idx = 0; idx < msg->vp_count; idx++) {
u64 partition_id = msg->partition_ids[idx];
u32 vp_index = msg->vp_indexes[idx];
if (idx == 0 || partition->pt_id != partition_id) {
partition = mshv_partition_find(partition_id);
if (unlikely(!partition)) {
pr_debug("failed to find partition %llu\n",
partition_id);
break;
}
}
if (unlikely(vp_index >= MSHV_MAX_VPS)) {
pr_debug("VP index %u out of bounds\n", vp_index);
break;
}
vp = partition->pt_vp_array[vp_index];
if (!vp) {
pr_debug("failed to find VP %u\n", vp_index);
break;
}
kick_vp(vp);
}
rcu_read_unlock();
}
static bool
mshv_scheduler_isr(struct hv_message *msg)
{
if (msg->header.message_type != HVMSG_SCHEDULER_VP_SIGNAL_BITSET &&
msg->header.message_type != HVMSG_SCHEDULER_VP_SIGNAL_PAIR)
return false;
if (msg->header.message_type == HVMSG_SCHEDULER_VP_SIGNAL_BITSET)
handle_bitset_message((struct hv_vp_signal_bitset_scheduler_message *)
msg->u.payload);
else
handle_pair_message((struct hv_vp_signal_pair_scheduler_message *)
msg->u.payload);
return true;
}
static bool
mshv_intercept_isr(struct hv_message *msg)
{
struct mshv_partition *partition;
bool handled = false;
struct mshv_vp *vp;
u64 partition_id;
u32 vp_index;
partition_id = msg->header.sender;
rcu_read_lock();
partition = mshv_partition_find(partition_id);
if (unlikely(!partition)) {
pr_debug("failed to find partition %llu\n",
partition_id);
goto unlock_out;
}
if (msg->header.message_type == HVMSG_X64_APIC_EOI) {
struct hv_x64_apic_eoi_message *eoi_msg =
(struct hv_x64_apic_eoi_message *)&msg->u.payload[0];
if (mshv_notify_acked_gsi(partition, eoi_msg->interrupt_vector)) {
handled = true;
goto unlock_out;
}
}
if (msg->header.message_type != HVMSG_OPAQUE_INTERCEPT) {
pr_debug("wrong message type %d", msg->header.message_type);
goto unlock_out;
}
vp_index =
((struct hv_opaque_intercept_message *)msg->u.payload)->vp_index;
vp = partition->pt_vp_array[vp_index];
if (unlikely(!vp)) {
pr_debug("failed to find VP %u\n", vp_index);
goto unlock_out;
}
kick_vp(vp);
handled = true;
unlock_out:
rcu_read_unlock();
return handled;
}
void mshv_isr(void)
{
struct hv_synic_pages *spages = this_cpu_ptr(synic_pages);
struct hv_message_page **msg_page = &spages->hyp_synic_message_page;
struct hv_message *msg;
bool handled;
if (unlikely(!(*msg_page))) {
pr_debug("Missing synic page!\n");
return;
}
msg = &((*msg_page)->sint_message[HV_SYNIC_INTERCEPTION_SINT_INDEX]);
if (msg->header.message_type == HVMSG_NONE)
return;
handled = mshv_doorbell_isr(msg);
if (!handled)
handled = mshv_scheduler_isr(msg);
if (!handled)
handled = mshv_async_call_completion_isr(msg);
if (!handled)
handled = mshv_intercept_isr(msg);
if (handled) {
msg->header.message_type = HVMSG_NONE;
mb();
if (msg->header.message_flags.msg_pending)
hv_set_non_nested_msr(HV_MSR_EOM, 0);
add_interrupt_randomness(mshv_sint_vector);
} else {
pr_warn_once("%s: unknown message type 0x%x\n", __func__,
msg->header.message_type);
}
}
static int mshv_synic_cpu_init(unsigned int cpu)
{
union hv_synic_simp simp;
union hv_synic_siefp siefp;
union hv_synic_sirbp sirbp;
union hv_synic_sint sint;
union hv_synic_scontrol sctrl;
struct hv_synic_pages *spages = this_cpu_ptr(synic_pages);
struct hv_message_page **msg_page = &spages->hyp_synic_message_page;
struct hv_synic_event_flags_page **event_flags_page =
&spages->synic_event_flags_page;
struct hv_synic_event_ring_page **event_ring_page =
&spages->synic_event_ring_page;
simp.as_uint64 = hv_get_non_nested_msr(HV_MSR_SIMP);
simp.simp_enabled = true;
*msg_page = memremap(simp.base_simp_gpa << HV_HYP_PAGE_SHIFT,
HV_HYP_PAGE_SIZE,
MEMREMAP_WB);
if (!(*msg_page))
return -EFAULT;
hv_set_non_nested_msr(HV_MSR_SIMP, simp.as_uint64);
siefp.as_uint64 = hv_get_non_nested_msr(HV_MSR_SIEFP);
siefp.siefp_enabled = true;
*event_flags_page = memremap(siefp.base_siefp_gpa << PAGE_SHIFT,
PAGE_SIZE, MEMREMAP_WB);
if (!(*event_flags_page))
goto cleanup;
hv_set_non_nested_msr(HV_MSR_SIEFP, siefp.as_uint64);
sirbp.as_uint64 = hv_get_non_nested_msr(HV_MSR_SIRBP);
sirbp.sirbp_enabled = true;
*event_ring_page = memremap(sirbp.base_sirbp_gpa << PAGE_SHIFT,
PAGE_SIZE, MEMREMAP_WB);
if (!(*event_ring_page))
goto cleanup;
hv_set_non_nested_msr(HV_MSR_SIRBP, sirbp.as_uint64);
if (mshv_sint_irq != -1)
enable_percpu_irq(mshv_sint_irq, 0);
sint.as_uint64 = 0;
sint.vector = mshv_sint_vector;
sint.masked = false;
sint.auto_eoi = hv_recommend_using_aeoi();
hv_set_non_nested_msr(HV_MSR_SINT0 + HV_SYNIC_INTERCEPTION_SINT_INDEX,
sint.as_uint64);
sint.as_uint64 = 0;
sint.vector = mshv_sint_vector;
sint.masked = false;
sint.as_intercept = 1;
sint.auto_eoi = hv_recommend_using_aeoi();
hv_set_non_nested_msr(HV_MSR_SINT0 + HV_SYNIC_DOORBELL_SINT_INDEX,
sint.as_uint64);
sctrl.as_uint64 = hv_get_non_nested_msr(HV_MSR_SCONTROL);
sctrl.enable = 1;
hv_set_non_nested_msr(HV_MSR_SCONTROL, sctrl.as_uint64);
return 0;
cleanup:
if (*event_ring_page) {
sirbp.sirbp_enabled = false;
hv_set_non_nested_msr(HV_MSR_SIRBP, sirbp.as_uint64);
memunmap(*event_ring_page);
}
if (*event_flags_page) {
siefp.siefp_enabled = false;
hv_set_non_nested_msr(HV_MSR_SIEFP, siefp.as_uint64);
memunmap(*event_flags_page);
}
if (*msg_page) {
simp.simp_enabled = false;
hv_set_non_nested_msr(HV_MSR_SIMP, simp.as_uint64);
memunmap(*msg_page);
}
return -EFAULT;
}
static int mshv_synic_cpu_exit(unsigned int cpu)
{
union hv_synic_sint sint;
union hv_synic_simp simp;
union hv_synic_siefp siefp;
union hv_synic_sirbp sirbp;
union hv_synic_scontrol sctrl;
struct hv_synic_pages *spages = this_cpu_ptr(synic_pages);
struct hv_message_page **msg_page = &spages->hyp_synic_message_page;
struct hv_synic_event_flags_page **event_flags_page =
&spages->synic_event_flags_page;
struct hv_synic_event_ring_page **event_ring_page =
&spages->synic_event_ring_page;
sint.as_uint64 = hv_get_non_nested_msr(HV_MSR_SINT0 + HV_SYNIC_INTERCEPTION_SINT_INDEX);
sint.masked = true;
hv_set_non_nested_msr(HV_MSR_SINT0 + HV_SYNIC_INTERCEPTION_SINT_INDEX,
sint.as_uint64);
sint.as_uint64 = hv_get_non_nested_msr(HV_MSR_SINT0 + HV_SYNIC_DOORBELL_SINT_INDEX);
sint.masked = true;
hv_set_non_nested_msr(HV_MSR_SINT0 + HV_SYNIC_DOORBELL_SINT_INDEX,
sint.as_uint64);
if (mshv_sint_irq != -1)
disable_percpu_irq(mshv_sint_irq);
sirbp.as_uint64 = hv_get_non_nested_msr(HV_MSR_SIRBP);
sirbp.sirbp_enabled = false;
hv_set_non_nested_msr(HV_MSR_SIRBP, sirbp.as_uint64);
memunmap(*event_ring_page);
siefp.as_uint64 = hv_get_non_nested_msr(HV_MSR_SIEFP);
siefp.siefp_enabled = false;
hv_set_non_nested_msr(HV_MSR_SIEFP, siefp.as_uint64);
memunmap(*event_flags_page);
simp.as_uint64 = hv_get_non_nested_msr(HV_MSR_SIMP);
simp.simp_enabled = false;
hv_set_non_nested_msr(HV_MSR_SIMP, simp.as_uint64);
memunmap(*msg_page);
sctrl.as_uint64 = hv_get_non_nested_msr(HV_MSR_SCONTROL);
sctrl.enable = 0;
hv_set_non_nested_msr(HV_MSR_SCONTROL, sctrl.as_uint64);
return 0;
}
int
mshv_register_doorbell(u64 partition_id, doorbell_cb_t doorbell_cb, void *data,
u64 gpa, u64 val, u64 flags)
{
struct hv_connection_info connection_info = { 0 };
union hv_connection_id connection_id = { 0 };
struct port_table_info *port_table_info;
struct hv_port_info port_info = { 0 };
union hv_port_id port_id = { 0 };
int ret;
port_table_info = kmalloc_obj(*port_table_info);
if (!port_table_info)
return -ENOMEM;
port_table_info->hv_port_type = HV_PORT_TYPE_DOORBELL;
port_table_info->hv_port_doorbell.doorbell_cb = doorbell_cb;
port_table_info->hv_port_doorbell.data = data;
ret = mshv_portid_alloc(port_table_info);
if (ret < 0) {
kfree(port_table_info);
return ret;
}
port_id.u.id = ret;
port_info.port_type = HV_PORT_TYPE_DOORBELL;
port_info.doorbell_port_info.target_sint = HV_SYNIC_DOORBELL_SINT_INDEX;
port_info.doorbell_port_info.target_vp = HV_ANY_VP;
ret = hv_call_create_port(hv_current_partition_id, port_id, partition_id,
&port_info,
0, 0, NUMA_NO_NODE);
if (ret < 0) {
mshv_portid_free(port_id.u.id);
return ret;
}
connection_id.u.id = port_id.u.id;
connection_info.port_type = HV_PORT_TYPE_DOORBELL;
connection_info.doorbell_connection_info.gpa = gpa;
connection_info.doorbell_connection_info.trigger_value = val;
connection_info.doorbell_connection_info.flags = flags;
ret = hv_call_connect_port(hv_current_partition_id, port_id, partition_id,
connection_id, &connection_info, 0, NUMA_NO_NODE);
if (ret < 0) {
hv_call_delete_port(hv_current_partition_id, port_id);
mshv_portid_free(port_id.u.id);
return ret;
}
return port_id.u.id;
}
void
mshv_unregister_doorbell(u64 partition_id, int doorbell_portid)
{
union hv_port_id port_id = { 0 };
union hv_connection_id connection_id = { 0 };
connection_id.u.id = doorbell_portid;
hv_call_disconnect_port(partition_id, connection_id);
port_id.u.id = doorbell_portid;
hv_call_delete_port(hv_current_partition_id, port_id);
mshv_portid_free(doorbell_portid);
}
static int mshv_synic_reboot_notify(struct notifier_block *nb,
unsigned long code, void *unused)
{
if (!hv_root_partition())
return 0;
cpuhp_remove_state(synic_cpuhp_online);
return 0;
}
static struct notifier_block mshv_synic_reboot_nb = {
.notifier_call = mshv_synic_reboot_notify,
};
#ifndef HYPERVISOR_CALLBACK_VECTOR
static DEFINE_PER_CPU(long, mshv_evt);
static irqreturn_t mshv_percpu_isr(int irq, void *dev_id)
{
mshv_isr();
return IRQ_HANDLED;
}
#ifdef CONFIG_ACPI
static int __init mshv_acpi_setup_sint_irq(void)
{
return acpi_register_gsi(NULL, mshv_sint_vector, ACPI_EDGE_SENSITIVE,
ACPI_ACTIVE_HIGH);
}
static void mshv_acpi_cleanup_sint_irq(void)
{
acpi_unregister_gsi(mshv_sint_vector);
}
#else
static int __init mshv_acpi_setup_sint_irq(void)
{
return -ENODEV;
}
static void mshv_acpi_cleanup_sint_irq(void)
{
}
#endif
static int __init mshv_sint_vector_setup(void)
{
int ret;
struct hv_register_assoc reg = {
.name = HV_ARM64_REGISTER_SINT_RESERVED_INTERRUPT_ID,
};
union hv_input_vtl input_vtl = { 0 };
if (acpi_disabled)
return -ENODEV;
ret = hv_call_get_vp_registers(HV_VP_INDEX_SELF, HV_PARTITION_ID_SELF,
1, input_vtl, ®);
if (ret || !reg.value.reg64)
return -ENODEV;
mshv_sint_vector = reg.value.reg64;
ret = mshv_acpi_setup_sint_irq();
if (ret < 0) {
pr_err("Failed to setup IRQ for MSHV SINT vector %d: %d\n",
mshv_sint_vector, ret);
goto out_fail;
}
mshv_sint_irq = ret;
ret = request_percpu_irq(mshv_sint_irq, mshv_percpu_isr, "MSHV",
&mshv_evt);
if (ret)
goto out_unregister;
return 0;
out_unregister:
mshv_acpi_cleanup_sint_irq();
out_fail:
return ret;
}
static void mshv_sint_vector_cleanup(void)
{
free_percpu_irq(mshv_sint_irq, &mshv_evt);
mshv_acpi_cleanup_sint_irq();
}
#else
static int __init mshv_sint_vector_setup(void)
{
mshv_sint_vector = HYPERVISOR_CALLBACK_VECTOR;
return 0;
}
static void mshv_sint_vector_cleanup(void)
{
}
#endif
int __init mshv_synic_init(struct device *dev)
{
int ret = 0;
ret = mshv_sint_vector_setup();
if (ret)
return ret;
synic_pages = alloc_percpu(struct hv_synic_pages);
if (!synic_pages) {
dev_err(dev, "Failed to allocate percpu synic page\n");
ret = -ENOMEM;
goto sint_vector_cleanup;
}
ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "mshv_synic",
mshv_synic_cpu_init,
mshv_synic_cpu_exit);
if (ret < 0) {
dev_err(dev, "Failed to setup cpu hotplug state: %i\n", ret);
goto free_synic_pages;
}
synic_cpuhp_online = ret;
ret = register_reboot_notifier(&mshv_synic_reboot_nb);
if (ret)
goto remove_cpuhp_state;
return 0;
remove_cpuhp_state:
cpuhp_remove_state(synic_cpuhp_online);
free_synic_pages:
free_percpu(synic_pages);
sint_vector_cleanup:
mshv_sint_vector_cleanup();
return ret;
}
void mshv_synic_exit(void)
{
unregister_reboot_notifier(&mshv_synic_reboot_nb);
cpuhp_remove_state(synic_cpuhp_online);
free_percpu(synic_pages);
mshv_sint_vector_cleanup();
}