Files
raven/native/nt/registry_api.cpp
T

605 lines
23 KiB
C++

#include "kernel_context.h"
#include "nls.h"
#include "registry.h"
#include "text.h"
#include <algorithm>
#include <cstring>
#include <cstdlib>
namespace raven {
namespace {
constexpr uint32_t error_more_data = 234, error_no_more_items = 259, error_invalid_parameter = 87, error_file_not_found = 2;
constexpr uint32_t type_sz = 1, type_expand_sz = 2, type_multi_sz = 7, type_dword = 4, type_qword = 11, type_binary = 3;
bool is_text(uint32_t type)
{
return type == type_sz || type == type_expand_sz || type == type_multi_sz;
}
// Registry text is stored as UTF-16; ANSI callers receive the process code page, always terminated.
std::vector<uint8_t> to_ansi_bytes(const std::vector<uint8_t> &wide, uint32_t code_page)
{
std::u16string text;
for (size_t index = 0; index + 1 < wide.size(); index += 2)
text.push_back(char16_t(wide[index] | wide[index + 1] << 8));
auto ansi = encode_ansi(text, code_page == 1252 ? 1252 : 1252);
if (ansi.empty() || ansi.back() != '\0')
ansi.push_back('\0');
return std::vector<uint8_t>(ansi.begin(), ansi.end());
}
std::vector<uint8_t> from_ansi_bytes(std::span<const uint8_t> ansi, uint32_t code_page)
{
const auto text = from_ansi(std::string_view(reinterpret_cast<const char *>(ansi.data()), ansi.size()), code_page);
std::vector<uint8_t> bytes(text.size() * 2);
std::memcpy(bytes.data(), text.data(), bytes.size());
return bytes;
}
}
// The settings the control panel keeps in the prefix registry take effect as the process starts: the runtime options under
// HKCU\Software\Raven\Environment, and the visual styles switched off as Windows records it (ThemeManager ThemeActive "0"). What the
// environment already sets wins over the registry.
void apply_registry_settings(NativeRegistry &hive)
{
const auto text = [](const std::vector<uint8_t> &data) {
std::u16string value(data.size() / 2, u'\0');
std::memcpy(value.data(), data.data(), value.size() * 2);
while (!value.empty() && !value.back())
value.pop_back();
return to_utf8(value);
};
uint64_t key = 0;
if (!hive.open(0x80000001, u"Software\\Raven\\Environment", key)) {
std::u16string name;
uint32_t type = 0;
std::vector<uint8_t> data;
for (uint32_t index = 0; !hive.enum_value(key, index, name, type, data); ++index) {
const auto variable = to_utf8(name);
if ((type == 1 || type == 2) && variable.starts_with("RAVEN_"))
::setenv(variable.c_str(), text(data).c_str(), 0);
}
hive.close(key);
}
if (!hive.open(0x80000001, u"Software\\Microsoft\\Windows\\CurrentVersion\\ThemeManager", key)) {
uint32_t type = 0;
std::vector<uint8_t> data;
if (!hive.query(key, u"ThemeActive", type, data) && type == 1 && text(data) == "0")
::setenv("RAVEN_THEME", "classic", 0);
hive.close(key);
}
}
void register_registry_api(ApiRegistry &registry, KernelContext &context, NativeRegistry &hive)
{
const uint32_t ansi_page = context.launch.ansi_code_page;
// Key handles come from the same space as every other handle, so CloseHandle and NtClose close them too.
hive.set_allocator([&objects = context.objects, &hive] { return objects.reserve([&hive](uint64_t key) { hive.close(key); }); });
const size_t handle_width = context.process.mode() == NativeMode::x86 ? 4 : 8;
const auto text_of = [](ApiCall &call, uint64_t address, bool wide) {
if (!address)
return std::u16string();
return wide ? call.wide(address, 16384) : from_ansi(call.narrow(address, 16384), 1252);
};
const auto put_handle = [handle_width](ApiCall &call, uint64_t address, uint64_t value) {
std::vector<uint8_t> bytes(handle_width);
std::memcpy(bytes.data(), &value, handle_width);
call.write(address, bytes);
};
// A 32-bit program sees HKEY_LOCAL_MACHINE\SOFTWARE as the Wow6432Node subkey of it, except for the keys both views share; the key
// SOFTWARE itself opens as that subkey, so that what is opened below it stays in the view. KEY_WOW64_64KEY (0x100) asks for the
// 64-bit view from either width and KEY_WOW64_32KEY (0x200) for the 32-bit one.
const bool wow64 = context.process.mode() == NativeMode::x86;
const auto view = [wow64](uint64_t parent, std::u16string path, uint32_t access = 0) {
const bool redirect = (access & 0x200) || (wow64 && !(access & 0x100));
if (!redirect || parent != 0x80000002ULL)
return path;
const auto fold = [](std::u16string text) {
for (auto &character : text)
if (character >= u'a' && character <= u'z')
character = char16_t(character - (u'a' - u'A'));
return text;
};
while (!path.empty() && path.front() == u'\\')
path.erase(0, 1);
const auto first = path.find(u'\\');
if (fold(path.substr(0, first)) != u"SOFTWARE")
return path;
if (first == std::u16string::npos || first + 1 == path.size())
return path.substr(0, first) + u"\\Wow6432Node";
const auto second_end = path.find(u'\\', first + 1);
const auto second = fold(path.substr(first + 1, second_end == std::u16string::npos ? std::u16string::npos : second_end - first - 1));
if (second == u"CLASSES" || second == u"WOW6432NODE")
return path;
return path.substr(0, first + 1) + u"Wow6432Node\\" + path.substr(first + 1);
};
for (const bool wide : {true, false}) {
const std::string suffix = wide ? "W" : "A";
registry.add("RegOpenKeyEx" + suffix, 5, [&hive, text_of, put_handle, wide, view](ApiCall &call) {
if (!call[4]) {
call.ret(error_invalid_parameter);
return;
}
uint64_t key = 0;
const auto status = hive.open(call[0], view(call[0], text_of(call, call[1], wide), call.u32(3)), key);
if (!status)
put_handle(call, call[4], key);
call.ret(status);
});
registry.add("RegOpenKey" + suffix, 3, [&hive, text_of, put_handle, wide, view](ApiCall &call) {
if (!call[2]) {
call.ret(error_invalid_parameter);
return;
}
uint64_t key = 0;
const auto status = hive.open(call[0], view(call[0], text_of(call, call[1], wide)), key);
if (!status)
put_handle(call, call[2], key);
call.ret(status);
});
registry.add("RegCreateKey" + suffix, 3, [&hive, text_of, put_handle, wide, view](ApiCall &call) {
if (!call[2]) {
call.ret(error_invalid_parameter);
return;
}
uint64_t key = 0;
uint32_t disposition = 0;
const auto status = hive.create(call[0], view(call[0], text_of(call, call[1], wide)), key, disposition);
if (!status)
put_handle(call, call[2], key);
call.ret(status);
});
registry.add("RegCreateKeyEx" + suffix, 9, [&hive, text_of, put_handle, wide, view](ApiCall &call) {
if (!call[7]) {
call.ret(error_invalid_parameter);
return;
}
uint64_t key = 0;
uint32_t disposition = 0;
const auto status = hive.create(call[0], view(call[0], text_of(call, call[1], wide), call.u32(5)), key, disposition);
if (!status) {
put_handle(call, call[7], key);
if (call[8]) call.store<uint32_t>(call[8], disposition);
}
call.ret(status);
});
registry.add("RegSetValueEx" + suffix, 6, [&hive, text_of, wide, ansi_page](ApiCall &call) {
const auto name = text_of(call, call[1], wide);
const uint32_t type = call.u32(3), size = call.u32(5);
if (!call[4] && size) {
call.ret(error_invalid_parameter);
return;
}
auto data = size ? call.read(call[4], size) : std::vector<uint8_t>{};
if (!wide && is_text(type))
data = from_ansi_bytes(data, ansi_page);
call.ret(hive.set(call[0], name, type, data));
});
registry.add("RegQueryValueEx" + suffix, 6, [&hive, text_of, wide, ansi_page](ApiCall &call) {
const auto name = text_of(call, call[1], wide);
if (call[4] && !call[5]) {
call.ret(error_invalid_parameter);
return;
}
uint32_t type = 0;
std::vector<uint8_t> data;
const auto status = hive.query(call[0], name, type, data);
if (status) {
call.ret(status);
return;
}
if (!wide && is_text(type))
data = to_ansi_bytes(data, ansi_page);
if (call[3]) call.store<uint32_t>(call[3], type);
if (!call[4]) {
if (call[5]) call.store<uint32_t>(call[5], uint32_t(data.size()));
call.ret(0);
return;
}
const uint32_t capacity = call.load<uint32_t>(call[5]);
call.store<uint32_t>(call[5], uint32_t(data.size()));
if (data.size() > capacity) {
call.ret(error_more_data);
return;
}
call.write(call[4], data);
call.ret(0);
});
registry.add("RegDeleteValue" + suffix, 2, [&hive, text_of, wide](ApiCall &call) { call.ret(hive.remove_value(call[0], text_of(call, call[1], wide))); });
registry.add("RegDeleteKey" + suffix, 2, [&hive, text_of, wide, view](ApiCall &call) { call.ret(hive.remove_key(call[0], view(call[0], text_of(call, call[1], wide)))); });
// The older form: the buffer size is a plain count of characters, not a pointer to one.
registry.add("RegEnumKey" + suffix, 4, [&hive, wide](ApiCall &call) {
std::u16string name;
const auto status = hive.enum_key(call[0], call.u32(1), name);
if (status) {
call.ret(status);
return;
}
const uint32_t capacity = call.u32(3);
const auto narrow = wide ? std::string() : encode_ansi(name, 1252);
const size_t length = wide ? name.size() : narrow.size();
if (!call[2] || capacity <= length) {
call.ret(call[2] ? error_more_data : error_invalid_parameter);
return;
}
if (wide) call.put_wide(call[2], name, capacity);
else call.put_narrow(call[2], narrow, capacity);
call.ret(0);
});
registry.add("RegEnumKeyEx" + suffix, 8, [&hive, wide](ApiCall &call) {
std::u16string name;
const auto status = hive.enum_key(call[0], call.u32(1), name);
if (status) {
call.ret(status);
return;
}
const uint32_t capacity = call.load<uint32_t>(call[3]);
const auto narrow = wide ? std::string() : encode_ansi(name, 1252);
const size_t length = wide ? name.size() : narrow.size();
if (capacity <= length) {
call.ret(error_more_data);
return;
}
if (wide) call.put_wide(call[2], name, capacity);
else call.put_narrow(call[2], narrow, capacity);
call.store<uint32_t>(call[3], uint32_t(length));
if (call[5] && call[6]) {
if (call.load<uint32_t>(call[6]) > 0)
call.store<uint16_t>(call[5], 0);
call.store<uint32_t>(call[6], 0);
}
if (call[7]) call.store<uint64_t>(call[7], 0);
call.ret(0);
});
registry.add("RegEnumValue" + suffix, 8, [&hive, wide, ansi_page](ApiCall &call) {
std::u16string name;
uint32_t type = 0;
std::vector<uint8_t> data;
const auto status = hive.enum_value(call[0], call.u32(1), name, type, data);
if (status) {
call.ret(status);
return;
}
if (!wide && is_text(type))
data = to_ansi_bytes(data, ansi_page);
const uint32_t name_capacity = call.load<uint32_t>(call[3]);
const auto narrow = wide ? std::string() : encode_ansi(name, 1252);
const size_t length = wide ? name.size() : narrow.size();
if (name_capacity <= length) {
call.ret(error_more_data);
return;
}
if (call[4]) call.ret(error_invalid_parameter);
if (call[5]) call.store<uint32_t>(call[5], type);
if (call[6]) {
if (!call[7]) {
call.ret(error_invalid_parameter);
return;
}
const uint32_t capacity = call.load<uint32_t>(call[7]);
call.store<uint32_t>(call[7], uint32_t(data.size()));
if (data.size() > capacity) {
call.ret(error_more_data);
return;
}
call.write(call[6], data);
} else if (call[7]) {
call.store<uint32_t>(call[7], uint32_t(data.size()));
}
if (wide) call.put_wide(call[2], name, name_capacity);
else call.put_narrow(call[2], narrow, name_capacity);
call.store<uint32_t>(call[3], uint32_t(length));
call.ret(0);
});
}
// The native registry calls name keys by object path: \Registry\Machine, or \Registry\User\<account> for the current user, then the key path;
// a key handle in the attributes makes the path relative to it.
constexpr uint32_t status_not_found = 0xc0000034U, status_denied = 0xc0000022U, status_invalid = 0xc000000dU, status_failure = 0xc0000001U,
status_overflow = 0x80000005U, status_too_small = 0xc0000023U;
const auto status_of = [](uint32_t win32) -> uint32_t {
switch (win32) {
case 0: return 0;
case 2: case 3: return status_not_found;
case 5: return status_denied;
case 87: return status_invalid;
default: return status_failure;
}
};
struct ObjectKey {
uint64_t root = 0;
std::u16string path;
};
const auto object_key = [handle_width](ApiCall &call, uint64_t attributes) -> std::optional<ObjectKey> {
if (!attributes)
return std::nullopt;
const auto root = call.load_pointer(attributes + 8);
const auto name = call.load_pointer(attributes + 8 + handle_width);
if (!name)
return std::nullopt;
const auto length = call.load<uint16_t>(name);
const auto buffer = call.load_pointer(name + (handle_width == 4 ? 4 : 8));
std::u16string text;
if (length) {
const auto bytes = call.read(buffer, length);
text.resize(length / 2);
std::memcpy(text.data(), bytes.data(), text.size() * 2);
}
ObjectKey result{root, text};
if (root)
return result;
const auto fold = [](std::u16string value) {
for (auto &character : value)
if (character >= u'a' && character <= u'z')
character = char16_t(character - (u'a' - u'A'));
return value;
};
auto rest = text;
const auto strip = [&](std::u16string_view prefix) {
if (fold(rest.substr(0, prefix.size())) != prefix || (rest.size() > prefix.size() && rest[prefix.size()] != u'\\'))
return false;
rest.erase(0, std::min(rest.size(), prefix.size() + 1));
return true;
};
if (strip(u"\\REGISTRY\\MACHINE")) {
result.root = 0x80000002ULL;
} else if (strip(u"\\REGISTRY\\USER")) {
result.root = 0x80000001ULL;
// The first component names the account; the current one is the only one.
const auto end = rest.find(u'\\');
rest = end == rest.npos ? std::u16string() : rest.substr(end + 1);
} else {
return std::nullopt;
}
result.path = rest;
return result;
};
const auto create_key = [&hive, object_key, put_handle, status_of](ApiCall &call, uint64_t attributes, uint64_t out, uint64_t disposition_out) {
const auto where = object_key(call, attributes);
if (!where || !out) {
call.ret(status_invalid);
return;
}
uint64_t key = 0;
uint32_t disposition = 0;
const auto status = hive.create(where->root, where->path, key, disposition);
if (!status) {
put_handle(call, out, key);
if (disposition_out)
call.store<uint32_t>(disposition_out, disposition);
}
call.ret(status_of(status));
};
registry.add("NtCreateKey", 7, [create_key](ApiCall &call) { create_key(call, call[2], call[0], call[6]); });
const auto open_key = [&hive, object_key, put_handle, status_of](ApiCall &call) {
const auto where = object_key(call, call[2]);
if (!where || !call[0]) {
call.ret(status_invalid);
return;
}
uint64_t key = 0;
const auto status = hive.open(where->root, where->path, key);
if (!status)
put_handle(call, call[0], key);
call.ret(status_of(status));
};
registry.add("NtOpenKey", 3, open_key);
registry.add("NtOpenKeyEx", 4, open_key);
registry.add("NtDeleteKey", 1, [&hive, status_of](ApiCall &call) { call.ret(status_of(hive.remove_key(call[0], u""))); });
const auto name_of = [handle_width](ApiCall &call, uint64_t address) {
if (!address)
return std::u16string();
const auto length = call.load<uint16_t>(address);
const auto buffer = call.load_pointer(address + (handle_width == 4 ? 4 : 8));
std::u16string text(length / 2, u'\0');
if (length)
std::memcpy(text.data(), call.read(buffer, length).data(), length / 2 * 2);
return text;
};
registry.add("NtDeleteValueKey", 2, [&hive, name_of, status_of](ApiCall &call) { call.ret(status_of(hive.remove_value(call[0], name_of(call, call[1])))); });
registry.add("NtSetValueKey", 6, [&hive, name_of, status_of](ApiCall &call) {
const uint32_t size = call.u32(5);
if (!call[4] && size) {
call.ret(status_invalid);
return;
}
call.ret(status_of(hive.set(call[0], name_of(call, call[1]), call.u32(3), size ? call.read(call[4], size) : std::vector<uint8_t>{})));
});
// KEY_VALUE_*_INFORMATION: the type and sizes come first; the basic form carries the name, the full form the name and the data, the partial one the data.
registry.add("NtQueryValueKey", 6, [&hive, name_of, status_of, status_overflow, status_too_small](ApiCall &call) {
const auto name = name_of(call, call[1]);
uint32_t type = 0;
std::vector<uint8_t> data;
const auto status = hive.query(call[0], name, type, data);
if (status) {
call.ret(status_of(status));
return;
}
const uint32_t information = call.u32(2), capacity = call.u32(4);
std::vector<uint8_t> out;
const auto put32 = [&](uint32_t value) { for (int shift = 0; shift < 32; shift += 8) out.push_back(uint8_t(value >> shift)); };
const auto put_name = [&] { for (const auto unit : name) { out.push_back(uint8_t(unit)); out.push_back(uint8_t(unit >> 8)); } };
if (information == 0) {
put32(0); put32(type); put32(uint32_t(name.size() * 2)); put_name();
} else if (information == 1) {
const uint32_t data_at = uint32_t(20 + name.size() * 2 + 3) / 4 * 4;
put32(0); put32(type); put32(data_at); put32(uint32_t(data.size())); put32(uint32_t(name.size() * 2));
put_name();
out.resize(data_at);
out.insert(out.end(), data.begin(), data.end());
} else if (information == 2) {
put32(0); put32(type); put32(uint32_t(data.size()));
out.insert(out.end(), data.begin(), data.end());
} else {
call.ret(status_invalid);
return;
}
if (call[5])
call.store<uint32_t>(call[5], uint32_t(out.size()));
const size_t header = information == 0 ? 12 : information == 1 ? 20 : 12;
if (capacity < header) {
call.ret(status_too_small);
return;
}
call.write(call[3], std::span<const uint8_t>(out.data(), std::min<size_t>(out.size(), capacity)));
call.ret(out.size() > capacity ? status_overflow : 0);
});
// The key path of the current user in the native namespace; the string belongs to the program, which frees it with RtlFreeUnicodeString.
registry.add("RtlFormatCurrentUserKeyPath", 1, [&context](ApiCall &call) {
if (!call[0]) {
call.ret(status_invalid);
return;
}
const std::u16string path = u"\\REGISTRY\\USER\\S-1-5-21-1000-1000-1000-1000";
const auto block = context.allocator.allocate(path.size() * 2 + 2, true);
if (!block) {
call.ret(0xc0000017U);
return;
}
call.write(block, std::span<const uint8_t>(reinterpret_cast<const uint8_t *>(path.data()), path.size() * 2));
const size_t width = call.pointer_width();
std::vector<uint8_t> text(width == 4 ? 8 : 16);
const uint16_t length = uint16_t(path.size() * 2), maximum = uint16_t(length + 2);
std::memcpy(text.data(), &length, 2);
std::memcpy(text.data() + 2, &maximum, 2);
std::memcpy(text.data() + (width == 4 ? 4 : 8), &block, width);
call.write(call[0], text);
call.ret(0);
});
registry.add("RtlFreeUnicodeString", 1, [&context](ApiCall &call) {
if (!call[0])
return;
const auto buffer = call.load_pointer(call[0] + (call.pointer_width() == 4 ? 4 : 8));
if (buffer)
context.allocator.release(buffer);
});
registry.add("RtlInitUnicodeString", 2, [](ApiCall &call) {
if (!call[0])
return;
size_t length = 0;
if (call[1]) {
const auto text = call.wide(call[1], 32767);
length = text.size() * 2;
}
const size_t width = call.pointer_width();
std::vector<uint8_t> text(width == 4 ? 8 : 16);
const uint16_t used = uint16_t(length), maximum = uint16_t(call[1] ? length + 2 : 0);
std::memcpy(text.data(), &used, 2);
std::memcpy(text.data() + 2, &maximum, 2);
const uint64_t source = call[1];
std::memcpy(text.data() + (width == 4 ? 4 : 8), &source, width);
call.write(call[0], text);
});
registry.add("RtlGetLastNtStatus", 0, [](ApiCall &call) { call.ret(0); });
registry.add("RtlNtStatusToDosError", 1, [](ApiCall &call) {
switch (call.u32(0)) {
case 0: call.ret(0); break;
case 0xc0000034U: case 0xc000003aU: call.ret(2); break;
case 0xc0000022U: call.ret(5); break;
case 0xc000000dU: call.ret(87); break;
case 0xc0000017U: call.ret(8); break;
case 0xc0000023U: call.ret(122); break;
case 0x80000005U: call.ret(234); break;
case 0xc0000008U: call.ret(6); break;
default: call.ret(317); break;
}
});
registry.add("RegDeleteTreeW", 2, [&hive, text_of, view](ApiCall &call) {
call.ret(hive.remove_tree(call[0], call[1] ? view(call[0], text_of(call, call[1], true)) : std::u16string()));
});
registry.add("RegDeleteTreeA", 2, [&hive, text_of, view](ApiCall &call) {
call.ret(hive.remove_tree(call[0], call[1] ? view(call[0], text_of(call, call[1], false)) : std::u16string()));
});
registry.add("RegDeleteKeyExW", 4, [&hive, text_of, view](ApiCall &call) { call.ret(hive.remove_key(call[0], view(call[0], text_of(call, call[1], true), call.u32(2)))); });
registry.add("RegCloseKey", 1, [&hive, &context](ApiCall &call) {
const auto status = hive.close(call[0]);
context.objects.release_reserved(call[0]);
call.ret(status);
});
registry.add("RegFlushKey", 1, [](ApiCall &call) { call.ret(0); });
registry.add("RegNotifyChangeKeyValue", 5, [](ApiCall &call) { call.ret(0); });
registry.add("RegOpenCurrentUser", 2, [put_handle](ApiCall &call) {
put_handle(call, call[1], 0x80000001ULL);
call.ret(0);
});
// The counts are the same in characters for the wide and the ANSI form; the class of a key is always empty here.
const auto query_info = [&hive](ApiCall &call) {
NativeRegistry::Info info;
const auto status = hive.info(call[0], info);
if (status) {
call.ret(status);
return;
}
if (call[2]) call.store<uint32_t>(call[2], 0);
if (call[4]) call.store<uint32_t>(call[4], info.subkeys);
if (call[5]) call.store<uint32_t>(call[5], info.longest_subkey);
if (call[6]) call.store<uint32_t>(call[6], 0);
if (call[7]) call.store<uint32_t>(call[7], info.values);
if (call[8]) call.store<uint32_t>(call[8], info.longest_name);
if (call[9]) call.store<uint32_t>(call[9], info.longest_data);
if (call[10]) call.store<uint32_t>(call[10], 0);
if (call[11]) call.store<uint64_t>(call[11], 0);
call.ret(0);
};
registry.add("RegQueryInfoKeyW", 12, query_info);
registry.add("RegQueryInfoKeyA", 12, query_info);
// RegGetValue filters by type (RRF_RT_*), opens the subkey itself and expands REG_EXPAND_SZ unless told not to.
registry.add("RegGetValueW", 7, [&hive, &context, text_of](ApiCall &call) {
const uint32_t flags = call.u32(3);
uint64_t key = call[0];
uint64_t opened = 0;
const auto subkey = text_of(call, call[1], true);
if (!subkey.empty()) {
const auto status = hive.open(key, subkey, opened);
if (status) {
call.ret(status);
return;
}
key = opened;
}
uint32_t type = 0;
std::vector<uint8_t> data;
auto status = hive.query(key, text_of(call, call[2], true), type, data);
if (opened) {
hive.close(opened);
context.objects.release_reserved(opened);
}
if (status) {
call.ret(status);
return;
}
const auto allowed = [&](uint32_t value) {
switch (value) {
case type_sz: return flags & 0x2; case type_expand_sz: return flags & 0x4; case type_multi_sz: return flags & 0x20;
case type_dword: return flags & 0x10; case type_qword: return flags & 0x40; case type_binary: return flags & 0x8;
default: return 0U;
}
};
if ((flags & 0xffff) && !allowed(type) && !(flags & 1)) {
call.ret(1629);
return;
}
if (call[4]) call.store<uint32_t>(call[4], type);
if (!call[5]) {
if (call[6]) call.store<uint32_t>(call[6], uint32_t(data.size()));
call.ret(0);
return;
}
const uint32_t capacity = call.load<uint32_t>(call[6]);
call.store<uint32_t>(call[6], uint32_t(data.size()));
if (data.size() > capacity) {
call.ret(error_more_data);
return;
}
call.write(call[5], data);
call.ret(0);
});
(void)error_no_more_items;
(void)error_file_not_found;
}
}