Skip to content

Commit 27590e8

Browse files
authored
xapi_vm: Implement RBAC checking for keys in VM.other_config and VM.platform (#7039)
Add per-key RBAC checking for `VM.platform` and `VM.other_config`, to cover a case where a lower-prileged user could circumvent permission checks on `other_config:{pci,hvm_serial}` and `platform:hvm_serial`. Introduces a generic per-key RBAC checker for map setters based on Task's manual RBAC checker (introduced in a3f2c6e). Uses it for the fields above. This is part of XSA-489 / CVE-2026-23562 and CVE-2026-42486
2 parents 9a34ddc + 47d8a6f commit 27590e8

7 files changed

Lines changed: 369 additions & 185 deletions

File tree

ocaml/idl/datamodel_vm.ml

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2384,6 +2384,105 @@ let sysprep =
23842384
as part of a reboot."
23852385
~allowed_roles:_R_VM_ADMIN ()
23862386

2387+
module Other_config = struct
2388+
let protected_keys =
2389+
[
2390+
("hvm_serial", _R_POOL_ADMIN)
2391+
; ("pci", _R_POOL_ADMIN)
2392+
; ("folder", _R_VM_OP)
2393+
; ("XenCenter.CustomFields.*", _R_VM_OP)
2394+
]
2395+
2396+
let call =
2397+
call
2398+
~lifecycle:[(Published, rel_rio, "additional configuration")]
2399+
~allowed_roles:_R_VM_ADMIN
2400+
2401+
let add_to_other_config =
2402+
call ~name:"add_to_other_config"
2403+
~doc:
2404+
"Add the given key-value pair to the other_config field of the given \
2405+
VM."
2406+
~params:
2407+
[
2408+
(Ref _vm, "self", "reference to object")
2409+
; (String, "key", "Key to add")
2410+
; (String, "value", "Value to add")
2411+
]
2412+
~map_keys_roles:protected_keys ~flags:[`Session] ()
2413+
2414+
let remove_from_other_config =
2415+
call ~name:"remove_from_other_config"
2416+
~doc:
2417+
"Remove the given key and its corresponding value from the \
2418+
other_config field of the given VM. If the key is not in that Map, \
2419+
then do nothing."
2420+
~params:
2421+
[
2422+
(Ref _vm, "self", "reference to object")
2423+
; (String, "key", "Key of entry to remove")
2424+
]
2425+
~map_keys_roles:protected_keys ~flags:[`Session] ()
2426+
2427+
(* map_keys_roles can't be cited here, since they're only implemented for
2428+
{add_to,remove_from}_other_config, RBAC handling is done in a manual
2429+
implementation. *)
2430+
let set_other_config =
2431+
call ~name:"set_other_config"
2432+
~doc:"Set the other_config field of the given VM."
2433+
~params:
2434+
[
2435+
(Ref _vm, "self", "reference to object")
2436+
; (Map (String, String), "value", "New value to set")
2437+
]
2438+
~flags:[`Session] ()
2439+
end
2440+
2441+
module Platform = struct
2442+
let protected_keys = [("hvm_serial", _R_POOL_ADMIN)]
2443+
2444+
let call =
2445+
call
2446+
~lifecycle:[(Published, rel_rio, "platform-specific configuration")]
2447+
~allowed_roles:_R_VM_ADMIN
2448+
2449+
let add_to_platform =
2450+
call ~name:"add_to_platform"
2451+
~doc:"Add the given key-value pair to the platform field of the given VM."
2452+
~params:
2453+
[
2454+
(Ref _vm, "self", "reference to object")
2455+
; (String, "key", "Key to add")
2456+
; (String, "value", "Value to add")
2457+
]
2458+
~map_keys_roles:protected_keys ~flags:[`Session] ()
2459+
2460+
let remove_from_platform =
2461+
call ~name:"remove_from_platform"
2462+
~doc:
2463+
"Remove the given key and its corresponding value from the platform \
2464+
field of the given VM. If the key is not in that Map, then do \
2465+
nothing."
2466+
~params:
2467+
[
2468+
(Ref _vm, "self", "reference to object")
2469+
; (String, "key", "Key of entry to remove")
2470+
]
2471+
~map_keys_roles:protected_keys ~flags:[`Session] ()
2472+
2473+
(* map_keys_roles can't be cited here, since they're only implemented for
2474+
{add_to,remove_from}_platform, RBAC handling is done in a manual
2475+
implementation. *)
2476+
let set_platform =
2477+
call ~name:"set_platform" ~doc:"Set the platform field of the given VM."
2478+
~params:
2479+
[
2480+
(Ref _vm, "self", "reference to object")
2481+
; (Map (String, String), "value", "New value to set")
2482+
]
2483+
~flags:[`Session] ()
2484+
end
2485+
23872486
let vm_uefi_mode =
23882487
Enum
23892488
( "vm_uefi_mode"
@@ -2587,6 +2686,12 @@ let t =
25872686
; add_to_blocked_operations
25882687
; remove_from_blocked_operations
25892688
; sysprep
2689+
; Other_config.add_to_other_config
2690+
; Other_config.remove_from_other_config
2691+
; Other_config.set_other_config
2692+
; Platform.add_to_platform
2693+
; Platform.remove_from_platform
2694+
; Platform.set_platform
25902695
]
25912696
~contents:
25922697
([
@@ -2715,9 +2820,10 @@ let t =
27152820
~qualifier:DynamicRO ~ty:(Set (Ref _vtpm)) "VTPMs" "virtual TPMs"
27162821
; namespace ~name:"PV" ~contents:pv ()
27172822
; namespace ~name:"HVM" ~contents:hvm ()
2718-
; field
2823+
; field ~qualifier:StaticRO
27192824
~ty:(Map (String, String))
27202825
~lifecycle:[(Published, rel_rio, "platform-specific configuration")]
2826+
~map_keys_roles:[("hvm_serial", _R_POOL_ADMIN)]
27212827
"platform" "platform-specific configuration"
27222828
; field
27232829
~lifecycle:
@@ -2726,13 +2832,14 @@ let t =
27262832
; (Deprecated, rel_boston, "Field was never used")
27272833
]
27282834
"PCI_bus" "PCI bus path for pass-through devices"
2729-
; field
2835+
; field ~qualifier:StaticRO
27302836
~lifecycle:[(Published, rel_rio, "additional configuration")]
27312837
~ty:(Map (String, String))
27322838
"other_config" "additional configuration"
27332839
~map_keys_roles:
27342840
[
27352841
("pci", _R_POOL_ADMIN)
2842+
; ("hvm_serial", _R_POOL_ADMIN)
27362843
; ("folder", _R_VM_OP)
27372844
; ("XenCenter.CustomFields.*", _R_VM_OP)
27382845
]

ocaml/idl/schematest.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ let hash x = Digest.string x |> Digest.to_hex
33
(* BEWARE: if this changes, check that schema has been bumped accordingly in
44
ocaml/idl/datamodel_common.ml, usually schema_minor_vsn *)
55

6-
let last_known_schema_hash = "87b39cf2131c990f186bb6baa6e5ece8"
6+
let last_known_schema_hash = "74ab53bec7861bcc9743cd8af10c0929"
77

88
let current_schema_hash : string =
99
let open Datamodel_types in

ocaml/xapi/helpers.ml

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ module TraceHelper = struct
428428
end
429429

430430
(** Once the server functor has been instantiated, xapi sets this reference to the appropriate
431-
"fake_rpc" (loopback non-HTTP) rpc function.
431+
"fake_rpc" (loopback non-HTTP) rpc function.
432432
This way, internally the coordinator can short-circuit API calls without having to go over the network. *)
433433
let rpc_fun : (Http.Request.t -> Rpc.call -> Rpc.response) option ref = ref None
434434

@@ -2477,3 +2477,185 @@ module AuthenticationCache = struct
24772477
None
24782478
end
24792479
end
2480+
2481+
(* Simple trie data structure that performs a favoured lookup to
2482+
implement a simple form of wildcard key matching. The trie is not
2483+
pruned during (or after) construction. *)
2484+
module MatchTrie = struct
2485+
type 'a node = {arrows: (string, 'a node) Hashtbl.t; mutable value: 'a option}
2486+
2487+
let create_node () =
2488+
let arrows = Hashtbl.create 16 in
2489+
let value = None in
2490+
{arrows; value}
2491+
2492+
let create = create_node
2493+
2494+
let insert root ~key ~value =
2495+
let parts = String.split_on_char '.' key in
2496+
let rec extend focused = function
2497+
| part :: parts ->
2498+
let next =
2499+
match Hashtbl.find_opt focused.arrows part with
2500+
| Some node ->
2501+
node
2502+
| _ ->
2503+
let next = create_node () in
2504+
Hashtbl.replace focused.arrows part next ;
2505+
next
2506+
in
2507+
extend next parts
2508+
| [] ->
2509+
focused
2510+
in
2511+
let final = extend root parts in
2512+
final.value <- Some value
2513+
2514+
let find root ~key =
2515+
let parts = String.split_on_char '.' key in
2516+
let rec find focused = function
2517+
| part :: parts -> (
2518+
(* Wildcard edges override other edges. *)
2519+
match Hashtbl.find_opt focused.arrows "*" with
2520+
| Some _ as sink ->
2521+
sink
2522+
| _ -> (
2523+
match Hashtbl.find_opt focused.arrows part with
2524+
| Some next ->
2525+
(find [@tailcall]) next parts
2526+
| _ ->
2527+
None
2528+
)
2529+
)
2530+
| _ ->
2531+
Some focused
2532+
in
2533+
match find root parts with Some node -> node.value | _ -> None
2534+
end
2535+
2536+
(* Given an input key, compare against the protected keys of the
2537+
task.other_config field. If a protected key matches, return it.
2538+
2539+
For example, if the datamodel specifies "foo.bar.*" as a protected
2540+
key, then: match_protected_key ~key:"foo.bar.baz" = Some "foo.bar.*".
2541+
2542+
It must return the protected key as that is what key-related RBAC
2543+
entries are defined in terms of.
2544+
*)
2545+
let match_protected_key objname fieldname =
2546+
(* Attain the listing of protected keys from the datamodel at module
2547+
initialisation. Usually, this list is passed to Rbac.check by
2548+
handlers inside the auto-generated server.ml file. *)
2549+
let protected_keys =
2550+
let api = Datamodel.all_api in
2551+
let field = Dm_api.get_field_by_name api ~objname ~fieldname in
2552+
List.map fst field.field_map_keys_roles
2553+
in
2554+
(* Define the lookup function in terms of a simple trie data
2555+
structure - which is flexible to account for overlapping paths and
2556+
presence of wildcards. *)
2557+
let trie =
2558+
let root = MatchTrie.create () in
2559+
let add key = MatchTrie.insert root ~key ~value:key in
2560+
List.iter add protected_keys ;
2561+
root
2562+
in
2563+
MatchTrie.find trie
2564+
2565+
(* The behaviour of this function, with respect to RBAC checking, must
2566+
match serial "remove_from" and "add_to" operations (for only the keys
2567+
that are changing).
2568+
2569+
There is normally no key-related RBAC checking for
2570+
"set_X" (e.g. set_other_config) because the required writer role for the
2571+
entire field is usually higher than the role(s) required for
2572+
individually-protected keys.
2573+
2574+
{Task,VM}.set_other_config and VM.set_platform are special cases where
2575+
lower-privileged sessions must be able to manipulate a subset of entries
2576+
(those not protected by a more privileged role).
2577+
*)
2578+
let set_map_with_rbac ~__context ~self ~value ~get_fn ~set_fn ~match_protected
2579+
~object_name ~field_name =
2580+
let match_protected = match_protected field_name in
2581+
let module S = Set.Make (String) in
2582+
let create_lookup kvs =
2583+
let table = List.to_seq kvs |> Hashtbl.of_seq in
2584+
Hashtbl.find_opt table
2585+
in
2586+
let old_value = get_fn ~__context ~self in
2587+
let lookup_old, lookup_new = (create_lookup old_value, create_lookup value) in
2588+
let keys_before, keys_after =
2589+
let keys = List.map fst in
2590+
let before = keys old_value in
2591+
let after = keys value in
2592+
S.(of_list before, of_list after)
2593+
in
2594+
let keys_removed =
2595+
(* Keys no longer appearing in the map. The user must have the
2596+
"remove_from" role for each of the protected keys in the set. *)
2597+
S.diff keys_before keys_after
2598+
in
2599+
let keys_unchanged =
2600+
(* Keys that persist across the update. If any key in this set is
2601+
protected AND the value mapped to by the key would be changed by
2602+
the update, the session must have the "add_to" role. *)
2603+
let updated = S.inter keys_before keys_after in
2604+
let is_entry_unchanged key =
2605+
let is_same =
2606+
let ( let* ) = Option.bind in
2607+
let* old_value = lookup_old key in
2608+
let* new_value = lookup_new key in
2609+
Some (old_value = new_value)
2610+
in
2611+
Option.value ~default:false is_same
2612+
in
2613+
(* Filter out the unchanged entries, as you don't need any
2614+
extra privileges to maintain an entry that's already there. *)
2615+
S.filter is_entry_unchanged updated
2616+
in
2617+
let keys_added =
2618+
(* Treat all keys as new, unless they're referring to entries that
2619+
are unchanged across the update. *)
2620+
S.diff keys_after keys_unchanged
2621+
in
2622+
let permissions =
2623+
(* Map each of the added and removed keys to protected keys, if
2624+
such a key exists. *)
2625+
let filter keys =
2626+
S.filter_map (fun key -> match_protected ~key) keys |> S.elements
2627+
in
2628+
let added = filter keys_added in
2629+
let removed = filter keys_removed in
2630+
let format operation key =
2631+
(* All the permissions are stored in lowercase. *)
2632+
let key = String.lowercase_ascii key in
2633+
Printf.sprintf "%s.%s_%s/key:%s" object_name operation field_name key
2634+
in
2635+
(* The required permissions are defined in terms of those
2636+
generated for "add_to" and "remove_from" (both implemented
2637+
above). They can be defined as custom AND use RBAC checking within
2638+
server.ml because their operation is purely destructive, so it's
2639+
sufficient to guard the entire action with Rbac.check. *)
2640+
let added_perms = List.map (format "add_to") added in
2641+
let removed_perms = List.map (format "remove_from") removed in
2642+
added_perms @ removed_perms
2643+
in
2644+
(* Find the first disallowed permission, indicating that we cannot
2645+
perform the action. *)
2646+
let session_id = Context.get_session_id __context in
2647+
match
2648+
Rbac.find_first_disallowed_permission ~__context ~session_id ~permissions
2649+
with
2650+
| None ->
2651+
(* No disallowed permission, perform the update. *)
2652+
set_fn ~__context ~self ~value
2653+
| Some disallowed ->
2654+
(* Report it as an RBAC error. *)
2655+
let action = Printf.sprintf "%s.set_%s" object_name field_name in
2656+
let extra_msg = "" in
2657+
let extra_dmsg = "" in
2658+
raise
2659+
(Rbac.disallowed_permission_exn ~extra_dmsg ~extra_msg ~__context
2660+
~permission:disallowed ~action
2661+
)

ocaml/xapi/message_forwarding.ml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3180,6 +3180,34 @@ functor
31803180
~policy (fun () ->
31813181
forward_vm_op ~local_fn ~__context ~vm:self ~remote_fn
31823182
)
3183+
3184+
let add_to_other_config ~__context ~self ~key ~value =
3185+
info "VM.add_to_other_config: self = '%s', key = '%s'"
3186+
(vm_uuid ~__context self) key ;
3187+
Local.VM.add_to_other_config ~__context ~self ~key ~value
3188+
3189+
let remove_from_other_config ~__context ~self ~key =
3190+
info "VM.remove_from_other_config: self = '%s', key = '%s'"
3191+
(vm_uuid ~__context self) key ;
3192+
Local.VM.remove_from_other_config ~__context ~self ~key
3193+
3194+
let set_other_config ~__context ~self ~value =
3195+
info "VM.set_other_config: self = '%s'" (vm_uuid ~__context self) ;
3196+
Local.VM.set_other_config ~__context ~self ~value
3197+
3198+
let add_to_platform ~__context ~self ~key ~value =
3199+
info "VM.add_to_platform: self = '%s', key = '%s'"
3200+
(vm_uuid ~__context self) key ;
3201+
Local.VM.add_to_platform ~__context ~self ~key ~value
3202+
3203+
let remove_from_platform ~__context ~self ~key =
3204+
info "VM.remove_from_platform: self = '%s', key = '%s'"
3205+
(vm_uuid ~__context self) key ;
3206+
Local.VM.remove_from_platform ~__context ~self ~key
3207+
3208+
let set_platform ~__context ~self ~value =
3209+
info "VM.set_platform: self = '%s'" (vm_uuid ~__context self) ;
3210+
Local.VM.set_platform ~__context ~self ~value
31833211
end
31843212

31853213
module VM_metrics = struct end

0 commit comments

Comments
 (0)