Skip to content

Commit 9505dc2

Browse files
committed
xapi_vm: Implement RBAC checking for keys in set_other_config
map_keys_roles parameter was RBAC checked for {add_to,remove_from}_other_config, but set_other_config allowed circumventing this check. Since VM is the only object that has a key ("pci") in other_config with the privilege level required for modification higher than that of the other_config field generally, this meant that vm-admin could not modify the "pci" key in other_config through add_to_other_config, but could circumvent the check with set_other_config. Implement a checker for VM.other_config setters based on Task's manual RBAC checker (introduced in a3f2c6e) The only difference in the schematest comes from changing the type of the other_config field from RW to StaticRO, which is necessary to provide custom implementations of setters. With a modified schematest, the diff is: < "qualifier": "RW", --- > "qualifier": "StaticRO", This is part of XSA-489 / CVE-2026-YYYYYY Signed-off-by: Andrii Sultanov <andriy.sultanov@vates.tech>
1 parent a2b1b5c commit 9505dc2

7 files changed

Lines changed: 281 additions & 183 deletions

File tree

ocaml/idl/datamodel_vm.ml

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2384,6 +2384,59 @@ 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+
("pci", _R_POOL_ADMIN)
2391+
; ("folder", _R_VM_OP)
2392+
; ("XenCenter.CustomFields.*", _R_VM_OP)
2393+
]
2394+
2395+
let call =
2396+
call
2397+
~lifecycle:[(Published, rel_rio, "additional configuration")]
2398+
~allowed_roles:_R_VM_ADMIN
2399+
2400+
let add_to_other_config =
2401+
call ~name:"add_to_other_config"
2402+
~doc:
2403+
"Add the given key-value pair to the other_config field of the given \
2404+
VM."
2405+
~params:
2406+
[
2407+
(Ref _vm, "self", "reference to object")
2408+
; (String, "key", "Key to add")
2409+
; (String, "value", "Value to add")
2410+
]
2411+
~map_keys_roles:protected_keys ~flags:[`Session] ()
2412+
2413+
let remove_from_other_config =
2414+
call ~name:"remove_from_other_config"
2415+
~doc:
2416+
"Remove the given key and its corresponding value from the \
2417+
other_config field of the given VM. If the key is not in that Map, \
2418+
then do nothing."
2419+
~params:
2420+
[
2421+
(Ref _vm, "self", "reference to object")
2422+
; (String, "key", "Key of entry to remove")
2423+
]
2424+
~map_keys_roles:protected_keys ~flags:[`Session] ()
2425+
2426+
(* map_keys_roles can't be cited here, since they're only implemented for
2427+
{add_to,remove_from}_other_config, RBAC handling is done in a manual
2428+
implementation. *)
2429+
let set_other_config =
2430+
call ~name:"set_other_config"
2431+
~doc:"Set the other_config field of the given VM."
2432+
~params:
2433+
[
2434+
(Ref _vm, "self", "reference to object")
2435+
; (Map (String, String), "value", "New value to set")
2436+
]
2437+
~flags:[`Session] ()
2438+
end
2439+
23872440
let vm_uefi_mode =
23882441
Enum
23892442
( "vm_uefi_mode"
@@ -2587,6 +2640,9 @@ let t =
25872640
; add_to_blocked_operations
25882641
; remove_from_blocked_operations
25892642
; sysprep
2643+
; Other_config.add_to_other_config
2644+
; Other_config.remove_from_other_config
2645+
; Other_config.set_other_config
25902646
]
25912647
~contents:
25922648
([
@@ -2726,7 +2782,7 @@ let t =
27262782
; (Deprecated, rel_boston, "Field was never used")
27272783
]
27282784
"PCI_bus" "PCI bus path for pass-through devices"
2729-
; field
2785+
; field ~qualifier:StaticRO
27302786
~lifecycle:[(Published, rel_rio, "additional configuration")]
27312787
~ty:(Map (String, String))
27322788
"other_config" "additional configuration"

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 = "3e1599a74a6116a9c847f523a8ac4a9f"
77

88
let current_schema_hash : string =
99
let open Datamodel_types in

ocaml/xapi/helpers.ml

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2477,3 +2477,187 @@ 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 =
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 =
2552+
Dm_api.get_field_by_name api ~objname ~fieldname:"other_config"
2553+
in
2554+
List.map fst field.field_map_keys_roles
2555+
in
2556+
(* Define the lookup function in terms of a simple trie data
2557+
structure - which is flexible to account for overlapping paths and
2558+
presence of wildcards. *)
2559+
let trie =
2560+
let root = MatchTrie.create () in
2561+
let add key = MatchTrie.insert root ~key ~value:key in
2562+
List.iter add protected_keys ;
2563+
root
2564+
in
2565+
MatchTrie.find trie
2566+
2567+
(* The behaviour of this function, with respect to RBAC checking, must
2568+
match serial "remove_from" and "add_to" operations (for only the keys
2569+
that are changing).
2570+
2571+
There is normally no key-related RBAC checking for
2572+
"set_other_config" because the required writer role for the entire
2573+
field is usually higher than the role(s) required for
2574+
individually-protected keys.
2575+
2576+
Task and VM's "set_other_config" are special cases where lower-privileged
2577+
sessions must be able to manipulate a subset of entries (those not
2578+
protected by a more privileged role).
2579+
*)
2580+
let set_other_config ~__context ~self ~value ~get_other_config ~set_other_config
2581+
~object_name =
2582+
let module S = Set.Make (String) in
2583+
let create_lookup kvs =
2584+
let table = List.to_seq kvs |> Hashtbl.of_seq in
2585+
Hashtbl.find_opt table
2586+
in
2587+
let old_value = get_other_config ~__context ~self in
2588+
let lookup_old, lookup_new = (create_lookup old_value, create_lookup value) in
2589+
let keys_before, keys_after =
2590+
let keys = List.map fst in
2591+
let before = keys old_value in
2592+
let after = keys value in
2593+
S.(of_list before, of_list after)
2594+
in
2595+
let keys_removed =
2596+
(* Keys no longer appearing in the map. The user must have the
2597+
"remove_from" role for each of the protected keys in the set. *)
2598+
S.diff keys_before keys_after
2599+
in
2600+
let keys_unchanged =
2601+
(* Keys that persist across the update. If any key in this set is
2602+
protected AND the value mapped to by the key would be changed by
2603+
the update, the session must have the "add_to" role. *)
2604+
let updated = S.inter keys_before keys_after in
2605+
let is_entry_unchanged key =
2606+
let is_same =
2607+
let ( let* ) = Option.bind in
2608+
let* old_value = lookup_old key in
2609+
let* new_value = lookup_new key in
2610+
Some (old_value = new_value)
2611+
in
2612+
Option.value ~default:false is_same
2613+
in
2614+
(* Filter out the unchanged entries, as you don't need any
2615+
extra privileges to maintain an entry that's already there. *)
2616+
S.filter is_entry_unchanged updated
2617+
in
2618+
let keys_added =
2619+
(* Treat all keys as new, unless they're referring to entries that
2620+
are unchanged across the update. *)
2621+
S.diff keys_after keys_unchanged
2622+
in
2623+
let permissions =
2624+
(* Map each of the added and removed keys to protected keys, if
2625+
such a key exists. *)
2626+
let filter keys =
2627+
S.filter_map (fun key -> match_protected_key object_name ~key) keys
2628+
|> S.elements
2629+
in
2630+
let added = filter keys_added in
2631+
let removed = filter keys_removed in
2632+
let format operation key =
2633+
(* All the permissions are stored in lowercase. *)
2634+
let key = String.lowercase_ascii key in
2635+
Printf.sprintf "%s.%s_other_config/key:%s" object_name operation key
2636+
in
2637+
(* The required permissions are defined in terms of those
2638+
generated for "add_to" and "remove_from" (both implemented
2639+
above). They can be defined as custom AND use RBAC checking within
2640+
server.ml because their operation is purely destructive, so it's
2641+
sufficient to guard the entire action with Rbac.check. *)
2642+
let added_perms = List.map (format "add_to") added in
2643+
let removed_perms = List.map (format "remove_from") removed in
2644+
added_perms @ removed_perms
2645+
in
2646+
(* Find the first disallowed permission, indicating that we cannot
2647+
perform the action. *)
2648+
let session_id = Context.get_session_id __context in
2649+
match
2650+
Rbac.find_first_disallowed_permission ~__context ~session_id ~permissions
2651+
with
2652+
| None ->
2653+
(* No disallowed permission, perform the update. *)
2654+
set_other_config ~__context ~self ~value
2655+
| Some disallowed ->
2656+
(* Report it as an RBAC error. *)
2657+
let action = Printf.sprintf "%s.set_other_config" object_name in
2658+
let extra_msg = "" in
2659+
let extra_dmsg = "" in
2660+
raise
2661+
(Rbac.disallowed_permission_exn ~extra_dmsg ~extra_msg ~__context
2662+
~permission:disallowed ~action
2663+
)

ocaml/xapi/message_forwarding.ml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3180,6 +3180,20 @@ 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
31833197
end
31843198

31853199
module VM_metrics = struct end

0 commit comments

Comments
 (0)