From bc20ee1ca1a905fa7d04fce9863f2c58c40db2b9 Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Tue, 13 Oct 2026 11:55:58 +0300 Subject: [PATCH] kunit: Test event import, delivery and timeout accounting Move the existing client layout into a private header and link separate suites with shared fixtures. Add task-scoped usercopy and allocation boundaries around the actual ioctl and timeout paths. Use private clients without scheduling work or accessing userspace memory. Preserve payload validation and counter behavior; the delivered-timeout assertion exposes the existing double decrement. Include the private event headers needed by ordinary in-tree builds in generated kernel patches. --- doc/KUNIT.md | 2 +- scripts/generate-kernel-patch | 5 +- scst/src/kunit/event/scst_event_fixture.c | 221 ++++++++++++++ scst/src/kunit/event/scst_event_fixture.h | 65 ++++ .../event/suites/scst_kunit_event_delivery.c | 279 ++++++++++++++++++ .../event/suites/scst_kunit_event_import.c | 228 ++++++++++++++ scst/src/kunit/objects.mk | 3 + scst/src/scst_event.c | 94 +++--- scst/src/scst_event_priv.h | 27 ++ scst/src/scst_event_test.h | 23 ++ 10 files changed, 910 insertions(+), 37 deletions(-) create mode 100644 scst/src/kunit/event/scst_event_fixture.c create mode 100644 scst/src/kunit/event/scst_event_fixture.h create mode 100644 scst/src/kunit/event/suites/scst_kunit_event_delivery.c create mode 100644 scst/src/kunit/event/suites/scst_kunit_event_import.c create mode 100644 scst/src/scst_event_priv.h create mode 100644 scst/src/scst_event_test.h diff --git a/doc/KUNIT.md b/doc/KUNIT.md index d911f3899..4b8a96c7d 100644 --- a/doc/KUNIT.md +++ b/doc/KUNIT.md @@ -60,7 +60,7 @@ validates the selection against the supplied `.ko` files. | Module | Labels | | --- | --- | -| `scst.ko` | `ALUA CDB LIB ORDER UNMAP` | +| `scst.ko` | `ALUA CDB EVENT LIB ORDER UNMAP` | The runner reads suite metadata from the modules and loads selected owners with their dependencies. Unselected suites are not counted as skipped. diff --git a/scripts/generate-kernel-patch b/scripts/generate-kernel-patch index b0c5ae8f4..aa04256af 100755 --- a/scripts/generate-kernel-patch +++ b/scripts/generate-kernel-patch @@ -309,8 +309,9 @@ scst_03_public_headers="scst/include/scst.h scst/include/scst_const.h \ scst/include/scst_event.h scst/include/backport.h" scst_04_main="scst/src/scst_main.c scst/src/scst_priv.h \ scst/src/scst_copy_mgr.c scst/src/scst_dlm.c scst/src/scst_dlm.h \ -scst/src/scst_event.c scst/src/scst_no_dlm.c \ -scst/src/scst_test.h" +scst/src/scst_event.c scst/src/scst_event_priv.h scst/src/scst_no_dlm.c \ +scst/src/scst_test.h \ +scst/src/scst_event_test.h" scst_05_targ="scst/src/scst_targ.c scst/src/scst_local_cmd.c \ scst/src/scst_local_cmd.h \ scst/src/scst_targ_test.h" diff --git a/scst/src/kunit/event/scst_event_fixture.c b/scst/src/kunit/event/scst_event_fixture.c new file mode 100644 index 000000000..f9b304bba --- /dev/null +++ b/scst/src/kunit/event/scst_event_fixture.c @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "scst_event_fixture.h" + +void __user * const event_test_arg = (void __user *)0x1000UL; + +static int event_test_get_size(s32 *size, const void __user *arg) +{ + struct kunit *test = kunit_get_current_test(); + struct event_test_fixture *f = test->priv; + + KUNIT_EXPECT_PTR_EQ(test, arg, event_test_arg); + f->reads++; + if (!f->read_error) + *size = f->user_size; + return f->read_error; +} + +static int event_test_get_payload_size(u32 *size, struct scst_event_user __user *arg) +{ + s32 value; + int res = event_test_get_size(&value, arg); + + if (!res) + *size = value; + return res; +} + +static int event_test_put_size(s32 size, void __user *arg) +{ + struct kunit *test = kunit_get_current_test(); + struct event_test_fixture *f = test->priv; + + KUNIT_EXPECT_PTR_EQ(test, arg, event_test_arg); + f->puts++; + f->needed_size = size; + return f->put_error; +} + +static struct scst_event_entry *event_test_alloc(size_t size) +{ + struct event_test_fixture *f = kunit_get_current_test()->priv; + + f->allocations++; + f->allocation_size = size; + f->allocation = f->fail_alloc ? NULL : kzalloc(size, GFP_KERNEL); + return f->allocation; +} + +static unsigned long event_test_copy_from(void *to, const void __user *from, size_t size) +{ + struct kunit *test = kunit_get_current_test(); + struct event_test_fixture *f = test->priv; + size_t left = min(f->from_left, size); + + KUNIT_EXPECT_PTR_EQ(test, from, event_test_arg); + f->imports++; + f->imported = size; + /* Reject the historical entry-size copy before it can overrun the fixture. */ + if (size != f->input_size || size > sizeof(f->input)) { + KUNIT_FAIL(test, "incorrect usercopy size: %zu, expected %zu", size, f->input_size); + return size; + } + if (f->importing && + (!f->allocation || to != &f->allocation->event || + size + offsetof(struct scst_event_entry, event) > f->allocation_size)) { + KUNIT_FAIL(test, "import does not fit the allocated event member"); + return size; + } + memcpy(to, f->input, size - left); + /* copy_from_user() zeroes the uncopied tail. */ + memset((u8 *)to + size - left, 0, left); + return left; +} + +static unsigned long event_test_copy_to(void __user *to, const void *from, size_t size) +{ + struct kunit *test = kunit_get_current_test(); + struct event_test_fixture *f = test->priv; + size_t offset = offsetof(struct scst_event_user, out_event); + size_t left = min(f->to_left, size); + + KUNIT_EXPECT_EQ(test, (unsigned long)to, (unsigned long)event_test_arg + offset); + f->exports++; + f->exported = size; + if (size > sizeof(struct scst_event) + SCST_MAX_PAYLOAD) { + KUNIT_FAIL(test, "oversized event export: %zu", size); + return size; + } + memcpy(f->output + offset, from, size - left); + return left; +} + +static void event_test_notify(struct scst_event *event, void *data, int status) +{ + struct event_test_fixture *f = data; + + lockdep_assert_not_held(&scst_event_mutex); + f->notifications++; + f->notify_status = status; + f->notify_id = event->event_id; +} + +static void event_test_fill(struct scst_event *event, unsigned int length, u32 code, + const char *issuer, u32 id) +{ + unsigned int i; + + event->payload_len = length; + event->event_code = code; + event->event_id = id; + strscpy(event->issuer_name, issuer, sizeof(event->issuer_name)); + for (i = 0; i < length; i++) + event->payload[i] = i; +} + +struct scst_event *event_test_input(struct event_test_fixture *f, unsigned int length, + u32 code, const char *issuer) +{ + struct scst_event *event = (void *)f->input; + + memset(f->input, 0, sizeof(f->input)); + event_test_fill(event, length, code, issuer, 77); + f->user_size = length; + f->input_size = sizeof(*event) + length; + f->importing = true; + return event; +} + +long event_test_ioctl(struct event_test_fixture *f, unsigned int command) +{ + return scst_event_ioctl(&f->file, command, (unsigned long)event_test_arg); +} + +struct scst_event_entry *event_test_queue(struct event_test_fixture *f, + unsigned int length, u32 id, bool notify) +{ + struct scst_event_entry *e = kzalloc(sizeof(*e) + length, GFP_KERNEL); + + if (!e) + return NULL; + event_test_fill(&e->event, length, 1, "kunit", id); + INIT_DELAYED_WORK(&e->event_timeout_work, scst_event_timeout_fn); + if (notify) { + e->event_notify_fn = event_test_notify; + e->notify_fn_priv = f; + } + e->pqueued_events_cnt = &f->priv.queued_events_cnt; + list_add_tail(&e->events_list_entry, &f->priv.queued_events_list); + f->priv.queued_events_cnt++; + return e; +} + +void event_test_ack_input(struct event_test_fixture *f, u32 id, int status) +{ + struct scst_event_notify_done n = { .event_id = id, .status = status }; + + memcpy(f->input, &n, sizeof(n)); + f->input_size = sizeof(n); + f->importing = false; +} + +unsigned int event_test_list_count(struct list_head *head) +{ + struct list_head *pos; + unsigned int count = 0; + + list_for_each(pos, head) + count++; + return count; +} + +int event_test_init(struct kunit *test) +{ + struct event_test_fixture *f = kunit_kzalloc(test, sizeof(*f), GFP_KERNEL); + + test->priv = f; + if (!f) + return -ENOMEM; + INIT_LIST_HEAD(&f->priv.privs_list_entry); + INIT_LIST_HEAD(&f->priv.allowed_events_list); + INIT_LIST_HEAD(&f->priv.queued_events_list); + INIT_LIST_HEAD(&f->priv.processing_events_list); + init_waitqueue_head(&f->priv.queued_events_waitQ); + f->file.private_data = &f->priv; + f->user_size = INT_MAX; + memset(f->output, 0xa5, sizeof(f->output)); + kunit_activate_static_stub(test, scst_event_get_user_payload_size, + event_test_get_payload_size); + kunit_activate_static_stub(test, scst_event_get_user_size, event_test_get_size); + kunit_activate_static_stub(test, scst_event_put_user_size, event_test_put_size); + kunit_activate_static_stub(test, scst_event_alloc_user_entry, event_test_alloc); + kunit_activate_static_stub(test, scst_event_copy_from_user, event_test_copy_from); + kunit_activate_static_stub(test, scst_event_copy_to_user, event_test_copy_to); + return 0; +} + +static void event_test_free_list(struct list_head *head, bool work_initialized) +{ + struct scst_event_entry *e, *next; + + list_for_each_entry_safe(e, next, head, events_list_entry) { + list_del(&e->events_list_entry); + if (work_initialized) + cancel_delayed_work_sync(&e->event_timeout_work); + kfree(e); + } +} + +void event_test_exit(struct kunit *test) +{ + struct event_test_fixture *f = test->priv; + + if (!f) + return; + /* No entry or client is registered globally, and no work is scheduled. */ + event_test_free_list(&f->priv.allowed_events_list, false); + event_test_free_list(&f->priv.queued_events_list, true); + event_test_free_list(&f->priv.processing_events_list, true); + kfree(f->detached); + test->priv = NULL; +} diff --git a/scst/src/kunit/event/scst_event_fixture.h b/scst/src/kunit/event/scst_event_fixture.h new file mode 100644 index 000000000..021b19745 --- /dev/null +++ b/scst/src/kunit/event/scst_event_fixture.h @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef SCST_EVENT_FIXTURE_H +#define SCST_EVENT_FIXTURE_H + +#include "../scst_kunit.h" + +#ifdef INSIDE_KERNEL_TREE +#include +#else +#include "scst.h" +#endif +#include "../../scst_priv.h" +#include +#include +#ifdef INSIDE_KERNEL_TREE +#include +#else +#include "scst_event.h" +#endif +#include "../../scst_event_priv.h" +#include "../../scst_event_test.h" + +struct event_test_fixture { + struct file file; + struct scst_event_priv priv; + u8 input[sizeof(struct scst_event) + SCST_MAX_PAYLOAD]; + u8 output[sizeof(struct scst_event_user) + SCST_MAX_PAYLOAD + 16]; + size_t input_size; + s32 user_size; + s32 needed_size; + int read_error; + int put_error; + unsigned int reads; + unsigned int puts; + unsigned int imports; + unsigned int exports; + unsigned int allocations; + size_t from_left; + size_t to_left; + size_t imported; + size_t exported; + size_t allocation_size; + struct scst_event_entry *allocation; + struct scst_event_entry *detached; + bool fail_alloc; + bool importing; + unsigned int notifications; + int notify_status; + u32 notify_id; +}; + +/* Stubs interpret this token; it is never dereferenced as a userspace address. */ + +extern void __user * const event_test_arg; +struct scst_event *event_test_input(struct event_test_fixture *f, unsigned int length, + u32 code, const char *issuer); +long event_test_ioctl(struct event_test_fixture *f, unsigned int command); +struct scst_event_entry *event_test_queue(struct event_test_fixture *f, + unsigned int length, u32 id, bool notify); +void event_test_ack_input(struct event_test_fixture *f, u32 id, int status); +unsigned int event_test_list_count(struct list_head *head); +int event_test_init(struct kunit *test); +void event_test_exit(struct kunit *test); + +#endif /* SCST_EVENT_FIXTURE_H */ diff --git a/scst/src/kunit/event/suites/scst_kunit_event_delivery.c b/scst/src/kunit/event/suites/scst_kunit_event_delivery.c new file mode 100644 index 000000000..0d10985d1 --- /dev/null +++ b/scst/src/kunit/event/suites/scst_kunit_event_delivery.c @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "../scst_event_fixture.h" +/* Event ioctl ownership and error paths, without registering an event client. */ + +struct event_delivery_case { + const char *name; + unsigned int length; + int delta; + int result; + size_t uncopied; + bool large_buffer; + bool zero_buffer; + bool negative_buffer; + bool read_fault; + bool put_fault; + bool notify; + bool empty; + bool exiting; +}; + +static const struct event_delivery_case event_delivery_cases[] = { + { .name = "empty_payload_exact_buffer" }, + { .name = "one_byte_exact_buffer", .length = 1 }, + { .name = "binary_payload", .length = 256 }, + { .name = "maximum_payload", .length = SCST_MAX_PAYLOAD }, + { .name = "larger_buffer", .length = 3, .delta = 1 }, + { .name = "int_max_buffer", .length = 3, .large_buffer = true }, + { .name = "zero_buffer", .zero_buffer = true, .result = -ENOSPC }, + { .name = "negative_buffer", .negative_buffer = true, .result = -ENOSPC }, + { .name = "short_header", .delta = -1, .result = -ENOSPC }, + { .name = "short_payload", .length = SCST_MAX_PAYLOAD, .delta = -1, + .result = -ENOSPC }, + { .name = "needed_size_write_fault", .delta = -1, .put_fault = true, + .result = -EFAULT }, + { .name = "capacity_read_fault", .read_fault = true, .result = -EFAULT }, + { .name = "full_copy_fault", .uncopied = SIZE_MAX, .result = -EFAULT }, + { .name = "last_header_byte_fault", .uncopied = 1, .result = -EFAULT }, + { .name = "partial_payload_fault", .length = 256, .uncopied = 128, + .result = -EFAULT }, + { .name = "notify_moves_to_processing", .length = 3, .notify = true }, + { .name = "notify_copy_fault_stays_queued", .length = 3, .notify = true, + .uncopied = 1, .result = -EFAULT }, + { .name = "notify_short_buffer_stays_queued", .length = 3, .notify = true, + .delta = -1, .result = -ENOSPC }, + { .name = "empty_nonblocking_queue", .empty = true, .result = -EAGAIN }, + { .name = "client_exiting", .empty = true, .exiting = true, .result = -EINTR }, +}; + +static void event_delivery_desc(const struct event_delivery_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(event_delivery, event_delivery_cases, event_delivery_desc); + +static void event_delivery_test(struct kunit *test) +{ + const struct event_delivery_case *p = test->param_value; + struct event_test_fixture *f = test->priv; + struct scst_event_entry *e; + size_t needed = sizeof(struct scst_event) + p->length; + size_t offset = offsetof(struct scst_event_user, out_event); + bool short_buffer, copied; + unsigned int queued, processing; + size_t i, bytes; + + if (!p->empty) { + e = event_test_queue(f, p->length, 42, p->notify); + KUNIT_ASSERT_NOT_NULL(test, e); + memcpy(f->input, &e->event, needed); + KUNIT_ASSERT_NOT_NULL(test, event_test_queue(f, 0, 43, false)); + } + f->user_size = needed + p->delta; + if (p->large_buffer) + f->user_size = INT_MAX; + if (p->zero_buffer) + f->user_size = 0; + if (p->negative_buffer) + f->user_size = -1; + f->read_error = p->read_fault ? -EFAULT : 0; + f->put_error = p->put_fault ? -EFAULT : 0; + f->to_left = p->uncopied; + f->priv.going_to_exit = p->exiting; + short_buffer = !p->empty && !p->read_fault && (int)needed > f->user_size; + copied = !p->empty && !p->read_fault && !short_buffer; + + KUNIT_EXPECT_EQ(test, event_test_ioctl(f, SCST_EVENT_GET_NEXT_EVENT), (long)p->result); + KUNIT_EXPECT_EQ(test, f->reads, 1U); + KUNIT_EXPECT_EQ(test, f->puts, (unsigned int)short_buffer); + KUNIT_EXPECT_EQ(test, f->exports, (unsigned int)copied); + KUNIT_EXPECT_EQ(test, f->notifications, 0U); + if (short_buffer) + KUNIT_EXPECT_EQ(test, f->needed_size, (s32)needed); + bytes = copied ? needed - min(needed, p->uncopied) : 0; + if (copied) { + KUNIT_EXPECT_EQ(test, f->exported, needed); + KUNIT_EXPECT_EQ(test, memcmp(f->output + offset, f->input, bytes), 0); + } + /* Neither the outer size field nor bytes after the event are exported. */ + for (i = 0; i < offset; i++) + KUNIT_EXPECT_EQ(test, f->output[i], (u8)0xa5); + KUNIT_EXPECT_NULL(test, memchr_inv(f->output + offset + bytes, 0xa5, + sizeof(f->output) - offset - bytes)); + queued = p->empty ? 0 : p->result ? 2 : 1; + processing = p->notify && !p->result; + KUNIT_EXPECT_EQ(test, f->priv.queued_events_cnt, (int)queued); + KUNIT_EXPECT_EQ(test, event_test_list_count(&f->priv.queued_events_list), queued); + KUNIT_EXPECT_EQ(test, event_test_list_count(&f->priv.processing_events_list), processing); + if (queued) { + KUNIT_ASSERT_FALSE(test, list_empty(&f->priv.queued_events_list)); + e = list_first_entry(&f->priv.queued_events_list, struct scst_event_entry, + events_list_entry); + KUNIT_EXPECT_EQ(test, e->event.event_id, p->result ? 42U : 43U); + } + if (processing) { + KUNIT_ASSERT_FALSE(test, list_empty(&f->priv.processing_events_list)); + e = list_first_entry(&f->priv.processing_events_list, struct scst_event_entry, + events_list_entry); + KUNIT_EXPECT_EQ(test, e->event.event_id, 42U); + } +} + +static void event_delivery_retry_test(struct kunit *test) +{ + struct event_test_fixture *f = test->priv; + struct scst_event_user *output = (void *)f->output; + + KUNIT_ASSERT_NOT_NULL(test, event_test_queue(f, 256, 42, false)); + KUNIT_ASSERT_NOT_NULL(test, event_test_queue(f, 0, 43, false)); + f->user_size = 0; + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_GET_NEXT_EVENT), (long)-ENOSPC); + KUNIT_EXPECT_EQ(test, f->needed_size, (s32)(sizeof(struct scst_event) + 256)); + f->user_size = f->needed_size; + f->to_left = 1; + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_GET_NEXT_EVENT), (long)-EFAULT); + KUNIT_EXPECT_EQ(test, f->priv.queued_events_cnt, 2); + f->to_left = 0; + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_GET_NEXT_EVENT), 0L); + KUNIT_EXPECT_EQ(test, output->out_event.event_id, 42U); + KUNIT_EXPECT_EQ(test, f->priv.queued_events_cnt, 1); + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_GET_NEXT_EVENT), 0L); + KUNIT_EXPECT_EQ(test, output->out_event.event_id, 43U); + KUNIT_EXPECT_EQ(test, f->priv.queued_events_cnt, 0); + KUNIT_EXPECT_TRUE(test, list_empty(&f->priv.queued_events_list)); + KUNIT_EXPECT_EQ(test, event_test_ioctl(f, SCST_EVENT_GET_NEXT_EVENT), (long)-EAGAIN); +} + +struct event_ack_case { + const char *name; + int status; + int result; + size_t uncopied; + bool unknown; + bool queued; + bool repeat; +}; + +static const struct event_ack_case event_ack_cases[] = { + { .name = "successful_notification" }, + { .name = "negative_status", .status = -EIO }, + { .name = "positive_status", .status = 1 }, + { .name = "unknown_id", .unknown = true, .result = -ENOENT }, + { .name = "queued_event_is_not_delivered", .queued = true, .result = -ENOENT }, + { .name = "input_copy_fault", .uncopied = SIZE_MAX, .result = -EFAULT }, + { .name = "partial_status_copy_fault", .uncopied = 1, .result = -EFAULT }, + { .name = "acknowledge_once", .repeat = true }, +}; + +static void event_ack_desc(const struct event_ack_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(event_ack, event_ack_cases, event_ack_desc); + +static void event_ack_test(struct kunit *test) +{ + const struct event_ack_case *p = test->param_value; + struct event_test_fixture *f = test->priv; + struct scst_event_entry *e; + unsigned int processing = p->queued ? 0 : p->result ? 2 : 1; + + KUNIT_ASSERT_NOT_NULL(test, event_test_queue(f, 3, 42, true)); + KUNIT_ASSERT_NOT_NULL(test, event_test_queue(f, 3, 43, true)); + if (!p->queued) { + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_GET_NEXT_EVENT), 0L); + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_GET_NEXT_EVENT), 0L); + } + event_test_ack_input(f, p->unknown ? 99 : 42, p->status); + f->from_left = p->uncopied; + KUNIT_EXPECT_EQ(test, event_test_ioctl(f, SCST_EVENT_NOTIFY_DONE), (long)p->result); + KUNIT_EXPECT_EQ(test, f->imported, sizeof(struct scst_event_notify_done)); + KUNIT_EXPECT_EQ(test, f->notifications, (unsigned int)!p->result); + if (!p->result) { + KUNIT_EXPECT_EQ(test, f->notify_status, p->status); + KUNIT_EXPECT_EQ(test, f->notify_id, 42U); + } + if (p->repeat) { + KUNIT_EXPECT_EQ(test, event_test_ioctl(f, SCST_EVENT_NOTIFY_DONE), (long)-ENOENT); + KUNIT_EXPECT_EQ(test, f->notifications, 1U); + } + KUNIT_EXPECT_EQ(test, f->priv.queued_events_cnt, p->queued ? 2 : 0); + KUNIT_EXPECT_EQ(test, event_test_list_count(&f->priv.queued_events_list), + p->queued ? 2U : 0U); + KUNIT_EXPECT_EQ(test, event_test_list_count(&f->priv.processing_events_list), processing); + if (processing) { + KUNIT_ASSERT_FALSE(test, list_empty(&f->priv.processing_events_list)); + e = list_first_entry(&f->priv.processing_events_list, struct scst_event_entry, + events_list_entry); + KUNIT_EXPECT_EQ(test, e->event.event_id, p->result ? 42U : 43U); + } +} + +struct event_timeout_case { + const char *name; + bool delivered; + bool detached; + bool second; +}; + +static const struct event_timeout_case event_timeout_cases[] = { + { .name = "expire_queued_event", .second = true }, + { .name = "expire_delivered_event", .delivered = true }, + { .name = "delivered_timeout_preserves_queued_count", .delivered = true, .second = true }, + { .name = "already_detached_event", .detached = true }, +}; + +static void event_timeout_desc(const struct event_timeout_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(event_timeout, event_timeout_cases, event_timeout_desc); + +static void event_timeout_test(struct kunit *test) +{ + const struct event_timeout_case *p = test->param_value; + struct event_test_fixture *f = test->priv; + struct scst_event_entry *e = event_test_queue(f, 3, 42, true); + + KUNIT_ASSERT_NOT_NULL(test, e); + if (p->second) + KUNIT_ASSERT_NOT_NULL(test, event_test_queue(f, 0, 43, false)); + if (p->delivered) + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_GET_NEXT_EVENT), 0L); + if (p->detached) { + list_del_init(&e->events_list_entry); + f->priv.queued_events_cnt--; + f->detached = e; + } + /* Deterministic expiry: the delayed work was initialized but never queued. */ + scst_event_timeout_fn(&e->event_timeout_work.work); + KUNIT_EXPECT_EQ(test, f->priv.queued_events_cnt, (int)p->second); + KUNIT_EXPECT_EQ(test, event_test_list_count(&f->priv.queued_events_list), + (unsigned int)p->second); + KUNIT_EXPECT_TRUE(test, list_empty(&f->priv.processing_events_list)); + KUNIT_EXPECT_EQ(test, f->notifications, (unsigned int)!p->detached); + if (!p->detached) { + KUNIT_EXPECT_EQ(test, f->notify_status, -ETIMEDOUT); + KUNIT_EXPECT_EQ(test, f->notify_id, 42U); + } + event_test_ack_input(f, 42, 0); + KUNIT_EXPECT_EQ(test, event_test_ioctl(f, SCST_EVENT_NOTIFY_DONE), (long)-ENOENT); + KUNIT_EXPECT_EQ(test, f->notifications, (unsigned int)!p->detached); +} + +static struct kunit_case scst_event_delivery_cases[] = { + KUNIT_CASE_PARAM(event_delivery_test, event_delivery_gen_params), + KUNIT_CASE(event_delivery_retry_test), + KUNIT_CASE_PARAM(event_ack_test, event_ack_gen_params), + KUNIT_CASE_PARAM(event_timeout_test, event_timeout_gen_params), + {} +}; + +scst_kunit_suite(scst_event_delivery_suite, "EVENT", "-delivery", + .init = event_test_init, + .exit = event_test_exit, + .test_cases = scst_event_delivery_cases, +); diff --git a/scst/src/kunit/event/suites/scst_kunit_event_import.c b/scst/src/kunit/event/suites/scst_kunit_event_import.c new file mode 100644 index 000000000..05d729f98 --- /dev/null +++ b/scst/src/kunit/event/suites/scst_kunit_event_import.c @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "../scst_event_fixture.h" + +struct event_import_case { + const char *name; + int length; + int copied_delta; + size_t uncopied; + int result; + bool read_fault; + bool alloc_fault; + bool unterminated; +}; + +static const struct event_import_case event_import_cases[] = { + { .name = "wildcard_empty_payload" }, + { .name = "one_byte", .length = 1 }, + { .name = "binary_payload", .length = 256 }, + { .name = "below_payload_limit", .length = SCST_MAX_PAYLOAD - 1 }, + { .name = "at_payload_limit", .length = SCST_MAX_PAYLOAD }, + { .name = "above_payload_limit", .length = SCST_MAX_PAYLOAD + 1, .result = -EINVAL }, + { .name = "int_max_length", .length = INT_MAX, .result = -EINVAL }, + { .name = "int_min_length", .length = INT_MIN, .result = -EINVAL }, + { .name = "negative_length", .length = -1, .result = -EINVAL }, + { .name = "size_read_fault", .read_fault = true, .result = -EFAULT }, + { .name = "allocation_failure", .alloc_fault = true, .result = -ENOMEM }, + { .name = "header_copy_fault", .uncopied = SIZE_MAX, .result = -EFAULT }, + { .name = "last_header_byte_fault", .uncopied = 1, .result = -EFAULT }, + { .name = "partial_payload_fault", .length = 256, .uncopied = 128, .result = -EFAULT }, + { .name = "last_payload_byte_fault", .length = SCST_MAX_PAYLOAD, + .uncopied = 1, .result = -EFAULT }, + { .name = "length_grew_during_copy", .copied_delta = 1, .result = -EINVAL }, + { .name = "length_shrank_during_copy", .length = 1, .copied_delta = -1, + .result = -EINVAL }, + { .name = "length_became_negative", .copied_delta = -1, .result = -EINVAL }, + { .name = "length_crossed_limit_during_copy", .length = SCST_MAX_PAYLOAD, + .copied_delta = 1, .result = -EINVAL }, + { .name = "unterminated_issuer", .length = 1, .unterminated = true }, +}; + +static void event_import_desc(const struct event_import_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(event_import, event_import_cases, event_import_desc); + +static void event_import_test(struct kunit *test) +{ + const struct event_import_case *p = test->param_value; + struct event_test_fixture *f = test->priv; + bool valid_length = p->length >= 0 && p->length <= SCST_MAX_PAYLOAD; + unsigned int allocations = !p->read_fault && valid_length; + unsigned int imports = allocations && !p->alloc_fault; + struct scst_event *input = event_test_input(f, valid_length ? p->length : 0, 0, "*"); + struct scst_event_entry *e; + + f->user_size = p->length; + input->payload_len = valid_length ? p->length + p->copied_delta : p->length; + if (p->unterminated) + memset(input->issuer_name, 'x', sizeof(input->issuer_name)); + f->read_error = p->read_fault ? -EFAULT : 0; + f->fail_alloc = p->alloc_fault; + f->from_left = p->uncopied; + + KUNIT_EXPECT_EQ(test, event_test_ioctl(f, SCST_EVENT_ALLOW_EVENT), (long)p->result); + KUNIT_EXPECT_EQ(test, f->reads, 1U); + KUNIT_EXPECT_EQ(test, f->allocations, allocations); + KUNIT_EXPECT_EQ(test, f->imports, imports); + if (allocations) + KUNIT_EXPECT_EQ(test, f->allocation_size, sizeof(*e) + p->length); + if (imports) + KUNIT_EXPECT_EQ(test, f->imported, sizeof(*input) + p->length); + KUNIT_EXPECT_EQ(test, f->priv.allowed_events_cnt, !p->result); + KUNIT_EXPECT_EQ(test, event_test_list_count(&f->priv.allowed_events_list), + (unsigned int)!p->result); + if (p->result) + return; + KUNIT_ASSERT_FALSE(test, list_empty(&f->priv.allowed_events_list)); + e = list_first_entry(&f->priv.allowed_events_list, struct scst_event_entry, + events_list_entry); + input->issuer_name[sizeof(input->issuer_name) - 1] = '\0'; + KUNIT_EXPECT_EQ(test, memcmp(&e->event, input, f->input_size), 0); + KUNIT_EXPECT_NULL(test, e->event_notify_fn); + KUNIT_EXPECT_NULL(test, e->notify_fn_priv); + KUNIT_EXPECT_NULL(test, e->pqueued_events_cnt); +} + +struct event_subscription_case { + const char *name; + unsigned int command; + u32 code; + const char *issuer; + unsigned int length; + unsigned int remaining; + int result; + bool different_payload; + bool copy_fault; +}; + +static const struct event_subscription_case event_subscription_cases[] = { + { "duplicate_ignores_event_id", SCST_EVENT_ALLOW_EVENT, 1, "alpha", 3, 7, -EEXIST }, + { "different_code", SCST_EVENT_ALLOW_EVENT, 3, "alpha", 3, 15 }, + { "different_payload_length", SCST_EVENT_ALLOW_EVENT, 1, "alpha", 1, 15 }, + { "different_binary_payload", SCST_EVENT_ALLOW_EVENT, 1, "alpha", 3, 15, + .different_payload = true }, + { "wildcard_duplicate", SCST_EVENT_ALLOW_EVENT, 0, "*", 0, 7, -EEXIST }, + { "remove_exact", SCST_EVENT_DISALLOW_EVENT, 1, "alpha", 3, 6 }, + { "wildcard_code", SCST_EVENT_DISALLOW_EVENT, 0, "beta", 3, 1 }, + { "wildcard_issuer", SCST_EVENT_DISALLOW_EVENT, 1, "*", 3, 2 }, + { "wildcard_payload", SCST_EVENT_DISALLOW_EVENT, 1, "alpha", 0, 6 }, + { "remove_all", SCST_EVENT_DISALLOW_EVENT, 0, "*", 0, 0 }, + { "missing_code", SCST_EVENT_DISALLOW_EVENT, 3, "*", 0, 7, -ENOENT }, + { "different_length_does_not_match", SCST_EVENT_DISALLOW_EVENT, 1, "alpha", 1, + 7, -ENOENT }, + { "different_bytes_do_not_match", SCST_EVENT_DISALLOW_EVENT, 1, "alpha", 3, + 7, -ENOENT, .different_payload = true }, + { "failed_disallow_preserves_subscriptions", SCST_EVENT_DISALLOW_EVENT, 0, "*", 0, + 7, -EFAULT, .copy_fault = true }, +}; + +static void event_subscription_desc(const struct event_subscription_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(event_subscription, event_subscription_cases, event_subscription_desc); + +static void event_subscription_test(struct kunit *test) +{ + const struct event_subscription_case *p = test->param_value; + struct event_test_fixture *f = test->priv; + struct scst_event_entry *e; + struct scst_event *input; + unsigned int i, remaining = 0; + + for (i = 0; i < 3; i++) { + input = event_test_input(f, 3, i == 1 ? 2 : 1, i ? "beta" : "alpha"); + input->event_id = i; + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_ALLOW_EVENT), 0L); + } + input = event_test_input(f, p->length, p->code, p->issuer); + input->event_id = 3; + if (p->different_payload) + input->payload[2] ^= 0xff; + f->from_left = p->copy_fault ? 1 : 0; + KUNIT_EXPECT_EQ(test, event_test_ioctl(f, p->command), (long)p->result); + KUNIT_EXPECT_EQ(test, f->priv.allowed_events_cnt, (int)hweight32(p->remaining)); + KUNIT_EXPECT_EQ(test, event_test_list_count(&f->priv.allowed_events_list), + hweight32(p->remaining)); + list_for_each_entry(e, &f->priv.allowed_events_list, events_list_entry) { + KUNIT_ASSERT_LT(test, e->event.event_id, 4U); + remaining |= BIT(e->event.event_id); + } + KUNIT_EXPECT_EQ(test, remaining, p->remaining); +} + +static void event_subscription_limit_test(struct kunit *test) +{ + struct event_test_fixture *f = test->priv; + unsigned int i; + + for (i = 1; i <= SCST_MAX_EVENTS; i++) { + event_test_input(f, 0, i, "limit"); + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_ALLOW_EVENT), 0L); + } + event_test_input(f, 0, SCST_MAX_EVENTS + 1, "limit"); + KUNIT_EXPECT_EQ(test, event_test_ioctl(f, SCST_EVENT_ALLOW_EVENT), (long)-EMFILE); + KUNIT_EXPECT_EQ(test, f->priv.allowed_events_cnt, SCST_MAX_EVENTS); + KUNIT_EXPECT_EQ(test, event_test_list_count(&f->priv.allowed_events_list), + (unsigned int)SCST_MAX_EVENTS); + event_test_input(f, 0, 1, "limit"); + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_DISALLOW_EVENT), 0L); + KUNIT_EXPECT_EQ(test, f->priv.allowed_events_cnt, SCST_MAX_EVENTS - 1); + event_test_input(f, 0, SCST_MAX_EVENTS + 1, "limit"); + KUNIT_ASSERT_EQ(test, event_test_ioctl(f, SCST_EVENT_ALLOW_EVENT), 0L); + KUNIT_EXPECT_EQ(test, f->priv.allowed_events_cnt, SCST_MAX_EVENTS); + KUNIT_EXPECT_EQ(test, event_test_list_count(&f->priv.allowed_events_list), + (unsigned int)SCST_MAX_EVENTS); +} + +static const unsigned int event_clone_lengths[] = { 0, 1, 256, SCST_MAX_PAYLOAD }; + +static void event_clone_desc(const unsigned int *length, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "payload_%u", *length); +} + +KUNIT_ARRAY_PARAM(event_clone, event_clone_lengths, event_clone_desc); + +static void event_clone_test(struct kunit *test) +{ + struct event_test_fixture *f = test->priv; + unsigned int length = *(const unsigned int *)test->param_value; + struct scst_event_entry *original = event_test_queue(f, length, 42, false); + struct scst_event_entry *clone; + + KUNIT_ASSERT_NOT_NULL(test, original); + KUNIT_ASSERT_EQ(test, scst_clone_event(original, &f->detached), 0); + clone = f->detached; + KUNIT_ASSERT_NOT_NULL(test, clone); + KUNIT_EXPECT_PTR_NE(test, clone, original); + KUNIT_EXPECT_EQ(test, memcmp(&clone->event, &original->event, + sizeof(clone->event) + length), 0); + KUNIT_EXPECT_NULL(test, clone->pqueued_events_cnt); + KUNIT_EXPECT_NULL(test, clone->event_notify_fn); + KUNIT_EXPECT_NULL(test, clone->notify_fn_priv); + clone->event.event_id = 99; + if (length) { + clone->event.payload[length - 1] ^= 0xff; + KUNIT_EXPECT_EQ(test, original->event.payload[length - 1], (u8)(length - 1)); + } + KUNIT_EXPECT_EQ(test, original->event.event_id, 42U); +} + +static struct kunit_case scst_event_import_cases[] = { + KUNIT_CASE_PARAM(event_import_test, event_import_gen_params), + KUNIT_CASE_PARAM(event_subscription_test, event_subscription_gen_params), + KUNIT_CASE(event_subscription_limit_test), + KUNIT_CASE_PARAM(event_clone_test, event_clone_gen_params), + {} +}; + +scst_kunit_suite(scst_event_import_suite, "EVENT", "-import", + .init = event_test_init, + .exit = event_test_exit, + .test_cases = scst_event_import_cases, +); diff --git a/scst/src/kunit/objects.mk b/scst/src/kunit/objects.mk index eb9e7cceb..d4bf3f19f 100644 --- a/scst/src/kunit/objects.mk +++ b/scst/src/kunit/objects.mk @@ -4,6 +4,9 @@ scst-kunit-scst := \ kunit/alua/suites/scst_kunit_alua.o \ kunit/cdb/suites/scst_kunit_cdb.o \ + kunit/event/scst_event_fixture.o \ + kunit/event/suites/scst_kunit_event_import.o \ + kunit/event/suites/scst_kunit_event_delivery.o \ kunit/lib/suites/scst_kunit_lib.o \ kunit/order/suites/scst_kunit_order.o \ kunit/unmap/suites/scst_kunit_unmap.o diff --git a/scst/src/scst_event.c b/scst/src/scst_event.c index 913a0c8a9..185e2a4f0 100644 --- a/scst/src/scst_event.c +++ b/scst/src/scst_event.c @@ -28,6 +28,8 @@ #endif #include "scst_priv.h" +#include "scst_event_priv.h" +#include "scst_event_test.h" #undef DEFAULT_SYMBOL_NAMESPACE #define DEFAULT_SYMBOL_NAMESPACE SCST_NAMESPACE @@ -37,31 +39,52 @@ static struct class *scst_event_sysfs_class; static int scst_event_major; -#define SCST_MAX_EVENTS 2048 -#define SCST_MAX_PAYLOAD (3 * 1024) -#define SCST_DEFAULT_EVENT_TIMEOUT (60 * HZ) - -struct scst_event_priv { - struct list_head privs_list_entry; - int allowed_events_cnt; - int queued_events_cnt; - unsigned int going_to_exit:1; - unsigned int blocking:1; - pid_t owner_pid; - /* - * WARNING: payloads in events in the allowed list queued AS IS from the - * user space, so they are UNTRUSTED and can NOT be used in any other - * way, except as BLOBs for comparison! - */ - struct list_head allowed_events_list; - struct list_head queued_events_list; - wait_queue_head_t queued_events_waitQ; - struct list_head processing_events_list; -}; - -static DEFINE_MUTEX(scst_event_mutex); +SCST_KUNIT_VISIBLE +struct mutex scst_event_mutex = __MUTEX_INITIALIZER(scst_event_mutex); static LIST_HEAD(scst_event_privs_list); +SCST_KUNIT_VISIBLE +int scst_event_get_user_payload_size(u32 *size, struct scst_event_user __user *arg) +{ + SCST_KUNIT_REDIRECT(scst_event_get_user_payload_size, size, arg); + return get_user(*size, &arg->max_event_size); +} + +SCST_KUNIT_VISIBLE +int scst_event_get_user_size(s32 *size, const void __user *arg) +{ + SCST_KUNIT_REDIRECT(scst_event_get_user_size, size, arg); + return get_user(*size, (const s32 __user *)arg); +} + +SCST_KUNIT_VISIBLE +int scst_event_put_user_size(s32 size, void __user *arg) +{ + SCST_KUNIT_REDIRECT(scst_event_put_user_size, size, arg); + return put_user(size, (s32 __user *)arg); +} + +SCST_KUNIT_VISIBLE +unsigned long scst_event_copy_from_user(void *to, const void __user *from, size_t size) +{ + SCST_KUNIT_REDIRECT(scst_event_copy_from_user, to, from, size); + return copy_from_user(to, from, size); +} + +SCST_KUNIT_VISIBLE +unsigned long scst_event_copy_to_user(void __user *to, const void *from, size_t size) +{ + SCST_KUNIT_REDIRECT(scst_event_copy_to_user, to, from, size); + return copy_to_user(to, from, size); +} + +SCST_KUNIT_VISIBLE +struct scst_event_entry *scst_event_alloc_user_entry(size_t size) +{ + SCST_KUNIT_REDIRECT(scst_event_alloc_user_entry, size); + return kzalloc(size, GFP_KERNEL); +} + /* * Compares events e1_wild and e2, where e1_wild can have wildcard matching, * i.e.: @@ -97,7 +120,8 @@ out: return res; } -static void scst_event_timeout_fn(struct work_struct *work) +SCST_KUNIT_VISIBLE +void scst_event_timeout_fn(struct work_struct *work) { struct scst_event_entry *event_entry = container_of(work, struct scst_event_entry, event_timeout_work.work); @@ -132,8 +156,9 @@ out: TRACE_EXIT(); } -static int scst_clone_event(const struct scst_event_entry *orig_entry, - struct scst_event_entry **new_event_entry) +SCST_KUNIT_VISIBLE +int scst_clone_event(const struct scst_event_entry *orig_entry, + struct scst_event_entry **new_event_entry) { int res = 0; const struct scst_event *event = &orig_entry->event; @@ -582,7 +607,7 @@ static int scst_event_get_event_from_user(struct scst_event_user __user *arg, TRACE_ENTRY(); - res = get_user(payload_len, &arg->max_event_size); + res = scst_event_get_user_payload_size(&payload_len, arg); if (res != 0) { PRINT_ERROR("Failed to get payload len: %d", res); goto out; @@ -599,7 +624,7 @@ static int scst_event_get_event_from_user(struct scst_event_user __user *arg, event_entry_len = sizeof(*event_entry) + payload_len; - event_entry = kzalloc(event_entry_len, GFP_KERNEL); + event_entry = scst_event_alloc_user_entry(event_entry_len); if (!event_entry) { PRINT_ERROR("Unable to allocate event entry (size %d)", event_entry_len); @@ -612,7 +637,7 @@ static int scst_event_get_event_from_user(struct scst_event_user __user *arg, event = &event_entry->event; event_len = sizeof(*event) + payload_len; - rc = copy_from_user((u8 *)event, arg, event_len); + rc = scst_event_copy_from_user((u8 *)event, arg, event_len); if (rc != 0) { PRINT_ERROR("Failed to copy %d user's bytes", rc); res = -EFAULT; @@ -737,7 +762,7 @@ static int scst_event_user_next_event(struct scst_event_priv *priv, void __user TRACE_ENTRY(); - res = get_user(max_event_size, (int32_t __user *)arg); + res = scst_event_get_user_size(&max_event_size, arg); if (res != 0) { PRINT_ERROR("Failed to get max event size: %d", res); goto out; @@ -773,13 +798,13 @@ static int scst_event_user_next_event(struct scst_event_priv *priv, void __user if (needed_size > max_event_size) { TRACE_DBG("Too big event (size %d, max size %d)", needed_size, max_event_size); - res = put_user(needed_size, (int32_t __user *)arg); + res = scst_event_put_user_size(needed_size, arg); if (res == 0) res = -ENOSPC; goto out; } - rc = copy_to_user(&event_user->out_event, &event_entry->event, needed_size); + rc = scst_event_copy_to_user(&event_user->out_event, &event_entry->event, needed_size); if (rc != 0) { PRINT_ERROR("Copy to user failed (%d)", rc); res = -EFAULT; @@ -815,7 +840,7 @@ static int scst_event_user_notify_done(struct scst_event_priv *priv, void __user TRACE_ENTRY(); - rc = copy_from_user(&n, arg, sizeof(n)); + rc = scst_event_copy_from_user(&n, arg, sizeof(n)); if (rc != 0) { PRINT_ERROR("Failed to copy %d user's bytes of notify done", rc); res = -EFAULT; @@ -911,7 +936,8 @@ out_put: goto out; } -static long scst_event_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +SCST_KUNIT_VISIBLE +long scst_event_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { long res; struct scst_event_priv *priv; diff --git a/scst/src/scst_event_priv.h b/scst/src/scst_event_priv.h new file mode 100644 index 000000000..b4d2bd8bc --- /dev/null +++ b/scst/src/scst_event_priv.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef SCST_EVENT_PRIV_H +#define SCST_EVENT_PRIV_H + +#define SCST_MAX_EVENTS 2048 +#define SCST_MAX_PAYLOAD (3 * 1024) +#define SCST_DEFAULT_EVENT_TIMEOUT (60 * HZ) + +struct scst_event_priv { + struct list_head privs_list_entry; + int allowed_events_cnt; + int queued_events_cnt; + unsigned int going_to_exit:1; + unsigned int blocking:1; + pid_t owner_pid; + /* + * WARNING: payloads in events in the allowed list queued AS IS from the + * user space, so they are UNTRUSTED and can NOT be used in any other + * way, except as BLOBs for comparison! + */ + struct list_head allowed_events_list; + struct list_head queued_events_list; + wait_queue_head_t queued_events_waitQ; + struct list_head processing_events_list; +}; + +#endif /* SCST_EVENT_PRIV_H */ diff --git a/scst/src/scst_event_test.h b/scst/src/scst_event_test.h new file mode 100644 index 000000000..d61b4fbb1 --- /dev/null +++ b/scst/src/scst_event_test.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef SCST_EVENT_TEST_H +#define SCST_EVENT_TEST_H + +#include "scst_test.h" + +#ifdef SCST_KUNIT_ENABLED + +extern struct mutex scst_event_mutex; + +int scst_event_get_user_payload_size(u32 *size, struct scst_event_user __user *arg); +int scst_event_get_user_size(s32 *size, const void __user *arg); +int scst_event_put_user_size(s32 size, void __user *arg); +unsigned long scst_event_copy_from_user(void *to, const void __user *from, size_t size); +unsigned long scst_event_copy_to_user(void __user *to, const void *from, size_t size); +struct scst_event_entry *scst_event_alloc_user_entry(size_t size); +void scst_event_timeout_fn(struct work_struct *work); +int scst_clone_event(const struct scst_event_entry *orig_entry, + struct scst_event_entry **new_event_entry); +long scst_event_ioctl(struct file *file, unsigned int cmd, unsigned long arg); + +#endif /* SCST_KUNIT_ENABLED */ +#endif /* SCST_EVENT_TEST_H */