mirror of
https://github.com/Atmosphere-NX/Atmosphere
synced 2024-12-23 04:41:12 +00:00
exo2: implement SmcComputeAes, SmcGetResult, SmcGetResultData
This commit is contained in:
parent
b6b114ec40
commit
e0dbfc69a8
16 changed files with 486 additions and 24 deletions
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
#include <exosphere.hpp>
|
#include <exosphere.hpp>
|
||||||
#include "secmon_cache.hpp"
|
#include "secmon_cache.hpp"
|
||||||
|
#include "secmon_setup.hpp"
|
||||||
#include "secmon_map.hpp"
|
#include "secmon_map.hpp"
|
||||||
|
|
||||||
namespace ams::secmon {
|
namespace ams::secmon {
|
||||||
|
@ -24,8 +25,12 @@ namespace ams::secmon {
|
||||||
constexpr inline const uintptr_t BootCodeAddress = MemoryRegionVirtualTzramBootCode.GetAddress();
|
constexpr inline const uintptr_t BootCodeAddress = MemoryRegionVirtualTzramBootCode.GetAddress();
|
||||||
constexpr inline const size_t BootCodeSize = MemoryRegionVirtualTzramBootCode.GetSize();
|
constexpr inline const size_t BootCodeSize = MemoryRegionVirtualTzramBootCode.GetSize();
|
||||||
|
|
||||||
|
constinit uintptr_t g_smc_user_page_physical_address = 0;
|
||||||
|
|
||||||
using namespace ams::mmu;
|
using namespace ams::mmu;
|
||||||
|
|
||||||
|
constexpr inline PageTableMappingAttribute MappingAttributesEl3NonSecureRwData = AddMappingAttributeIndex(PageTableMappingAttributes_El3NonSecureRwData, MemoryAttributeIndexNormal);
|
||||||
|
|
||||||
constexpr void UnmapBootCodeImpl(u64 *l1, u64 *l2, u64 *l3, uintptr_t boot_code, size_t boot_code_size) {
|
constexpr void UnmapBootCodeImpl(u64 *l1, u64 *l2, u64 *l3, uintptr_t boot_code, size_t boot_code_size) {
|
||||||
/* Unmap the L3 entries corresponding to the boot code. */
|
/* Unmap the L3 entries corresponding to the boot code. */
|
||||||
InvalidateL3Entries(l3, boot_code, boot_code_size);
|
InvalidateL3Entries(l3, boot_code, boot_code_size);
|
||||||
|
@ -42,6 +47,16 @@ namespace ams::secmon {
|
||||||
InvalidateL1Entries(l1, MemoryRegionPhysical.GetAddress(), MemoryRegionPhysical.GetSize());
|
InvalidateL1Entries(l1, MemoryRegionPhysical.GetAddress(), MemoryRegionPhysical.GetSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr void MapSmcUserPageImpl(u64 *l3, uintptr_t address) {
|
||||||
|
/* Set the L3 entry. */
|
||||||
|
SetL3BlockEntry(l3, MemoryRegionVirtualSmcUserPage.GetAddress(), address, MemoryRegionVirtualSmcUserPage.GetSize(), MappingAttributesEl3NonSecureRwData);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void UnmapSmcUserPageImpl(u64 *l3) {
|
||||||
|
/* Unmap the L3 entry. */
|
||||||
|
InvalidateL3Entries(l3, MemoryRegionVirtualSmcUserPage.GetAddress(), MemoryRegionVirtualSmcUserPage.GetSize());
|
||||||
|
}
|
||||||
|
|
||||||
void ClearLow(uintptr_t address, size_t size) {
|
void ClearLow(uintptr_t address, size_t size) {
|
||||||
/* Clear the low part. */
|
/* Clear the low part. */
|
||||||
util::ClearMemory(reinterpret_cast<void *>(address), size / 2);
|
util::ClearMemory(reinterpret_cast<void *>(address), size / 2);
|
||||||
|
@ -85,4 +100,43 @@ namespace ams::secmon {
|
||||||
secmon::EnsureMappingConsistency();
|
secmon::EnsureMappingConsistency();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uintptr_t MapSmcUserPage(uintptr_t address) {
|
||||||
|
if (g_smc_user_page_physical_address != 0) {
|
||||||
|
if (!(MemoryRegionDram.GetAddress() <= address && address <= MemoryRegionDramHigh.GetEndAddress() - MemoryRegionVirtualSmcUserPage.GetSize())) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!util::IsAligned(address, 4_KB)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_smc_user_page_physical_address = address;
|
||||||
|
|
||||||
|
u64 * const l2_l3 = MemoryRegionVirtualTzramL2L3PageTable.GetPointer<u64>();
|
||||||
|
|
||||||
|
MapSmcUserPageImpl(l2_l3, address);
|
||||||
|
|
||||||
|
/* Ensure the mappings are consistent. */
|
||||||
|
secmon::EnsureMappingConsistency(MemoryRegionVirtualSmcUserPage.GetAddress());
|
||||||
|
} else {
|
||||||
|
AMS_ABORT_UNLESS(address == g_smc_user_page_physical_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MemoryRegionVirtualSmcUserPage.GetAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnmapSmcUserPage() {
|
||||||
|
if (g_smc_user_page_physical_address == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 * const l2_l3 = MemoryRegionVirtualTzramL2L3PageTable.GetPointer<u64>();
|
||||||
|
|
||||||
|
UnmapSmcUserPageImpl(l2_l3);
|
||||||
|
|
||||||
|
/* Ensure the mappings are consistent. */
|
||||||
|
secmon::EnsureMappingConsistency(MemoryRegionVirtualSmcUserPage.GetAddress());
|
||||||
|
|
||||||
|
g_smc_user_page_physical_address = 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,4 +20,7 @@ namespace ams::secmon {
|
||||||
|
|
||||||
void UnmapTzram();
|
void UnmapTzram();
|
||||||
|
|
||||||
|
uintptr_t MapSmcUserPage(uintptr_t address);
|
||||||
|
void UnmapSmcUserPage();
|
||||||
|
|
||||||
}
|
}
|
|
@ -20,6 +20,7 @@
|
||||||
#include "secmon_cpu_context.hpp"
|
#include "secmon_cpu_context.hpp"
|
||||||
#include "secmon_interrupt_handler.hpp"
|
#include "secmon_interrupt_handler.hpp"
|
||||||
#include "secmon_misc.hpp"
|
#include "secmon_misc.hpp"
|
||||||
|
#include "smc/secmon_random_cache.hpp"
|
||||||
#include "smc/secmon_smc_power_management.hpp"
|
#include "smc/secmon_smc_power_management.hpp"
|
||||||
#include "smc/secmon_smc_se_lock.hpp"
|
#include "smc/secmon_smc_se_lock.hpp"
|
||||||
|
|
||||||
|
@ -938,6 +939,9 @@ namespace ams::secmon {
|
||||||
ExitChargerHiZMode();
|
ExitChargerHiZMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Refill the random cache, which is volatile and thus wiped on warmboot. */
|
||||||
|
smc::FillRandomCache();
|
||||||
|
|
||||||
/* Unlock the security engine. */
|
/* Unlock the security engine. */
|
||||||
secmon::smc::UnlockSecurityEngine();
|
secmon::smc::UnlockSecurityEngine();
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,13 @@ namespace ams::secmon::smc {
|
||||||
KeyType_Count,
|
KeyType_Count,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum CipherMode {
|
||||||
|
CipherMode_CbcEncryption = 0,
|
||||||
|
CipherMode_CbcDecryption = 1,
|
||||||
|
CipherMode_Ctr = 2,
|
||||||
|
CipherMode_Cmac = 3,
|
||||||
|
};
|
||||||
|
|
||||||
struct GenerateAesKekOption {
|
struct GenerateAesKekOption {
|
||||||
using IsDeviceUnique = util::BitPack32::Field<0, 1, bool>;
|
using IsDeviceUnique = util::BitPack32::Field<0, 1, bool>;
|
||||||
using KeyTypeIndex = util::BitPack32::Field<1, 4, KeyType>;
|
using KeyTypeIndex = util::BitPack32::Field<1, 4, KeyType>;
|
||||||
|
@ -54,6 +61,11 @@ namespace ams::secmon::smc {
|
||||||
using Reserved = util::BitPack32::Field<8, 24, u32>;
|
using Reserved = util::BitPack32::Field<8, 24, u32>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ComputeAesOption {
|
||||||
|
using KeySlot = util::BitPack32::Field<0, 3, int>;
|
||||||
|
using CipherModeIndex = util::BitPack32::Field<4, 2, CipherMode>;
|
||||||
|
};
|
||||||
|
|
||||||
constexpr const u8 SealKeySources[SealKey_Count][AesKeySize] = {
|
constexpr const u8 SealKeySources[SealKey_Count][AesKeySize] = {
|
||||||
[SealKey_LoadAesKey] = { 0xF4, 0x0C, 0x16, 0x26, 0x0D, 0x46, 0x3B, 0xE0, 0x8C, 0x6A, 0x56, 0xE5, 0x82, 0xD4, 0x1B, 0xF6 },
|
[SealKey_LoadAesKey] = { 0xF4, 0x0C, 0x16, 0x26, 0x0D, 0x46, 0x3B, 0xE0, 0x8C, 0x6A, 0x56, 0xE5, 0x82, 0xD4, 0x1B, 0xF6 },
|
||||||
[SealKey_DecryptDeviceUniqueData] = { 0x7F, 0x54, 0x2C, 0x98, 0x1E, 0x54, 0x18, 0x3B, 0xBA, 0x63, 0xBD, 0x4C, 0x13, 0x5B, 0xF1, 0x06 },
|
[SealKey_DecryptDeviceUniqueData] = { 0x7F, 0x54, 0x2C, 0x98, 0x1E, 0x54, 0x18, 0x3B, 0xBA, 0x63, 0xBD, 0x4C, 0x13, 0x5B, 0xF1, 0x06 },
|
||||||
|
@ -81,6 +93,40 @@ namespace ams::secmon::smc {
|
||||||
[SealKey_LoadEsClientCertKey] = { 0x89, 0x96, 0x43, 0x9A, 0x7C, 0xD5, 0x59, 0x55, 0x24, 0xD5, 0x24, 0x18, 0xAB, 0x6C, 0x04, 0x61 },
|
[SealKey_LoadEsClientCertKey] = { 0x89, 0x96, 0x43, 0x9A, 0x7C, 0xD5, 0x59, 0x55, 0x24, 0xD5, 0x24, 0x18, 0xAB, 0x6C, 0x04, 0x61 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
constexpr uintptr_t LinkedListAddressMinimum = secmon::MemoryRegionDram.GetAddress();
|
||||||
|
constexpr size_t LinkedListAddressRangeSize = 4_MB - 2_KB;
|
||||||
|
constexpr uintptr_t LinkedListAddressMaximum = LinkedListAddressMinimum + LinkedListAddressRangeSize;
|
||||||
|
|
||||||
|
constexpr size_t LinkedListSize = 12;
|
||||||
|
|
||||||
|
constexpr bool IsValidLinkedListAddress(uintptr_t address) {
|
||||||
|
return LinkedListAddressMinimum <= address && address <= (LinkedListAddressMaximum - LinkedListSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
constinit bool g_is_compute_aes_completed = false;
|
||||||
|
|
||||||
|
void SecurityEngineDoneHandler() {
|
||||||
|
/* Check that the compute succeeded. */
|
||||||
|
se::ValidateAesOperationResult();
|
||||||
|
|
||||||
|
/* End the asynchronous operation. */
|
||||||
|
g_is_compute_aes_completed = true;
|
||||||
|
EndAsyncOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
SmcResult GetComputeAesResult(void *dst, size_t size) {
|
||||||
|
/* Arguments are unused. */
|
||||||
|
AMS_UNUSED(dst);
|
||||||
|
AMS_UNUSED(size);
|
||||||
|
|
||||||
|
/* Check that the operation is completed. */
|
||||||
|
SMC_R_UNLESS(g_is_compute_aes_completed, Busy);
|
||||||
|
|
||||||
|
/* Unlock the security engine and succeed. */
|
||||||
|
UnlockSecurityEngine();
|
||||||
|
return SmcResult::Success;
|
||||||
|
}
|
||||||
|
|
||||||
int PrepareMasterKey(int generation) {
|
int PrepareMasterKey(int generation) {
|
||||||
if (generation == GetKeyGeneration()) {
|
if (generation == GetKeyGeneration()) {
|
||||||
return pkg1::AesKeySlot_Master;
|
return pkg1::AesKeySlot_Master;
|
||||||
|
@ -199,6 +245,42 @@ namespace ams::secmon::smc {
|
||||||
return SmcResult::Success;
|
return SmcResult::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SmcResult ComputeAesImpl(SmcArguments &args) {
|
||||||
|
/* Decode arguments. */
|
||||||
|
u8 iv[se::AesBlockSize];
|
||||||
|
|
||||||
|
const util::BitPack32 option = { static_cast<u32>(args.r[1]) };
|
||||||
|
std::memcpy(iv, std::addressof(args.r[2]), sizeof(iv));
|
||||||
|
const u32 input_address = args.r[4];
|
||||||
|
const u32 output_address = args.r[5];
|
||||||
|
const u32 size = args.r[6];
|
||||||
|
|
||||||
|
const int slot = option.Get<ComputeAesOption::KeySlot>();
|
||||||
|
const auto cipher_mode = option.Get<ComputeAesOption::CipherModeIndex>();
|
||||||
|
|
||||||
|
/* Validate arguments. */
|
||||||
|
SMC_R_UNLESS(pkg1::IsUserAesKeySlot(slot), InvalidArgument);
|
||||||
|
SMC_R_UNLESS(util::IsAligned(size, se::AesBlockSize), InvalidArgument);
|
||||||
|
SMC_R_UNLESS(IsValidLinkedListAddress(input_address), InvalidArgument);
|
||||||
|
SMC_R_UNLESS(IsValidLinkedListAddress(output_address), InvalidArgument);
|
||||||
|
|
||||||
|
/* We're starting an aes operation, so reset the completion status. */
|
||||||
|
g_is_compute_aes_completed = false;
|
||||||
|
|
||||||
|
/* Dispatch the correct aes operation asynchronously. */
|
||||||
|
switch (cipher_mode) {
|
||||||
|
case CipherMode_CbcEncryption: se::EncryptAes128CbcAsync(output_address, slot, input_address, size, iv, sizeof(iv), SecurityEngineDoneHandler); break;
|
||||||
|
case CipherMode_CbcDecryption: se::DecryptAes128CbcAsync(output_address, slot, input_address, size, iv, sizeof(iv), SecurityEngineDoneHandler); break;
|
||||||
|
case CipherMode_Ctr: se::ComputeAes128CtrAsync(output_address, slot, input_address, size, iv, sizeof(iv), SecurityEngineDoneHandler); break;
|
||||||
|
case CipherMode_Cmac:
|
||||||
|
return SmcResult::NotImplemented;
|
||||||
|
default:
|
||||||
|
return SmcResult::InvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SmcResult::Success;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SmcResult SmcGenerateAesKek(SmcArguments &args) {
|
SmcResult SmcGenerateAesKek(SmcArguments &args) {
|
||||||
|
@ -210,8 +292,7 @@ namespace ams::secmon::smc {
|
||||||
}
|
}
|
||||||
|
|
||||||
SmcResult SmcComputeAes(SmcArguments &args) {
|
SmcResult SmcComputeAes(SmcArguments &args) {
|
||||||
/* TODO */
|
return LockSecurityEngineAndInvokeAsync(args, ComputeAesImpl, GetComputeAesResult);
|
||||||
return SmcResult::NotImplemented;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SmcResult SmcGenerateSpecificAesKey(SmcArguments &args) {
|
SmcResult SmcGenerateSpecificAesKey(SmcArguments &args) {
|
||||||
|
|
|
@ -16,17 +16,91 @@
|
||||||
#include <exosphere.hpp>
|
#include <exosphere.hpp>
|
||||||
#include "../secmon_error.hpp"
|
#include "../secmon_error.hpp"
|
||||||
#include "secmon_smc_result.hpp"
|
#include "secmon_smc_result.hpp"
|
||||||
|
#include "secmon_user_page_mapper.hpp"
|
||||||
|
|
||||||
namespace ams::secmon::smc {
|
namespace ams::secmon::smc {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constinit u64 g_async_key = InvalidAsyncKey;
|
||||||
|
constinit GetResultHandler g_async_handler = nullptr;
|
||||||
|
|
||||||
|
u64 GenerateRandomU64() {
|
||||||
|
/* NOTE: This is one of the only places where Nintendo does not do data flushing. */
|
||||||
|
/* to ensure coherency when doing random byte generation. */
|
||||||
|
/* It is not clear why it is necessary elsewhere but not here. */
|
||||||
|
/* TODO: Figure out why. */
|
||||||
|
u64 v;
|
||||||
|
se::GenerateRandomBytes(std::addressof(v), sizeof(v));
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 BeginAsyncOperation(GetResultHandler handler) {
|
||||||
|
/* Only allow one async operation at a time. */
|
||||||
|
if (g_async_key != InvalidAsyncKey) {
|
||||||
|
return InvalidAsyncKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generate a random async key. */
|
||||||
|
g_async_key = GenerateRandomU64();
|
||||||
|
g_async_handler = handler;
|
||||||
|
|
||||||
|
return g_async_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CancelAsyncOperation(u64 async_key) {
|
||||||
|
if (async_key == g_async_key) {
|
||||||
|
g_async_key = InvalidAsyncKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndAsyncOperation() {
|
||||||
|
gic::SetPending(SecurityEngineUserInterruptId);
|
||||||
|
}
|
||||||
|
|
||||||
SmcResult SmcGetResult(SmcArguments &args) {
|
SmcResult SmcGetResult(SmcArguments &args) {
|
||||||
/* TODO */
|
/* Decode arguments. */
|
||||||
return SmcResult::NotImplemented;
|
const u64 async_key = args.r[1];
|
||||||
|
|
||||||
|
/* Validate arguments. */
|
||||||
|
SMC_R_UNLESS(g_async_key != InvalidAsyncKey, NoAsyncOperation);
|
||||||
|
SMC_R_UNLESS(g_async_key == async_key, InvalidAsyncOperation);
|
||||||
|
|
||||||
|
/* Call the handler. */
|
||||||
|
args.r[1] = static_cast<u64>(g_async_handler(nullptr, 0));
|
||||||
|
g_async_key = InvalidAsyncKey;
|
||||||
|
|
||||||
|
return SmcResult::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
SmcResult SmcGetResultData(SmcArguments &args) {
|
SmcResult SmcGetResultData(SmcArguments &args) {
|
||||||
/* TODO */
|
/* Decode arguments. */
|
||||||
return SmcResult::NotImplemented;
|
const u64 async_key = args.r[1];
|
||||||
|
const uintptr_t user_phys_addr = args.r[2];
|
||||||
|
const size_t user_size = args.r[3];
|
||||||
|
|
||||||
|
/* Allocate a work buffer on the stack. */
|
||||||
|
alignas(8) u8 work_buffer[1_KB];
|
||||||
|
|
||||||
|
/* Validate arguments. */
|
||||||
|
SMC_R_UNLESS(g_async_key != InvalidAsyncKey, NoAsyncOperation);
|
||||||
|
SMC_R_UNLESS(g_async_key == async_key, InvalidAsyncOperation);
|
||||||
|
SMC_R_UNLESS(user_size <= sizeof(work_buffer), InvalidArgument);
|
||||||
|
|
||||||
|
/* Call the handler. */
|
||||||
|
args.r[1] = static_cast<u64>(g_async_handler(work_buffer, user_size));
|
||||||
|
g_async_key = InvalidAsyncKey;
|
||||||
|
|
||||||
|
/* Map the user buffer. */
|
||||||
|
{
|
||||||
|
UserPageMapper mapper(user_phys_addr);
|
||||||
|
SMC_R_UNLESS(mapper.Map(), InvalidArgument);
|
||||||
|
SMC_R_UNLESS(mapper.CopyToUser(user_phys_addr, work_buffer, user_size), InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SmcResult::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
61
exosphere2/program/source/smc/secmon_user_page_mapper.cpp
Normal file
61
exosphere2/program/source/smc/secmon_user_page_mapper.cpp
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2020 Atmosphère-NX
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms and conditions of the GNU General Public License,
|
||||||
|
* version 2, as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#include <exosphere.hpp>
|
||||||
|
#include "../secmon_map.hpp"
|
||||||
|
#include "secmon_user_page_mapper.hpp"
|
||||||
|
|
||||||
|
namespace ams::secmon::smc {
|
||||||
|
|
||||||
|
bool UserPageMapper::Map() {
|
||||||
|
this->virtual_address = MapSmcUserPage(this->physical_address);
|
||||||
|
return this->virtual_address != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *UserPageMapper::GetPointerTo(uintptr_t phys, size_t size) const {
|
||||||
|
/* Ensure we stay within the page. */
|
||||||
|
if (util::AlignDown(phys, 4_KB) != this->physical_address) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (size != 0) {
|
||||||
|
if (util::AlignDown(phys + size - 1, 4_KB) != this->physical_address) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reinterpret_cast<void *>(phys + (this->virtual_address - this->physical_address));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UserPageMapper::CopyToUser(uintptr_t dst_phys, const void *src, size_t size) const {
|
||||||
|
void * const dst = this->GetPointerTo(dst_phys, size);
|
||||||
|
if (dst == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(dst, src, size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UserPageMapper::CopyFromUser(void *dst, uintptr_t src_phys, size_t size) const {
|
||||||
|
const void * const src = this->GetPointerTo(src_phys, size);
|
||||||
|
if (src == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(dst, src, size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
exosphere2/program/source/smc/secmon_user_page_mapper.hpp
Normal file
35
exosphere2/program/source/smc/secmon_user_page_mapper.hpp
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2020 Atmosphère-NX
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms and conditions of the GNU General Public License,
|
||||||
|
* version 2, as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
#include <exosphere.hpp>
|
||||||
|
#include "secmon_smc_common.hpp"
|
||||||
|
|
||||||
|
namespace ams::secmon::smc {
|
||||||
|
|
||||||
|
class UserPageMapper {
|
||||||
|
private:
|
||||||
|
uintptr_t physical_address;
|
||||||
|
uintptr_t virtual_address;
|
||||||
|
public:
|
||||||
|
constexpr UserPageMapper(uintptr_t phys) : physical_address(util::AlignDown(phys, 4_KB)), virtual_address() { /* ... */ }
|
||||||
|
|
||||||
|
bool Map();
|
||||||
|
void *GetPointerTo(uintptr_t phys, size_t size) const;
|
||||||
|
bool CopyToUser(uintptr_t dst_phys, const void *src, size_t size) const;
|
||||||
|
bool CopyFromUser(void *dst, uintptr_t src_phys, size_t size) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -36,6 +36,8 @@ namespace ams::gic {
|
||||||
void SetSpiTargetCpu(int interrupt_id, u32 cpu_mask);
|
void SetSpiTargetCpu(int interrupt_id, u32 cpu_mask);
|
||||||
void SetSpiMode(int interrupt_id, InterruptMode mode);
|
void SetSpiMode(int interrupt_id, InterruptMode mode);
|
||||||
|
|
||||||
|
void SetPending(int interrupt_id);
|
||||||
|
|
||||||
int GetInterruptRequestId();
|
int GetInterruptRequestId();
|
||||||
void SetEndOfInterrupt(int interrupt_id);
|
void SetEndOfInterrupt(int interrupt_id);
|
||||||
|
|
||||||
|
|
|
@ -35,4 +35,8 @@ namespace ams::se {
|
||||||
|
|
||||||
void ComputeAes128Ctr(void *dst, size_t dst_size, int slot, const void *src, size_t src_size, const void *iv, size_t iv_size);
|
void ComputeAes128Ctr(void *dst, size_t dst_size, int slot, const void *src, size_t src_size, const void *iv, size_t iv_size);
|
||||||
|
|
||||||
|
void EncryptAes128CbcAsync(u32 out_ll_address, int slot, u32 in_ll_address, u32 size, const void *iv, size_t iv_size, DoneHandler handler);
|
||||||
|
void DecryptAes128CbcAsync(u32 out_ll_address, int slot, u32 in_ll_address, u32 size, const void *iv, size_t iv_size, DoneHandler handler);
|
||||||
|
void ComputeAes128CtrAsync(u32 out_ll_address, int slot, u32 in_ll_address, u32 size, const void *iv, size_t iv_size, DoneHandler handler);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,4 +30,6 @@ namespace ams::se {
|
||||||
|
|
||||||
void HandleInterrupt();
|
void HandleInterrupt();
|
||||||
|
|
||||||
|
void ValidateAesOperationResult();
|
||||||
|
|
||||||
}
|
}
|
|
@ -73,6 +73,7 @@ namespace ams::secmon {
|
||||||
constexpr inline const MemoryRegion MemoryRegionVirtual = MemoryRegion(UINT64_C(0x1F0000000), 2_MB);
|
constexpr inline const MemoryRegion MemoryRegionVirtual = MemoryRegion(UINT64_C(0x1F0000000), 2_MB);
|
||||||
constexpr inline const MemoryRegion MemoryRegionPhysical = MemoryRegion(UINT64_C( 0x40000000), 1_GB);
|
constexpr inline const MemoryRegion MemoryRegionPhysical = MemoryRegion(UINT64_C( 0x40000000), 1_GB);
|
||||||
constexpr inline const MemoryRegion MemoryRegionDram = MemoryRegion(UINT64_C( 0x80000000), 2_GB);
|
constexpr inline const MemoryRegion MemoryRegionDram = MemoryRegion(UINT64_C( 0x80000000), 2_GB);
|
||||||
|
constexpr inline const MemoryRegion MemoryRegionDramHigh = MemoryRegion(MemoryRegionDram.GetEndAddress(), 2_GB);
|
||||||
|
|
||||||
constexpr inline const MemoryRegion MemoryRegionDramGpuCarveout = MemoryRegion(UINT64_C(0x80020000), UINT64_C(0x40000));
|
constexpr inline const MemoryRegion MemoryRegionDramGpuCarveout = MemoryRegion(UINT64_C(0x80020000), UINT64_C(0x40000));
|
||||||
static_assert(MemoryRegionDram.Contains(MemoryRegionDramGpuCarveout));
|
static_assert(MemoryRegionDram.Contains(MemoryRegionDramGpuCarveout));
|
||||||
|
|
|
@ -206,6 +206,10 @@ namespace ams::gic {
|
||||||
ReadWrite(g_distributor_address + offsetof(GicDistributor, icfgr), 2, interrupt_id, static_cast<u32>(mode) << 1);
|
ReadWrite(g_distributor_address + offsetof(GicDistributor, icfgr), 2, interrupt_id, static_cast<u32>(mode) << 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetPending(int interrupt_id) {
|
||||||
|
Write(g_distributor_address + offsetof(GicDistributor, ispendr), 1, interrupt_id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
int GetInterruptRequestId() {
|
int GetInterruptRequestId() {
|
||||||
return reg::Read(GetCpuInterface()->iar);
|
return reg::Read(GetCpuInterface()->iar);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,22 @@ namespace ams::se {
|
||||||
SE_REG_BITS_ENUM (CRYPTO_CONFIG_XOR_POS, BOTTOM),
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_XOR_POS, BOTTOM),
|
||||||
SE_REG_BITS_ENUM (CRYPTO_CONFIG_HASH_ENB, DISABLE));
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_HASH_ENB, DISABLE));
|
||||||
|
|
||||||
|
constexpr inline u32 AesConfigCbcEncrypt = reg::Encode(SE_REG_BITS_VALUE(CRYPTO_CONFIG_CTR_CNTN, 0),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_KEYSCH_BYPASS, DISABLE),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_IV_SELECT, ORIGINAL),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_VCTRAM_SEL, INIT_AESOUT),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_INPUT_SEL, MEMORY),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_XOR_POS, TOP),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_HASH_ENB, DISABLE));
|
||||||
|
|
||||||
|
constexpr inline u32 AesConfigCbcDecrypt = reg::Encode(SE_REG_BITS_VALUE(CRYPTO_CONFIG_CTR_CNTN, 0),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_KEYSCH_BYPASS, DISABLE),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_IV_SELECT, ORIGINAL),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_VCTRAM_SEL, INIT_PREV_MEMORY),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_INPUT_SEL, MEMORY),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_XOR_POS, BOTTOM),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_CONFIG_HASH_ENB, DISABLE));
|
||||||
|
|
||||||
void SetConfig(volatile SecurityEngineRegisters *SE, bool encrypt, SE_CONFIG_DST dst) {
|
void SetConfig(volatile SecurityEngineRegisters *SE, bool encrypt, SE_CONFIG_DST dst) {
|
||||||
reg::Write(SE->SE_CONFIG, SE_REG_BITS_ENUM (CONFIG_ENC_MODE, AESMODE_KEY128),
|
reg::Write(SE->SE_CONFIG, SE_REG_BITS_ENUM (CONFIG_ENC_MODE, AESMODE_KEY128),
|
||||||
SE_REG_BITS_ENUM (CONFIG_DEC_MODE, AESMODE_KEY128),
|
SE_REG_BITS_ENUM (CONFIG_DEC_MODE, AESMODE_KEY128),
|
||||||
|
@ -73,9 +89,9 @@ namespace ams::se {
|
||||||
reg::ReadWrite(SE->SE_CONFIG, REG_BITS_VALUE(16, 16, mode));
|
reg::ReadWrite(SE->SE_CONFIG, REG_BITS_VALUE(16, 16, mode));
|
||||||
}
|
}
|
||||||
|
|
||||||
// void UpdateMemoryInterface(volatile SecurityEngineRegisters *SE, MemoryInterface memif) {
|
void UpdateMemoryInterface(volatile SecurityEngineRegisters *SE, MemoryInterface memif) {
|
||||||
// reg::ReadWrite(SE->SE_CRYPTO_CONFIG, SE_REG_BITS_VALUE(CRYPTO_CONFIG_MEMIF, memif));
|
reg::ReadWrite(SE->SE_CRYPTO_CONFIG, SE_REG_BITS_VALUE(CRYPTO_CONFIG_MEMIF, memif));
|
||||||
// }
|
}
|
||||||
|
|
||||||
void SetCounter(volatile SecurityEngineRegisters *SE, const void *ctr) {
|
void SetCounter(volatile SecurityEngineRegisters *SE, const void *ctr) {
|
||||||
const u32 *ctr_32 = reinterpret_cast<const u32 *>(ctr);
|
const u32 *ctr_32 = reinterpret_cast<const u32 *>(ctr);
|
||||||
|
@ -87,6 +103,25 @@ namespace ams::se {
|
||||||
reg::Write(SE->SE_CRYPTO_LINEAR_CTR[3], util::LoadLittleEndian(ctr_32 + 3));
|
reg::Write(SE->SE_CRYPTO_LINEAR_CTR[3], util::LoadLittleEndian(ctr_32 + 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetAesKeyIv(volatile SecurityEngineRegisters *SE, int slot, const void *iv, size_t iv_size) {
|
||||||
|
AMS_ABORT_UNLESS(0 <= slot && slot < AesKeySlotCount);
|
||||||
|
AMS_ABORT_UNLESS(iv_size <= AesBlockSize);
|
||||||
|
|
||||||
|
/* Set each iv word in order. */
|
||||||
|
const u32 *iv_u32 = static_cast<const u32 *>(iv);
|
||||||
|
const int num_words = iv_size / sizeof(u32);
|
||||||
|
for (int i = 0; i < num_words; ++i) {
|
||||||
|
/* Select the keyslot. */
|
||||||
|
reg::Write(SE->SE_CRYPTO_KEYTABLE_ADDR, SE_REG_BITS_VALUE(CRYPTO_KEYTABLE_ADDR_KEYIV_KEY_SLOT, slot),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_KEYTABLE_ADDR_KEYIV_KEYIV_SEL, IV),
|
||||||
|
SE_REG_BITS_ENUM (CRYPTO_KEYTABLE_ADDR_KEYIV_IV_SEL, ORIGINAL_IV),
|
||||||
|
SE_REG_BITS_VALUE(CRYPTO_KEYTABLE_ADDR_KEYIV_KEY_WORD, i));
|
||||||
|
|
||||||
|
/* Set the key word. */
|
||||||
|
SE->SE_CRYPTO_KEYTABLE_DATA = *(iv_u32++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SetEncryptedAesKey(int dst_slot, int kek_slot, const void *key, size_t key_size, AesMode mode) {
|
void SetEncryptedAesKey(int dst_slot, int kek_slot, const void *key, size_t key_size, AesMode mode) {
|
||||||
AMS_ABORT_UNLESS(key_size <= AesKeySizeMax);
|
AMS_ABORT_UNLESS(key_size <= AesKeySizeMax);
|
||||||
AMS_ABORT_UNLESS(0 <= dst_slot && dst_slot < AesKeySlotCount);
|
AMS_ABORT_UNLESS(0 <= dst_slot && dst_slot < AesKeySlotCount);
|
||||||
|
@ -133,6 +168,29 @@ namespace ams::se {
|
||||||
ExecuteOperationSingleBlock(SE, dst, dst_size, src, src_size);
|
ExecuteOperationSingleBlock(SE, dst, dst_size, src, src_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputeAes128Async(u32 out_ll_address, int slot, u32 in_ll_address, u32 size, DoneHandler handler, u32 config, bool encrypt, volatile SecurityEngineRegisters *SE) {
|
||||||
|
/* If nothing to decrypt, succeed. */
|
||||||
|
if (size == 0) { return; }
|
||||||
|
|
||||||
|
/* Validate input. */
|
||||||
|
AMS_ABORT_UNLESS(0 <= slot && slot < AesKeySlotCount);
|
||||||
|
|
||||||
|
/* Configure for the specific operation. */
|
||||||
|
SetConfig(SE, encrypt, SE_CONFIG_DST_MEMORY);
|
||||||
|
SetAesConfig(SE, slot, encrypt, config);
|
||||||
|
UpdateMemoryInterface(SE, MemoryInterface_Mc);
|
||||||
|
|
||||||
|
/* Configure the number of blocks. */
|
||||||
|
const int num_blocks = size / AesBlockSize;
|
||||||
|
SetBlockCount(SE, num_blocks);
|
||||||
|
|
||||||
|
/* Set the done handler. */
|
||||||
|
SetDoneHandler(SE, handler);
|
||||||
|
|
||||||
|
/* Start the raw operation. */
|
||||||
|
StartOperationRaw(SE, SE_OPERATION_OP_START, out_ll_address, in_ll_address);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearAesKeySlot(int slot) {
|
void ClearAesKeySlot(int slot) {
|
||||||
|
@ -270,4 +328,49 @@ namespace ams::se {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EncryptAes128CbcAsync(u32 out_ll_address, int slot, u32 in_ll_address, u32 size, const void *iv, size_t iv_size, DoneHandler handler) {
|
||||||
|
/* Validate the iv. */
|
||||||
|
AMS_ABORT_UNLESS(iv_size == AesBlockSize);
|
||||||
|
|
||||||
|
/* Get the registers. */
|
||||||
|
volatile auto *SE = GetRegisters();
|
||||||
|
|
||||||
|
/* Set the iv. */
|
||||||
|
SetAesKeyIv(SE, slot, iv, iv_size);
|
||||||
|
|
||||||
|
/* Perform the asynchronous aes operation. */
|
||||||
|
ComputeAes128Async(out_ll_address, slot, in_ll_address, size, handler, AesConfigCbcEncrypt, true, SE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DecryptAes128CbcAsync(u32 out_ll_address, int slot, u32 in_ll_address, u32 size, const void *iv, size_t iv_size, DoneHandler handler) {
|
||||||
|
/* Validate the iv. */
|
||||||
|
AMS_ABORT_UNLESS(iv_size == AesBlockSize);
|
||||||
|
|
||||||
|
/* Get the registers. */
|
||||||
|
volatile auto *SE = GetRegisters();
|
||||||
|
|
||||||
|
/* Set the iv. */
|
||||||
|
SetAesKeyIv(SE, slot, iv, iv_size);
|
||||||
|
|
||||||
|
/* Perform the asynchronous aes operation. */
|
||||||
|
ComputeAes128Async(out_ll_address, slot, in_ll_address, size, handler, AesConfigCbcDecrypt, false, SE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputeAes128CtrAsync(u32 out_ll_address, int slot, u32 in_ll_address, u32 size, const void *iv, size_t iv_size, DoneHandler handler) {
|
||||||
|
/* Validate the iv. */
|
||||||
|
AMS_ABORT_UNLESS(iv_size == AesBlockSize);
|
||||||
|
|
||||||
|
/* Get the registers. */
|
||||||
|
volatile auto *SE = GetRegisters();
|
||||||
|
|
||||||
|
/* Here Nintendo writes 1 to SE_SPARE. It's unclear why they do this, but we will do so as well. */
|
||||||
|
SE->SE_SPARE = 0x1;
|
||||||
|
|
||||||
|
/* Set the counter. */
|
||||||
|
SetCounter(SE, iv);
|
||||||
|
|
||||||
|
/* Perform the asynchronous aes operation. */
|
||||||
|
ComputeAes128Async(out_ll_address, slot, in_ll_address, size, handler, AesConfigCtr, true, SE);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,13 @@ namespace ams::se {
|
||||||
reg::Write(SE->SE_OPERATION, SE_REG_BITS_VALUE(OPERATION_OP, op));
|
reg::Write(SE->SE_OPERATION, SE_REG_BITS_VALUE(OPERATION_OP, op));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EnsureOperationStarted(volatile SecurityEngineRegisters *SE) {
|
||||||
|
/* Read the operation register to make sure our write takes. */
|
||||||
|
reg::Read(SE->SE_OPERATION);
|
||||||
|
|
||||||
|
hw::DataSynchronizationBarrierInnerShareable();
|
||||||
|
}
|
||||||
|
|
||||||
void WaitForOperationComplete(volatile SecurityEngineRegisters *SE) {
|
void WaitForOperationComplete(volatile SecurityEngineRegisters *SE) {
|
||||||
/* Spin until the operation is done. */
|
/* Spin until the operation is done. */
|
||||||
while (reg::HasValue(SE->SE_INT_STATUS, SE_REG_BITS_ENUM(INT_STATUS_SE_OP_DONE, CLEAR))) { /* ... */ }
|
while (reg::HasValue(SE->SE_INT_STATUS, SE_REG_BITS_ENUM(INT_STATUS_SE_OP_DONE, CLEAR))) { /* ... */ }
|
||||||
|
@ -124,6 +131,18 @@ namespace ams::se {
|
||||||
std::memcpy(dst, aligned, dst_size);
|
std::memcpy(dst, aligned, dst_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StartOperationRaw(volatile SecurityEngineRegisters *SE, SE_OPERATION_OP op, u32 out_ll_address, u32 in_ll_address) {
|
||||||
|
/* Configure the linked list addresses. */
|
||||||
|
reg::Write(SE->SE_IN_LL_ADDR, in_ll_address);
|
||||||
|
reg::Write(SE->SE_OUT_LL_ADDR, out_ll_address);
|
||||||
|
|
||||||
|
/* Start the operation. */
|
||||||
|
StartOperation(SE, op);
|
||||||
|
|
||||||
|
/* Ensure the operation is started. */
|
||||||
|
EnsureOperationStarted(SE);
|
||||||
|
}
|
||||||
|
|
||||||
void ValidateAesOperationResult(volatile SecurityEngineRegisters *SE) {
|
void ValidateAesOperationResult(volatile SecurityEngineRegisters *SE) {
|
||||||
/* Ensure no error occurred. */
|
/* Ensure no error occurred. */
|
||||||
AMS_ABORT_UNLESS(reg::HasValue(SE->SE_INT_STATUS, SE_REG_BITS_ENUM(INT_STATUS_ERR_STAT, CLEAR)));
|
AMS_ABORT_UNLESS(reg::HasValue(SE->SE_INT_STATUS, SE_REG_BITS_ENUM(INT_STATUS_ERR_STAT, CLEAR)));
|
||||||
|
@ -135,4 +154,8 @@ namespace ams::se {
|
||||||
AMS_ABORT_UNLESS(reg::Read(SE->SE_ERR_STATUS) == 0);
|
AMS_ABORT_UNLESS(reg::Read(SE->SE_ERR_STATUS) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ValidateAesOperationResult() {
|
||||||
|
return ValidateAesOperationResult(GetRegisters());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ namespace ams::se {
|
||||||
void ExecuteOperation(volatile SecurityEngineRegisters *SE, SE_OPERATION_OP op, void *dst, size_t dst_size, const void *src, size_t src_size);
|
void ExecuteOperation(volatile SecurityEngineRegisters *SE, SE_OPERATION_OP op, void *dst, size_t dst_size, const void *src, size_t src_size);
|
||||||
void ExecuteOperationSingleBlock(volatile SecurityEngineRegisters *SE, void *dst, size_t dst_size, const void *src, size_t src_size);
|
void ExecuteOperationSingleBlock(volatile SecurityEngineRegisters *SE, void *dst, size_t dst_size, const void *src, size_t src_size);
|
||||||
|
|
||||||
|
void StartOperationRaw(volatile SecurityEngineRegisters *SE, SE_OPERATION_OP op, u32 out_ll_address, u32 in_ll_address);
|
||||||
|
void SetDoneHandler(volatile SecurityEngineRegisters *SE, DoneHandler handler);
|
||||||
|
|
||||||
void ValidateAesOperationResult(volatile SecurityEngineRegisters *SE);
|
void ValidateAesOperationResult(volatile SecurityEngineRegisters *SE);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,4 +105,12 @@ namespace ams::se {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetDoneHandler(volatile SecurityEngineRegisters *SE, DoneHandler handler) {
|
||||||
|
/* Set the done handler. */
|
||||||
|
g_done_handler = handler;
|
||||||
|
|
||||||
|
/* Configure to trigger an interrupt when done. */
|
||||||
|
reg::Write(SE->SE_INT_ENABLE, SE_REG_BITS_ENUM(INT_ENABLE_SE_OP_DONE, ENABLE));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue