@@ -167,7 +167,7 @@ static const char *balance_level_str(int level)
167167 * - BALANCE_CORE: No fallback possible (single CPU, fail immediately)
168168 * - BALANCE_CACHE: Only search within same cache domain
169169 * - BALANCE_PACKAGE: Only search within same package
170- * - BALANCE_NONE: Search within same NUMA node (widest scope)
170+ * - BALANCE_NONE: User opted out of balancing - decline fallback
171171 *
172172 * FIXES APPLIED:
173173 * 1. NUMA fallback now traverses topology tree to find actual CPUs
@@ -205,6 +205,22 @@ static int try_fallback_cpu(struct irq_info *info, cpumask_t applied_mask,
205205 return -1 ;
206206 }
207207
208+ /*
209+ * BALANCE_NONE means the user opted this IRQ out of balancing.
210+ * place_irq_in_node() only relocates such an IRQ when banned CPUs
211+ * forced it; we should not silently widen the scope further on
212+ * ENOSPC. Treat it like BALANCE_CORE: warn and decline.
213+ * This also avoids a NULL search_scope on non-NUMA systems where
214+ * no OBJ_TYPE_NODE ancestor exists in the topology.
215+ */
216+ if (balance_level == BALANCE_NONE ) {
217+ log (TO_ALL , LOG_WARNING ,
218+ "IRQ %d: cannot fallback - balance level is 'none' "
219+ "(user policy forbids relocation)\n" ,
220+ info -> irq );
221+ return -1 ;
222+ }
223+
208224 cpus_clear (tried_cpus );
209225
210226 /*
@@ -230,11 +246,6 @@ static int try_fallback_cpu(struct irq_info *info, cpumask_t applied_mask,
230246 while (search_scope && search_scope -> obj_type != OBJ_TYPE_PACKAGE )
231247 search_scope = search_scope -> parent ;
232248 break ;
233- case BALANCE_NONE :
234- search_scope = original -> parent ;
235- while (search_scope && search_scope -> obj_type != OBJ_TYPE_NODE )
236- search_scope = search_scope -> parent ;
237- break ;
238249 default :
239250 search_scope = NULL ;
240251 }
@@ -359,6 +370,23 @@ static int try_fallback_cpu(struct irq_info *info, cpumask_t applied_mask,
359370 *
360371 */
361372 migrate_irq_obj (original , fallback , info );
373+ /*
374+ * migrate_irq_obj() unconditionally increments the source
375+ * object's slots_left (0 -> 1), which would make the original
376+ * CPU look eligible again. The kernel returned ENOSPC, so its
377+ * vector table is still full; re-clamp to SATURATED so we
378+ * don't immediately retry the same dead-end placement.
379+ *
380+ * Only re-clamp when the source is an actual CPU. For
381+ * domain-assigned IRQs (cache/package/NUMA), the original
382+ * is a domain object whose slots_left aggregates its CPUs
383+ * and was never marked SLOTS_SATURATED by the ENOSPC handler
384+ * (see activate_mapping(): the obj_type==CPU branch).
385+ * Force-saturating the whole domain would penalize every CPU
386+ * inside it on the next placement cycle.
387+ */
388+ if (original -> obj_type == OBJ_TYPE_CPU )
389+ original -> slots_left = SLOTS_SATURATED ;
362390 info -> moved = 0 ;
363391 log (TO_ALL , LOG_DEBUG ,
364392 "IRQ %d: successfully placed on fallback CPU %d "
0 commit comments