1 /*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "berberis/runtime_primitives/memory_region_reservation.h"
18
19 #include <array>
20 #include <atomic>
21
22 #include "berberis/guest_state/guest_addr.h"
23 #include "berberis/guest_state/guest_state_arch.h"
24
25 namespace berberis {
26
27 namespace {
28
29 template <bool flag = false>
static_bad_size()30 void static_bad_size() {
31 static_assert(flag, "Expected Reservation to be of size 8 or 16");
32 }
33
34 template <typename ReservationType>
MemoryRegionReservationLoadTemplate(GuestAddr addr,std::memory_order mem_order)35 inline ReservationType MemoryRegionReservationLoadTemplate(GuestAddr addr,
36 std::memory_order mem_order) {
37 if constexpr (sizeof(ReservationType) == 16) {
38 // Intel doesn't have atomic 128-bit load other that CMPXCHG16B, which is also
39 // a store, and doesn't work for read-only memory. We only support guests that
40 // are similar to x86 in that a 128-bit load is two atomic 64-bit loads.
41 ReservationType low =
42 std::atomic_load_explicit(ToHostAddr<std::atomic<uint64_t>>(addr), mem_order);
43 ReservationType high =
44 std::atomic_load_explicit(ToHostAddr<std::atomic<uint64_t>>(addr + 8), mem_order);
45 return (high << 64) | low;
46 } else if constexpr (sizeof(ReservationType) == 8) {
47 ReservationType reservation;
48 #if defined(__i386__)
49 // Starting from i486 all accesses for all instructions are atomic when they are used for
50 // naturally-aligned variables of uint8_t, uint16_t and uint32_t types. But situation is not so
51 // straightforward when we are dealing with uint64_t.
52 //
53 // This is what Intel manual says about atomicity of 64-bit memory operations:
54 // The Pentium processor (and newer processors since) guarantees that the following additional
55 // memory operations will always be carried out atomically:
56 // * Reading or writing a quadword aligned on a 64-bit boundary
57 //
58 // AMD manual says the same thing:
59 // Single load or store operations (from instructions that do just a single load or store) are
60 // naturally atomic on any AMD64 processor as long as they do not cross an aligned 8-byte
61 // boundary. Accesses up to eight bytes in size which do cross such a boundary may be
62 // performed atomically using certain instructions with a lock prefix, such as XCHG, CMPXCHG
63 // or CMPXCHG8B, as long as all such accesses are done using the same technique.
64 //
65 // Fortunately, the RISC-V ISA manual agrees as well - only accesses to naturally aligned memory
66 // are required to be performed atomically.
67 //
68 // Thus using regular x86 movq is good enough for emulation of RISC-V behavior.
69 //
70 // But std::atomic<uint64_t> would always use heavy "lock chmpxchg8b" operation on IA32 platform
71 // because uint64_t is not guaranteed to be naturally-aligned on IA32!
72 //
73 // Not only is this slow, but this fails when we are accessing read-only memory!
74 //
75 // Use raw "movq" assembler instruction to circumvent that limitation of IA32 ABI.
76 __asm__ __volatile__("movq (%1),%0" : "=x"(reservation) : "r"(addr));
77 #else
78 reservation = std::atomic_load_explicit(ToHostAddr<std::atomic<uint64_t>>(addr), mem_order);
79 #endif
80 return reservation;
81 } else {
82 static_bad_size();
83 }
84 }
85
MemoryRegionReservationLoad(GuestAddr addr,std::memory_order mem_order)86 inline Reservation MemoryRegionReservationLoad(GuestAddr addr, std::memory_order mem_order) {
87 return MemoryRegionReservationLoadTemplate<Reservation>(addr, mem_order);
88 }
89
GetEntry(GuestAddr addr)90 MemoryRegionReservation::Entry& GetEntry(GuestAddr addr) {
91 static constexpr size_t kHashSize = 4096;
92 static std::array<MemoryRegionReservation::Entry, kHashSize> g_owners;
93
94 return g_owners[(addr / sizeof(Reservation)) % kHashSize];
95 }
96
97 // Special owner to disallow stealing. Only used when exclusive store is in progress.
98 int g_fake_cpu;
99 constexpr void* kLockedOwner = &g_fake_cpu;
100
101 } // namespace
102
SetOwner(GuestAddr aligned_addr,void * cpu)103 void MemoryRegionReservation::SetOwner(GuestAddr aligned_addr, void* cpu) {
104 auto& entry = GetEntry(aligned_addr);
105
106 // Try stealing. Fails if another thread is doing an exclusive store or wins a race.
107 // If stealing fails, then the subsequent exclusive store fails as well.
108 auto prev = entry.load();
109 if (prev != kLockedOwner) {
110 entry.compare_exchange_strong(prev, cpu);
111 }
112 }
113
TryLock(GuestAddr aligned_addr,void * cpu)114 MemoryRegionReservation::Entry* MemoryRegionReservation::TryLock(GuestAddr aligned_addr,
115 void* cpu) {
116 auto& entry = GetEntry(aligned_addr);
117
118 // Try locking. Fails if Load failed to steal the address or the address was stolen afterwards.
119 if (!entry.compare_exchange_strong(cpu, kLockedOwner)) {
120 return nullptr;
121 }
122
123 return &entry;
124 }
125
Unlock(MemoryRegionReservation::Entry * entry)126 void MemoryRegionReservation::Unlock(MemoryRegionReservation::Entry* entry) {
127 // No need to compare and swap as the locked address cannot be stolen.
128 entry->store(nullptr);
129 }
130
ReservationLoad(void * cpu,GuestAddr aligned_addr,std::memory_order mem_order)131 Reservation MemoryRegionReservation::ReservationLoad(void* cpu,
132 GuestAddr aligned_addr,
133 std::memory_order mem_order) {
134 SetOwner(aligned_addr, cpu);
135
136 // ATTENTION!
137 // For region size <= 8, region load is atomic, so this always returns a consistent value.
138 // For region size > 8, region load is NOT atomic! The returned value might be inconsistent.
139 //
140 // If, to load a 16-byte value atomically, the guest architecture suggests to perform a 16-byte
141 // exclusive load and then an exclusive store of the loaded value. The loaded value can be used
142 // only if the exclusive store succeeds.
143 //
144 // If developers are aware of the above and do not use the result of 16-byte exclusive load
145 // without a subsequent check by an exclusive store, an inconsistent return value here is safe.
146 // Too bad if this is not the case...
147 return MemoryRegionReservationLoad(aligned_addr, mem_order);
148 }
149
ReservationExchange(void * cpu,GuestAddr aligned_addr,Reservation expected,Reservation value,std::memory_order mem_order)150 bool MemoryRegionReservation::ReservationExchange(void* cpu,
151 GuestAddr aligned_addr,
152 Reservation expected,
153 Reservation value,
154 std::memory_order mem_order) {
155 auto* entry = TryLock(aligned_addr, cpu);
156
157 if (!entry) {
158 return false;
159 }
160
161 bool written = std::atomic_compare_exchange_strong_explicit(
162 ToHostAddr<std::atomic<Reservation>>(aligned_addr),
163 &expected,
164 value,
165 mem_order,
166 std::memory_order_relaxed);
167
168 Unlock(entry);
169
170 return written;
171 }
172
173 } // namespace berberis
174