xen/arch/arm/Kconfig | 1 + xen/arch/x86/Kconfig | 1 + xen/common/Kconfig | 3 +++ xen/common/Makefile | 2 +- xen/common/domain.c | 6 +++--- xen/common/domctl.c | 11 ++++++++--- xen/common/event_channel.c | 12 +++++++++--- xen/common/event_channel.h | 6 ++++++ xen/common/event_fifo.c | 11 ++++++++++- xen/common/time.c | 2 ++ xen/include/xen/shared.h | 8 +++++++- xen/include/xen/time.h | 4 ++++ 12 files changed, 55 insertions(+), 12 deletions(-)
On architectures that run guests in dom0less mode without the PV ABI
(currently RISC-V), no shared_info page is allocated and d->shared_info
remains NULL throughout the domain lifetime. Several places in common
code access d->shared_info through the shared_info() macro or directly,
causing UBSAN null-pointer errors on such architectures.
Rather than adding runtime NULL guards that are logically unreachable
on x86 and Arm (where shared_info is always allocated), introduce a new
Kconfig symbol CONFIG_HAS_SHARED_INFO selected by x86 and Arm.
On !HAS_SHARED_INFO the shared_info() macro expands to a dereference
of a pointer returned by shared_info_absent(), which is declared but
intentionally never defined. Any use of shared_info() that is not
dead-code-eliminated will therefore cause a link-time failure, making
missed guards impossible to overlook.
The 2L event-channel ops call shared_info() and must not be compiled on
architectures without a shared_info page, so event_2l.o is gated on
CONFIG_HAS_SHARED_INFO. On such architectures evtchn_init() installs
the FIFO ops as a placeholder instead; evtchn_fifo_word_from_port() is
guarded against uninitialised d->evtchn_fifo so the FIFO ops are safe
before evtchn_fifo_init_control() is called by the guest.
With CONFIG_HAS_SHARED_INFO=n all vCPUs fall back to the global
dummy_vcpu_info, so writes through vcpu_info() could leak data between
vCPUs. Reviewing the write paths in common code: the write in
map_guest_area() stores the constant ~0 so nothing serious would happen
if it were leaked; the event_2l.c paths are unreachable because the
preceding shared_info() call would trap first; the write in
vcpu_info_populate() targets the new mapping buffer, not
dummy_vcpu_info.
Outside common code, the remaining writes are x86 PV-specific, for which
CONFIG_HAS_SHARED_INFO=y. No code changes are needed.
Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v4:
- event_channel.c: drop the redundant evtchn_fifo_init_ops() in the
else branch of evtchn_reset(); evtchn_fifo_destroy() does not undo the
ops installed by evtchn_init(), so only the switch back to 2-level ABI
needs an explicit call.
- shared.h: simplify the !HAS_SHARED_INFO shared_info() definition to use
an undefined "extern struct shared_info *shared_info_absent" instead of
shared_info_absent() with a typeof cast.
- Extend the commit description to note that vcpu_info()/__vcpu_info()
uses were also audited: on !HAS_SHARED_INFO vcpu_info_area.map points at
dummy_vcpu_info, reads are harmless, and writes in common code do not
open a cross-domain info-leak side channel, so no code changes are
needed on that path.
---
Changes in v3:
- Introduce CONFIG_HAS_SHARED_INFO Kconfig symbol selected by x86
and Arm; RISC-V does not select it.
- Gate shared_info() macro on CONFIG_HAS_SHARED_INFO; on
!HAS_SHARED_INFO it calls shared_info_absent() (declared, never
defined) so any unguarded use produces a link-time error.
- Replace runtime if (!d->shared_info) guards with IS_ENABLED() at
call sites so both branches type-check and dead code is eliminated.
- Guard shared_info_frame assignment in domctl.c.
- Gate event_2l.o on CONFIG_HAS_SHARED_INFO; use FIFO ops as
placeholder on !HAS_SHARED_INFO archs instead of dedicated stub
ops; guard evtchn_fifo_word_from_port() against uninitialised
d->evtchn_fifo.
- Add static inline stubs for evtchn_2l_init() (!HAS_SHARED_INFO)
and evtchn_fifo_init_ops() (!EVTCHN_FIFO) so call sites can use
IS_ENABLED() without #ifdef.
- Drop inaccurate changelog entry about "only FIFO ABI" migration.
- Update the commit message.
- Drop R-by: Baptiste ... as some extra checks are added.
---
Changes in v2:
- Update commit message + subject.
- Drop Fixes tag.
---
xen/arch/arm/Kconfig | 1 +
xen/arch/x86/Kconfig | 1 +
xen/common/Kconfig | 3 +++
xen/common/Makefile | 2 +-
xen/common/domain.c | 6 +++---
xen/common/domctl.c | 11 ++++++++---
xen/common/event_channel.c | 12 +++++++++---
xen/common/event_channel.h | 6 ++++++
xen/common/event_fifo.c | 11 ++++++++++-
xen/common/time.c | 2 ++
xen/include/xen/shared.h | 8 +++++++-
xen/include/xen/time.h | 4 ++++
12 files changed, 55 insertions(+), 12 deletions(-)
diff --git a/xen/arch/arm/Kconfig b/xen/arch/arm/Kconfig
index 5fa89fcb2428..683ab7d25a1e 100644
--- a/xen/arch/arm/Kconfig
+++ b/xen/arch/arm/Kconfig
@@ -20,6 +20,7 @@ config ARM
select HAS_DEVICE_TREE_DISCOVERY
select HAS_DOM0LESS
select HAS_GRANT_CACHE_FLUSH if GRANT_TABLE
+ select HAS_SHARED_INFO
select HAS_STACK_PROTECTOR
select HAS_UBSAN
diff --git a/xen/arch/x86/Kconfig b/xen/arch/x86/Kconfig
index 2ce4747f6ea7..49697b795259 100644
--- a/xen/arch/x86/Kconfig
+++ b/xen/arch/x86/Kconfig
@@ -29,6 +29,7 @@ config X86
select HAS_PCI_MSI
select HAS_PIRQ
select HAS_SCHED_GRANULARITY
+ select HAS_SHARED_INFO
imply HAS_SOFT_RESET
select HAS_UBSAN
select HAS_VMAP
diff --git a/xen/common/Kconfig b/xen/common/Kconfig
index 5ff71480eebe..8b48d84c79e8 100644
--- a/xen/common/Kconfig
+++ b/xen/common/Kconfig
@@ -158,6 +158,9 @@ config HAS_PMAP
config HAS_SCHED_GRANULARITY
bool
+config HAS_SHARED_INFO
+ bool
+
config HAS_SOFT_RESET
bool
diff --git a/xen/common/Makefile b/xen/common/Makefile
index 6018e256147f..f69d47d18934 100644
--- a/xen/common/Makefile
+++ b/xen/common/Makefile
@@ -12,7 +12,7 @@ obj-$(CONFIG_DEVICE_TREE_PARSE) += device-tree/
obj-$(CONFIG_IOREQ_SERVER) += dm.o
obj-y += domain.o
obj-y += domid.o
-obj-y += event_2l.o
+obj-$(CONFIG_HAS_SHARED_INFO) += event_2l.o
obj-y += event_channel.o
obj-$(CONFIG_EVTCHN_FIFO) += event_fifo.o
obj-$(CONFIG_GRANT_TABLE) += grant_table.o
diff --git a/xen/common/domain.c b/xen/common/domain.c
index 8f2bfcae2890..fba8e9161937 100644
--- a/xen/common/domain.c
+++ b/xen/common/domain.c
@@ -316,9 +316,9 @@ void vcpu_info_reset(struct vcpu *v)
struct domain *d = v->domain;
v->vcpu_info_area.map =
- ((v->vcpu_id < XEN_LEGACY_MAX_VCPUS)
- ? (vcpu_info_t *)&shared_info(d, vcpu_info[v->vcpu_id])
- : &dummy_vcpu_info);
+ IS_ENABLED(CONFIG_HAS_SHARED_INFO) && v->vcpu_id < XEN_LEGACY_MAX_VCPUS
+ ? (vcpu_info_t *)&shared_info(d, vcpu_info[v->vcpu_id])
+ : &dummy_vcpu_info;
}
static struct domain *alloc_domain_struct(void)
diff --git a/xen/common/domctl.c b/xen/common/domctl.c
index e30b38a337ac..5e5632f912d3 100644
--- a/xen/common/domctl.c
+++ b/xen/common/domctl.c
@@ -102,9 +102,14 @@ void getdomaininfo(struct domain *d, struct xen_domctl_getdomaininfo *info)
#ifdef CONFIG_MEM_PAGING
info->paged_pages = atomic_read(&d->paged_pages);
#endif
- info->shared_info_frame =
- gfn_x(mfn_to_gfn(d, _mfn(virt_to_mfn(d->shared_info))));
- BUG_ON(SHARED_M2P(info->shared_info_frame));
+ if ( IS_ENABLED(CONFIG_HAS_SHARED_INFO) )
+ {
+ info->shared_info_frame =
+ gfn_x(mfn_to_gfn(d, _mfn(virt_to_mfn(d->shared_info))));
+ BUG_ON(SHARED_M2P(info->shared_info_frame));
+ }
+ else
+ info->shared_info_frame = INVALID_GFN_RAW;
info->cpupool = cpupool_get_id(d);
diff --git a/xen/common/event_channel.c b/xen/common/event_channel.c
index a3d18bc464e8..fae58985e501 100644
--- a/xen/common/event_channel.c
+++ b/xen/common/event_channel.c
@@ -1323,9 +1323,11 @@ int evtchn_reset(struct domain *d, bool resuming)
rc = -EAGAIN;
else if ( d->evtchn_fifo )
{
- /* Switching back to 2-level ABI. */
evtchn_fifo_destroy(d);
- evtchn_2l_init(d);
+
+ if ( IS_ENABLED(CONFIG_HAS_SHARED_INFO) )
+ /* Switching back to 2-level ABI. */
+ evtchn_2l_init(d);
}
write_unlock(&d->event_lock);
@@ -1624,7 +1626,11 @@ void evtchn_check_pollers(struct domain *d, unsigned int port)
int evtchn_init(struct domain *d, unsigned int max_port)
{
- evtchn_2l_init(d);
+ if ( IS_ENABLED(CONFIG_HAS_SHARED_INFO) )
+ evtchn_2l_init(d);
+ else
+ evtchn_fifo_init_ops(d);
+
d->max_evtchn_port = min_t(unsigned int, max_port, INT_MAX);
d->evtchn = alloc_evtchn_bucket(d, 0);
diff --git a/xen/common/event_channel.h b/xen/common/event_channel.h
index dc94a43cc2dd..91f6d7b3d4bf 100644
--- a/xen/common/event_channel.h
+++ b/xen/common/event_channel.h
@@ -44,7 +44,11 @@ static inline void evtchn_port_print_state(struct domain *d,
/* 2-level */
+#ifdef CONFIG_HAS_SHARED_INFO
void evtchn_2l_init(struct domain *d);
+#else
+static inline void evtchn_2l_init(struct domain *d) {}
+#endif
/* FIFO */
@@ -55,6 +59,7 @@ struct evtchn_expand_array;
int evtchn_fifo_init_control(struct evtchn_init_control *init_control);
int evtchn_fifo_expand_array(const struct evtchn_expand_array *expand_array);
void evtchn_fifo_destroy(struct domain *d);
+void evtchn_fifo_init_ops(struct domain *d);
#else
static inline int evtchn_fifo_init_control(struct evtchn_init_control *init_control)
{
@@ -68,6 +73,7 @@ static inline void evtchn_fifo_destroy(struct domain *d)
{
return;
}
+static inline void evtchn_fifo_init_ops(struct domain *d) {}
#endif /* CONFIG_EVTCHN_FIFO */
#endif /* EVENT_CHANNEL_H */
diff --git a/xen/common/event_fifo.c b/xen/common/event_fifo.c
index 37cba9bc4564..4aa34ca13a56 100644
--- a/xen/common/event_fifo.c
+++ b/xen/common/event_fifo.c
@@ -62,6 +62,9 @@ static inline event_word_t *evtchn_fifo_word_from_port(const struct domain *d,
*/
smp_rmb();
+ if ( unlikely(!d->evtchn_fifo) )
+ return NULL;
+
if ( unlikely(port >= d->evtchn_fifo->num_evtchns) )
return NULL;
@@ -420,6 +423,11 @@ static const struct evtchn_port_ops evtchn_port_ops_fifo =
.print_state = evtchn_fifo_print_state,
};
+void evtchn_fifo_init_ops(struct domain *d)
+{
+ d->evtchn_port_ops = &evtchn_port_ops_fifo;
+}
+
static int map_guest_page(struct domain *d, uint64_t gfn, void **virt)
{
struct page_info *p;
@@ -562,7 +570,8 @@ static void setup_ports(struct domain *d, unsigned int prev_evtchns)
evtchn = evtchn_from_port(d, port);
- if ( guest_test_bit(d, port, &shared_info(d, evtchn_pending)) )
+ if ( IS_ENABLED(CONFIG_HAS_SHARED_INFO) &&
+ guest_test_bit(d, port, &shared_info(d, evtchn_pending)) )
evtchn->pending = true;
evtchn_fifo_set_priority(d, evtchn, EVTCHN_FIFO_PRIORITY_DEFAULT);
diff --git a/xen/common/time.c b/xen/common/time.c
index 04a65f00b35c..cdfdc53b6a17 100644
--- a/xen/common/time.c
+++ b/xen/common/time.c
@@ -89,6 +89,7 @@ struct tm gmtime(unsigned long t)
return tbuf;
}
+#ifdef CONFIG_HAS_SHARED_INFO
void update_domain_wallclock_time(struct domain *d)
{
uint32_t *wc_version;
@@ -117,6 +118,7 @@ void update_domain_wallclock_time(struct domain *d)
spin_unlock(&wc_lock);
}
+#endif /* CONFIG_HAS_SHARED_INFO */
/* Set clock to <secs,usecs> after 00:00:00 UTC, 1 January, 1970. */
void do_settime(u64 secs, unsigned int nsecs, u64 system_time_base)
diff --git a/xen/include/xen/shared.h b/xen/include/xen/shared.h
index 5b71342cab32..f20a46801181 100644
--- a/xen/include/xen/shared.h
+++ b/xen/include/xen/shared.h
@@ -43,7 +43,13 @@ typedef struct vcpu_info vcpu_info_t;
extern vcpu_info_t dummy_vcpu_info;
-#define shared_info(d, field) __shared_info(d, (d)->shared_info, field)
+#ifdef CONFIG_HAS_SHARED_INFO
+#define shared_info(d, field) __shared_info(d, (d)->shared_info, field)
+#else
+extern struct shared_info *shared_info_absent;
+#define shared_info(d, field) (((void)(d), shared_info_absent)->field)
+#endif /* CONFIG_HAS_SHARED_INFO */
+
#define vcpu_info(v, field) \
__vcpu_info(v, (vcpu_info_t *)(v)->vcpu_info_area.map, field)
diff --git a/xen/include/xen/time.h b/xen/include/xen/time.h
index e9c0822e6f31..2f872f580ffc 100644
--- a/xen/include/xen/time.h
+++ b/xen/include/xen/time.h
@@ -66,7 +66,11 @@ struct tm wallclock_time(uint64_t *ns);
#define version_update_begin(v) (((v) + 1) | 1)
#define version_update_end(v) ((v) + 1)
extern void update_vcpu_system_time(struct vcpu *v);
+#ifdef CONFIG_HAS_SHARED_INFO
extern void update_domain_wallclock_time(struct domain *d);
+#else
+static inline void update_domain_wallclock_time(struct domain *d) {}
+#endif
extern void do_settime(
u64 secs, unsigned int nsecs, u64 system_time_base);
--
2.54.0
On 25.06.2026 18:02, Oleksii Kurochko wrote:
> On architectures that run guests in dom0less mode without the PV ABI
> (currently RISC-V), no shared_info page is allocated and d->shared_info
> remains NULL throughout the domain lifetime. Several places in common
> code access d->shared_info through the shared_info() macro or directly,
> causing UBSAN null-pointer errors on such architectures.
>
> Rather than adding runtime NULL guards that are logically unreachable
> on x86 and Arm (where shared_info is always allocated), introduce a new
> Kconfig symbol CONFIG_HAS_SHARED_INFO selected by x86 and Arm.
>
> On !HAS_SHARED_INFO the shared_info() macro expands to a dereference
> of a pointer returned by shared_info_absent(), which is declared but
> intentionally never defined.
This looks to need updating.
> Any use of shared_info() that is not
> dead-code-eliminated will therefore cause a link-time failure, making
> missed guards impossible to overlook.
>
> The 2L event-channel ops call shared_info() and must not be compiled on
> architectures without a shared_info page, so event_2l.o is gated on
> CONFIG_HAS_SHARED_INFO. On such architectures evtchn_init() installs
> the FIFO ops as a placeholder instead; evtchn_fifo_word_from_port() is
> guarded against uninitialised d->evtchn_fifo so the FIFO ops are safe
> before evtchn_fifo_init_control() is called by the guest.
>
> With CONFIG_HAS_SHARED_INFO=n all vCPUs fall back to the global
> dummy_vcpu_info, so writes through vcpu_info() could leak data between
> vCPUs. Reviewing the write paths in common code: the write in
> map_guest_area() stores the constant ~0 so nothing serious would happen
> if it were leaked; the event_2l.c paths are unreachable because the
> preceding shared_info() call would trap first;
Why "trap"? You can't build an image that way, can you?
> @@ -1624,7 +1626,11 @@ void evtchn_check_pollers(struct domain *d, unsigned int port)
>
> int evtchn_init(struct domain *d, unsigned int max_port)
> {
> - evtchn_2l_init(d);
> + if ( IS_ENABLED(CONFIG_HAS_SHARED_INFO) )
> + evtchn_2l_init(d);
For this to build when !HAS_SHARED_INFO, all you need is a declaration of
the function. The compiler will DCE the call. Hence ...
> --- a/xen/common/event_channel.h
> +++ b/xen/common/event_channel.h
> @@ -44,7 +44,11 @@ static inline void evtchn_port_print_state(struct domain *d,
>
> /* 2-level */
>
> +#ifdef CONFIG_HAS_SHARED_INFO
> void evtchn_2l_init(struct domain *d);
> +#else
> +static inline void evtchn_2l_init(struct domain *d) {}
> +#endif
>
> /* FIFO */
>
... this hunk should be unnecessary?
> @@ -55,6 +59,7 @@ struct evtchn_expand_array;
> int evtchn_fifo_init_control(struct evtchn_init_control *init_control);
> int evtchn_fifo_expand_array(const struct evtchn_expand_array *expand_array);
> void evtchn_fifo_destroy(struct domain *d);
> +void evtchn_fifo_init_ops(struct domain *d);
> #else
> static inline int evtchn_fifo_init_control(struct evtchn_init_control *init_control)
> {
> @@ -68,6 +73,7 @@ static inline void evtchn_fifo_destroy(struct domain *d)
> {
> return;
> }
> +static inline void evtchn_fifo_init_ops(struct domain *d) {}
> #endif /* CONFIG_EVTCHN_FIFO */
Unlike these two. Which raise a different question though: What will be the
behavior when EVTCHN_FIFO=n and HAS_SHARED_INFO=n? Taking
evtchn_alloc_unbound() as example, afaict evtchn_port_init() will stumble
over a NULL pointer. Looks like for that (and only that) case we still need
your earlier dummy fallback.
> @@ -420,6 +423,11 @@ static const struct evtchn_port_ops evtchn_port_ops_fifo =
> .print_state = evtchn_fifo_print_state,
> };
>
> +void evtchn_fifo_init_ops(struct domain *d)
> +{
> + d->evtchn_port_ops = &evtchn_port_ops_fifo;
> +}
Isn't this unreachable code when HAS_SHARED_INFO=y, violating Misra rule 2.1?
Jan
On 6/29/26 4:26 PM, Jan Beulich wrote:
> On 25.06.2026 18:02, Oleksii Kurochko wrote:
>> On architectures that run guests in dom0less mode without the PV ABI
>> (currently RISC-V), no shared_info page is allocated and d->shared_info
>> remains NULL throughout the domain lifetime. Several places in common
>> code access d->shared_info through the shared_info() macro or directly,
>> causing UBSAN null-pointer errors on such architectures.
>>
>> Rather than adding runtime NULL guards that are logically unreachable
>> on x86 and Arm (where shared_info is always allocated), introduce a new
>> Kconfig symbol CONFIG_HAS_SHARED_INFO selected by x86 and Arm.
>>
>> On !HAS_SHARED_INFO the shared_info() macro expands to a dereference
>> of a pointer returned by shared_info_absent(), which is declared but
>> intentionally never defined.
>
> This looks to need updating.
I will update it to:
On !HAS_SHARED_INFO the shared_info() macro expands to a dereference of
shared_info_absent, an extern pointer that is declared but intentionally
never defined.
>
>> Any use of shared_info() that is not
>> dead-code-eliminated will therefore cause a link-time failure, making
>> missed guards impossible to overlook.
>>
>> The 2L event-channel ops call shared_info() and must not be compiled on
>> architectures without a shared_info page, so event_2l.o is gated on
>> CONFIG_HAS_SHARED_INFO. On such architectures evtchn_init() installs
>> the FIFO ops as a placeholder instead; evtchn_fifo_word_from_port() is
>> guarded against uninitialised d->evtchn_fifo so the FIFO ops are safe
>> before evtchn_fifo_init_control() is called by the guest.
>>
>> With CONFIG_HAS_SHARED_INFO=n all vCPUs fall back to the global
>> dummy_vcpu_info, so writes through vcpu_info() could leak data between
>> vCPUs. Reviewing the write paths in common code: the write in
>> map_guest_area() stores the constant ~0 so nothing serious would happen
>> if it were leaked; the event_2l.c paths are unreachable because the
>> preceding shared_info() call would trap first;
>
> Why "trap"? You can't build an image that way, can you?
"trap" was shorthand for the link-time failure.
I will changed that part to:
... leaked; the event_2l.c paths are not compiled on !HAS_SHARED_INFO,
as event_2l.o is gated on CONFIG_HAS_SHARED_INFO; ...
>
>> @@ -1624,7 +1626,11 @@ void evtchn_check_pollers(struct domain *d, unsigned int port)
>>
>> int evtchn_init(struct domain *d, unsigned int max_port)
>> {
>> - evtchn_2l_init(d);
>> + if ( IS_ENABLED(CONFIG_HAS_SHARED_INFO) )
>> + evtchn_2l_init(d);
>
> For this to build when !HAS_SHARED_INFO, all you need is a declaration of
> the function. The compiler will DCE the call. Hence ...
>
>> --- a/xen/common/event_channel.h
>> +++ b/xen/common/event_channel.h
>> @@ -44,7 +44,11 @@ static inline void evtchn_port_print_state(struct domain *d,
>>
>> /* 2-level */
>>
>> +#ifdef CONFIG_HAS_SHARED_INFO
>> void evtchn_2l_init(struct domain *d);
>> +#else
>> +static inline void evtchn_2l_init(struct domain *d) {}
>> +#endif
>>
>> /* FIFO */
>>
>
> ... this hunk should be unnecessary?
Looks like you are right, I will double-check that.
>
>> @@ -55,6 +59,7 @@ struct evtchn_expand_array;
>> int evtchn_fifo_init_control(struct evtchn_init_control *init_control);
>> int evtchn_fifo_expand_array(const struct evtchn_expand_array *expand_array);
>> void evtchn_fifo_destroy(struct domain *d);
>> +void evtchn_fifo_init_ops(struct domain *d);
>> #else
>> static inline int evtchn_fifo_init_control(struct evtchn_init_control *init_control)
>> {
>> @@ -68,6 +73,7 @@ static inline void evtchn_fifo_destroy(struct domain *d)
>> {
>> return;
>> }
>> +static inline void evtchn_fifo_init_ops(struct domain *d) {}
>> #endif /* CONFIG_EVTCHN_FIFO */
>
> Unlike these two. Which raise a different question though: What will be the
> behavior when EVTCHN_FIFO=n and HAS_SHARED_INFO=n? Taking
> evtchn_alloc_unbound() as example, afaict evtchn_port_init() will stumble
> over a NULL pointer. Looks like for that (and only that) case we still need
> your earlier dummy fallback.
I will introduce dummy fallback (I will shrunk some stubs in final version):
+#ifndef CONFIG_HAS_SHARED_INFO
+static void cf_check evtchn_none_set_pending(
+ struct vcpu *v, struct evtchn *evtchn) {}
+static void cf_check evtchn_none_clear_pending(
+ struct domain *d, struct evtchn *evtchn) {}
+static void cf_check evtchn_none_unmask(
+ struct domain *d, struct evtchn *evtchn) {}
+static bool cf_check evtchn_none_is_pending(
+ const struct domain *d, const struct evtchn *evtchn) { return false; }
+static bool cf_check evtchn_none_is_masked(
+ const struct domain *d, const struct evtchn *evtchn) { return true; }
+static void cf_check evtchn_none_print_state(
+ struct domain *d, const struct evtchn *evtchn) {}
+
+static const struct evtchn_port_ops evtchn_port_ops_none = {
+ .set_pending = evtchn_none_set_pending,
+ .clear_pending = evtchn_none_clear_pending,
+ .unmask = evtchn_none_unmask,
+ .is_pending = evtchn_none_is_pending,
+ .is_masked = evtchn_none_is_masked,
+ .print_state = evtchn_none_print_state,
+};
+
+static void evtchn_none_init(struct domain *d)
+{
+ d->evtchn_port_ops = &evtchn_port_ops_none;
+}
+#endif
>
>> @@ -420,6 +423,11 @@ static const struct evtchn_port_ops evtchn_port_ops_fifo =
>> .print_state = evtchn_fifo_print_state,
>> };
>>
>> +void evtchn_fifo_init_ops(struct domain *d)
>> +{
>> + d->evtchn_port_ops = &evtchn_port_ops_fifo;
>> +}
>
> Isn't this unreachable code when HAS_SHARED_INFO=y, violating Misra rule 2.1?
I think if we are going to return back to dummy fallback I think that we
could drop evtchn_fifo_init_ops() as d->evtchn_port_ops will be
initialized later for FIFO. And then in evtchn_init:
int evtchn_init(struct domain *d, unsigned int max_port)
{
if ( IS_ENABLED(CONFIG_HAS_SHARED_INFO) )
evtchn_2l_init(d);
else
evtchn_none_init(d);
Thanks.
~ Oleksii
On 6/29/26 4:26 PM, Jan Beulich wrote:
> On 25.06.2026 18:02, Oleksii Kurochko wrote:
>> On architectures that run guests in dom0less mode without the PV ABI
>> (currently RISC-V), no shared_info page is allocated and d->shared_info
>> remains NULL throughout the domain lifetime. Several places in common
>> code access d->shared_info through the shared_info() macro or directly,
>> causing UBSAN null-pointer errors on such architectures.
>>
>> Rather than adding runtime NULL guards that are logically unreachable
>> on x86 and Arm (where shared_info is always allocated), introduce a new
>> Kconfig symbol CONFIG_HAS_SHARED_INFO selected by x86 and Arm.
>>
>> On !HAS_SHARED_INFO the shared_info() macro expands to a dereference
>> of a pointer returned by shared_info_absent(), which is declared but
>> intentionally never defined.
>
> This looks to need updating.
I will update it to:
On !HAS_SHARED_INFO the shared_info() macro expands to a dereference of
shared_info_absent, an extern pointer that is declared but intentionally
never defined.
>
>> Any use of shared_info() that is not
>> dead-code-eliminated will therefore cause a link-time failure, making
>> missed guards impossible to overlook.
>>
>> The 2L event-channel ops call shared_info() and must not be compiled on
>> architectures without a shared_info page, so event_2l.o is gated on
>> CONFIG_HAS_SHARED_INFO. On such architectures evtchn_init() installs
>> the FIFO ops as a placeholder instead; evtchn_fifo_word_from_port() is
>> guarded against uninitialised d->evtchn_fifo so the FIFO ops are safe
>> before evtchn_fifo_init_control() is called by the guest.
>>
>> With CONFIG_HAS_SHARED_INFO=n all vCPUs fall back to the global
>> dummy_vcpu_info, so writes through vcpu_info() could leak data between
>> vCPUs. Reviewing the write paths in common code: the write in
>> map_guest_area() stores the constant ~0 so nothing serious would happen
>> if it were leaked; the event_2l.c paths are unreachable because the
>> preceding shared_info() call would trap first;
>
> Why "trap"? You can't build an image that way, can you?
"trap" was shorthand for the link-time failure.
I will changed that part to:
... leaked; the event_2l.c paths are not compiled on !HAS_SHARED_INFO,
as event_2l.o is gated on CONFIG_HAS_SHARED_INFO; ...
>
>> @@ -1624,7 +1626,11 @@ void evtchn_check_pollers(struct domain *d, unsigned int port)
>>
>> int evtchn_init(struct domain *d, unsigned int max_port)
>> {
>> - evtchn_2l_init(d);
>> + if ( IS_ENABLED(CONFIG_HAS_SHARED_INFO) )
>> + evtchn_2l_init(d);
>
> For this to build when !HAS_SHARED_INFO, all you need is a declaration of
> the function. The compiler will DCE the call. Hence ...
>
>> --- a/xen/common/event_channel.h
>> +++ b/xen/common/event_channel.h
>> @@ -44,7 +44,11 @@ static inline void evtchn_port_print_state(struct domain *d,
>>
>> /* 2-level */
>>
>> +#ifdef CONFIG_HAS_SHARED_INFO
>> void evtchn_2l_init(struct domain *d);
>> +#else
>> +static inline void evtchn_2l_init(struct domain *d) {}
>> +#endif
>>
>> /* FIFO */
>>
>
> ... this hunk should be unnecessary?
Looks like you are right, I will double-check that.
>
>> @@ -55,6 +59,7 @@ struct evtchn_expand_array;
>> int evtchn_fifo_init_control(struct evtchn_init_control *init_control);
>> int evtchn_fifo_expand_array(const struct evtchn_expand_array *expand_array);
>> void evtchn_fifo_destroy(struct domain *d);
>> +void evtchn_fifo_init_ops(struct domain *d);
>> #else
>> static inline int evtchn_fifo_init_control(struct evtchn_init_control *init_control)
>> {
>> @@ -68,6 +73,7 @@ static inline void evtchn_fifo_destroy(struct domain *d)
>> {
>> return;
>> }
>> +static inline void evtchn_fifo_init_ops(struct domain *d) {}
>> #endif /* CONFIG_EVTCHN_FIFO */
>
> Unlike these two. Which raise a different question though: What will be the
> behavior when EVTCHN_FIFO=n and HAS_SHARED_INFO=n? Taking
> evtchn_alloc_unbound() as example, afaict evtchn_port_init() will stumble
> over a NULL pointer. Looks like for that (and only that) case we still need
> your earlier dummy fallback.
I will introduce dummy fallback (I will shrunk some stubs in final version):
+#ifndef CONFIG_HAS_SHARED_INFO
+static void cf_check evtchn_none_set_pending(
+ struct vcpu *v, struct evtchn *evtchn) {}
+static void cf_check evtchn_none_clear_pending(
+ struct domain *d, struct evtchn *evtchn) {}
+static void cf_check evtchn_none_unmask(
+ struct domain *d, struct evtchn *evtchn) {}
+static bool cf_check evtchn_none_is_pending(
+ const struct domain *d, const struct evtchn *evtchn) { return false; }
+static bool cf_check evtchn_none_is_masked(
+ const struct domain *d, const struct evtchn *evtchn) { return true; }
+static void cf_check evtchn_none_print_state(
+ struct domain *d, const struct evtchn *evtchn) {}
+
+static const struct evtchn_port_ops evtchn_port_ops_none = {
+ .set_pending = evtchn_none_set_pending,
+ .clear_pending = evtchn_none_clear_pending,
+ .unmask = evtchn_none_unmask,
+ .is_pending = evtchn_none_is_pending,
+ .is_masked = evtchn_none_is_masked,
+ .print_state = evtchn_none_print_state,
+};
+
+static void evtchn_none_init(struct domain *d)
+{
+ d->evtchn_port_ops = &evtchn_port_ops_none;
+}
+#endif
>
>> @@ -420,6 +423,11 @@ static const struct evtchn_port_ops evtchn_port_ops_fifo =
>> .print_state = evtchn_fifo_print_state,
>> };
>>
>> +void evtchn_fifo_init_ops(struct domain *d)
>> +{
>> + d->evtchn_port_ops = &evtchn_port_ops_fifo;
>> +}
>
> Isn't this unreachable code when HAS_SHARED_INFO=y, violating Misra rule 2.1?
I think if we are going to return back to dummy fallback I think that we
could drop evtchn_fifo_init_ops() as d->evtchn_port_ops will be
initialized later for FIFO. And then in evtchn_init:
int evtchn_init(struct domain *d, unsigned int max_port)
{
if ( IS_ENABLED(CONFIG_HAS_SHARED_INFO) )
evtchn_2l_init(d);
else
evtchn_none_init(d);
(and I expect evtchn_none_init() will be just dropped by compiler if
CONFIG_HAS_SHARED_INFO=y so nothing extra should be done)
Thanks.
~ Oleksii
On 6/30/26 5:02 PM, Oleksii Kurochko wrote:
>
>>
>>> @@ -55,6 +59,7 @@ struct evtchn_expand_array;
>>> int evtchn_fifo_init_control(struct evtchn_init_control
>>> *init_control);
>>> int evtchn_fifo_expand_array(const struct evtchn_expand_array
>>> *expand_array);
>>> void evtchn_fifo_destroy(struct domain *d);
>>> +void evtchn_fifo_init_ops(struct domain *d);
>>> #else
>>> static inline int evtchn_fifo_init_control(struct
>>> evtchn_init_control *init_control)
>>> {
>>> @@ -68,6 +73,7 @@ static inline void evtchn_fifo_destroy(struct
>>> domain *d)
>>> {
>>> return;
>>> }
>>> +static inline void evtchn_fifo_init_ops(struct domain *d) {}
>>> #endif /* CONFIG_EVTCHN_FIFO */
>>
>> Unlike these two. Which raise a different question though: What will
>> be the
>> behavior when EVTCHN_FIFO=n and HAS_SHARED_INFO=n? Taking
>> evtchn_alloc_unbound() as example, afaict evtchn_port_init() will stumble
>> over a NULL pointer. Looks like for that (and only that) case we still
>> need
>> your earlier dummy fallback.
>
> I will introduce dummy fallback (I will shrunk some stubs in final
> version):
>
> +#ifndef CONFIG_HAS_SHARED_INFO
> +static void cf_check evtchn_none_set_pending(
> + struct vcpu *v, struct evtchn *evtchn) {}
> +static void cf_check evtchn_none_clear_pending(
> + struct domain *d, struct evtchn *evtchn) {}
> +static void cf_check evtchn_none_unmask(
> + struct domain *d, struct evtchn *evtchn) {}
> +static bool cf_check evtchn_none_is_pending(
> + const struct domain *d, const struct evtchn *evtchn) { return false; }
> +static bool cf_check evtchn_none_is_masked(
> + const struct domain *d, const struct evtchn *evtchn) { return true; }
> +static void cf_check evtchn_none_print_state(
> + struct domain *d, const struct evtchn *evtchn) {}
> +
> +static const struct evtchn_port_ops evtchn_port_ops_none = {
> + .set_pending = evtchn_none_set_pending,
> + .clear_pending = evtchn_none_clear_pending,
> + .unmask = evtchn_none_unmask,
> + .is_pending = evtchn_none_is_pending,
> + .is_masked = evtchn_none_is_masked,
> + .print_state = evtchn_none_print_state,
> +};
> +
> +static void evtchn_none_init(struct domain *d)
> +{
> + d->evtchn_port_ops = &evtchn_port_ops_none;
> +}
> +#endif
I will shrink it to::
#ifndef CONFIG_HAS_SHARED_INFO
/*
* Placeholder ops for domains with neither a shared_info page nor (yet)
* a FIFO control block. None of these are ever reachable in practice;
* they only exist to keep d->evtchn_port_ops non-NULL.
*/
static void cf_check evtchn_none_set_pending(
struct vcpu *v, struct evtchn *evtchn) {}
static void cf_check evtchn_none_noop(
struct domain *d, struct evtchn *evtchn) {}
static bool cf_check evtchn_none_false(
const struct domain *d, const struct evtchn *evtchn) { return false; }
static void cf_check evtchn_none_print_state(
struct domain *d, const struct evtchn *evtchn) {}
static const struct evtchn_port_ops evtchn_port_ops_none = {
.set_pending = evtchn_none_set_pending,
.clear_pending = evtchn_none_noop,
.unmask = evtchn_none_noop,
.is_pending = evtchn_none_false,
.is_masked = evtchn_none_false,
.print_state = evtchn_none_print_state,
};
void evtchn_none_init(struct domain *d)
{
d->evtchn_port_ops = &evtchn_port_ops_none;
}
#endif /* CONFIG_HAS_SHARED_INFO */
~ Oleksii
© 2016 - 2026 Red Hat, Inc.