KasperskyOS Community Edition 1.1

Содержание

Разработка под KasperskyOS

В этом разделе справки

Запуск процессов

Файловые системы и сеть

IPC и транспорт

В начало
[Topic developing][Topic sc_application_start]

Обзор: Einit и init.yaml

Инициализирующая программа Einit

При старте ядро KasperskyOS находит в образе решения и запускает исполняемый файл с именем Einit (инициализирующая программа). Запущенный процесс имеет класс Einit и, как правило, используется для запуска остальных процессов, которые требуются в момент старта решения.

Генерация C-кода инициализирующей программы

В составе пакета инструментов KasperskyOS Community Edition поставляется утилита einit, которая позволяет сгенерировать C-код инициализирующей программы на основе init-описания (файл с описанием обычно имеет имя init.yaml). Полученная программа использует KasperskyOS API для выполнения следующих действий:

  • статическое создание и запуск процессов;
  • статическое создание IPC-каналов.

Стандартным способом использования утилиты einit является интеграция ее вызова в один из шагов сборочного скрипта, в результате которого утилита einit на основе файла init.yaml сгенерирует файл einit.c, содержащий код инициализирующей программы. На одном из следующих шагов сборочного скрипта необходимо скомпилировать файл einit.c в исполняемый файл Einit и включить в образ решения.

Для инициализирующей программы не требуется создавать файлы статических описаний. Эти файлы поставляются в составе пакета инструментов KasperskyOS Community Edition и автоматически подключаются при сборке решения. Однако класс процессов Einit должен быть описан в файле security.psl.

Синтаксис init.yaml

Init-описание содержит данные в формате YAML, которые идентифицируют:

  • процессы, запускаемые при загрузке KasperskyOS;
  • IPC-каналы, используемые процессами для взаимодействия между собой.

Эти данные представляют собой словарь с ключом entities, содержащий список словарей процессов. Ключи словаря процесса приведены в таблице ниже.

Ключи словаря процесса в init-описании

Ключ

Обязательный

Значение

name

Да

Класс безопасности процесса

task

Нет

Имя процесса. Если его не указывать, то будет взято имя класса безопасности. У каждого процесса должно быть уникальное имя.

Можно запустить несколько процессов одного класса безопасности, но с разными именами.

path

Нет

Имя исполняемого файла в ROMFS (в образе решения), из которого будет запущен процесс. Если его не указывать, то будет взято имя класса безопасности без "префиксов" и точек. Например, процессы классов безопасности Client и net.Client, для которых не указано имя исполняемого файла, будут запущены из файла Client.

Можно запустить несколько процессов из одного исполняемого файла.

connections

Нет

Список словарей IPC-каналов процесса. Этот список задает статически создаваемые IPC-каналы, клиентскими дескрипторами которых будет владеть процесс. По умолчанию список пуст. (Помимо статически создаваемых IPC-каналов процессы могут использовать динамически создаваемые IPC-каналы.)

args

Нет

Список аргументов, передаваемых процессу (функции main()). Максимальный размер одного элемента списка – 1024 байта.

env

Нет

Словарь переменных окружения, передаваемых процессу. Ключами в этом словаре являются имена переменных, которым сопоставлены передаваемые значения. Максимальный размер значения – 1024 байта.

Ключи словаря IPC-канала процесса приведены в таблице ниже.

Ключи словаря IPC-канала в init-описании

Ключ

Обязательный

Значение

id

Да

Имя IPC-канала, которое может быть задано как конкретным значением, так и ссылкой вида

{var: <имя константы>, include: <путь к заголовочному файлу>}.

target

Да

Имя процесса, который будет владеть серверным дескриптором IPC-канала.

В начало
[Topic einit_overview]

Примеры init-описаний

Здесь собраны init-описания, демонстрирующие различные аспекты запуска процессов.

В примерах в составе KasperskyOS Community Edition может использоваться формат init-описания с макросами (init.yaml.in).

Файл с init-описанием обычно называется init.yaml, хотя может иметь любое имя.

Соединение и запуск процесса-клиента и процесса-сервера

В следующем примере будут запущены два процесса: класса Client и класса Server. Имена процессов не указаны, поэтому они будут совпадать с именами классов процессов. Имена исполняемых файлов также не указаны, они также будут совпадать с именами классов. Процессы будут соединены IPC-каналом с именем server_connection.

init.yaml

entities:

- name: Client

connections:

- target: Server

id: server_connection

- name: Server

Указание исполняемого файла для запуска

В следующем примере будут запущены: процесс класса Client из исполняемого файла cl, процесс класса ClientServer из исполняемого файла csr и процесс класса MainServer из исполняемого файла msr. Имена процессов не указаны, поэтому они будут совпадать с именами классов процессов.

init.yaml

entities:

- name: Client

path: cl

- name: ClientServer

path: csr

- name: MainServer

path: msr

Запуск двух процессов из одного исполняемого файла

В следующем примере будут запущены три процесса: процесс класса Client из исполняемого файла по умолчанию (Client), а также процессы классов MainServer и BkServer из исполняемого файла srv. Имена процессов не указаны, поэтому они будут совпадать с именами классов процессов.

init.yaml

entities:

- name: Client

- name: MainServer

path: srv

- name: BkServer

path: srv

Запуск двух процессов одного класса

В следующем примере будут запущены: один процесс класса Client (с именем по умолчанию – Client) и два процесса класса Server с именами UserServer и PrivilegedServer. Клиентский процесс связан с серверными процессами IPC-каналами с именами server_connection_us и server_connection_ps, соответственно. Имена исполняемых файлов не указаны, поэтому они будут совпадать с именами классов процессов.

init.yaml

entities:

- name: Client

connections:

- id: server_connection_us

target: UserServer

- id: server_connection_ps

target: PrivilegedServer

- task: UserServer

name: Server

- task: PrivilegedServer

name: Server

Передача переменных окружения и аргументов функции main()

В следующем примере будут запущены два процесса: один класса VfsFirst (с именем по умолчанию – VfsFirst) и второй класса VfsSecond (с именем по умолчанию – VfsSecond). Первый процесс при запуске получит аргумент -f /etc/fstab, а также переменные окружения: ROOTFS со значением ramdisk0,0 / ext2 0 и UNMAP_ROMFS со значением 1. Второй процесс при запуске получит аргумент -l devfs /dev devfs 0.

Имена исполняемых файлов не указаны, поэтому они будут совпадать с именами классов процессов.

Если в решении используется программа Env, то передаваемые через нее аргументы и переменные окружения переопределяют значения, заданные через init.yaml.

init.yaml

entities:

- name: VfsFirst

args:

- -f

- /etc/fstab

env:

ROOTFS: ramdisk0,0 / ext2 0

UNMAP_ROMFS: 1

- name: VfsSecond

args:

- -l

- devfs /dev devfs 0

В начало
[Topic using_einit]

Запуск процесса с помощью KasperskyOS API

В этом примере: использование функций EntityInitEx() и EntityRun() для запуска исполняемого файла из образа решения.

Ниже приводится код функции GpMgrOpenSession(), выполняющей запуск серверного процесса, соединение его с клиентским процессом и инициализацию IPC-транспорта. Исполняемый файл нового процесса должен содержаться в ROMFS-хранилище решения.

#define CONNECT_RETRY 150 /* Количество попыток соединения */

#define CONNECT_DELAY 10 /* Задержка в мс между попытками */

/**

* Параметр classname задает имя класса запускаемого процесса,

* параметр server задает уникальное имя процесса, а параметр service содержит имя сервиса,

* используемое при динамическом создании канала.

* Выходной параметр transport содержит инициализированный транспорт,

* если IPC-канал до клиента успешно создан.

*/

Retcode GpMgrOpenSession(const char *classname, const char *server,

const char *service, NkKosTransport *transport)

{

Retcode rc;

Entity *e;

EntityInfo tae_info;

Handle endpoint;

rtl_uint32_t riid;

int count = CONNECT_RETRY;

/* Инициализация структуры описания процесса. */

rtl_memset(&tae_info, 0, sizeof(tae_info));

tae_info.eiid = classname;

tae_info.args[0] = server;

tae_info.args[1] = service;

/* Создание процесса с описанием tae_info и именем server.

* Поскольку третий параметр равен RTL_NULL, имя запускаемого

* бинарного файла совпадает с именем класса из описания tae_info.

* Созданный процесс находится в остановленном состоянии. */

if ((e = EntityInitEx(&tae_info, server, RTL_NULL)) == NK_NULL)

{

rtl_printf("Cannot init entity '%s'\n", tae_info.eiid);

return rcFail;

}

/* Запуск процесса. */

if ((rc = EntityRun(e)) != rcOk)

{

rtl_printf("Cannot launch entity %" RTL_PRId32 "\n", rc);

EntityFree(e);

return rc;

}

/* Динамическое создание IPC-канала. */

while ((rc = KnCmConnect(server, service, INFINITE_TIMEOUT, &endpoint, &riid) ==

rcResourceNotFound && count--)

{

KnSleep(CONNECT_DELAY);

}

if (rc != rcOk)

{

rtl_printf("Cannot connect to server %" RTL_PRId32 "\n", rc);

return rc;

}

/* Инициализация IPC-транспорта. */

NkKosTransport_Init(transport, endpoint, NK_NULL, 0);

...

return rcOk;

}

Чтобы процесс мог запускать другие процессы, политика безопасности решения должна разрешать ему использование следующих служб ядра: Handle, Task и VMM (их описания находятся в директории kl\core\).

В начало
[Topic app_static_start]

Обзор: программа Env

Служебная программа Env предназначена для передачи аргументов и переменных окружения запускаемым процессам. При запуске каждый процесс автоматически отправляет запрос процессу Env и получает необходимые данные.

Обращение процесса к Env переопределяет аргументы и переменные окружения, полученные через Einit.

Чтобы использовать программу Env в своем решении, необходимо:

1. Разработать код программы Env, используя макросы из env/env.h.

2. Собрать бинарный файл программы Env, скомпоновав ее с библиотекой env_server.

3. В init-описании указать, что необходимо запустить процесс Env и соединить с ней выбранные процессы (Env при этом является сервером). Имя канала задается макросом ENV_SERVICE_NAME, объявленным в файле env/env.h.

4. Включить бинарный файл Env в образ решения.

Код программы Env

В коде программы Env используются следующие макросы и функции, объявленные в файле env/env.h:

  • ENV_REGISTER_ARGS(name,argarr) – передать процессу с именем name аргументы из массива argarr (максимальный размер одного элемента – 256 байтов);
  • ENV_REGISTER_VARS(name,envarr) – передать процессу с именем name переменные окружения из массива envarr (максимальный размер одного элемента – 256 байтов);
  • ENV_REGISTER_PROGRAM_ENVIRONMENT(name,argarr,envarr) – передать процессу с именем name как аргументы, так и переменные окружения;
  • envServerRun() – инициализировать серверную часть программы Env, чтобы она могла отвечать на запросы.

Примеры использования Env

В начало
[Topic env_overview]

Передача переменных окружения и аргументов с помощью Env

Пример передачи аргументов при запуске процесса

Ниже приводится код программы Env, которая при запуске процесса с именем NetVfs передаст ему три аргумента: NetVfs, -l devfs /dev devfs 0 и -l romfs /etc romfs 0:

env.c

#include <env/env.h>

#include <stdlib.h>

int main(int argc, char** argv)

{

const char* NetVfsArgs[] = {

"-l", "devfs /dev devfs 0",

"-l", "romfs /etc romfs 0"

};

ENV_REGISTER_ARGS("NetVfs", NetVfsArgs);

envServerRun();

return EXIT_SUCCESS;

}

Пример передачи переменных окружения при запуске процесса

Ниже приводится код программы Env, которая при запуске процесса с именем Vfs3 передаст ему две переменных окружения: ROOTFS=ramdisk0,0 / ext2 0 и UNMAP_ROMFS=1:

env.c

#include <env/env.h>

#include <stdlib.h>

int main(int argc, char** argv)

{

const char* Vfs3Envs[] = {

"ROOTFS=ramdisk0,0 / ext2 0",

"UNMAP_ROMFS=1"

};

ENV_REGISTER_VARS("Vfs3", Vfs3Envs);

envServerRun();

return EXIT_SUCCESS;

}

В начало
[Topic using_env_app][Topic sc_filesystems_and_net]

Состав компонента VFS

Компонент VFS содержит набор исполняемых файлов, библиотек и файлов описаний, позволяющих использовать файловые системы и/или сетевой стек, вынесенные в отдельный процесс VFS (Virtual File System – виртуальная файловая система). При необходимости можно собрать собственные реализации VFS.

Библиотеки VFS

CMake-пакет vfs содержит следующие библиотеки:

  • vfs_fs – содержит реализации defvs, ramfs и romfs, а также позволяет добавить в VFS реализации других файловых систем;
  • vfs_net – содержит реализацию defvs и сетевого стека;
  • vfs_imp – содержит в себе сумму компонентов vfs_fs и vfs_net;
  • vfs_remote – клиентская транспортная библиотека; преобразует локальные вызовы в IPC-запросы к VFS и принимает IPC-ответы;
  • vfs_server – серверная транспортная библиотека VFS; принимает IPC-запросы, преобразует их в локальные вызовы и отправляет IPC-ответы;
  • vfs_local – используется для статической компоновки клиента с библиотеками VFS.

Исполняемые файлы VFS

CMake-пакет precompiled_vfs содержит следующие исполняемые файлы:

  • VfsRamFs
  • VfsSdCardFs
  • VfsNet

Исполняемые файлы VfsRamFs и VfsSdCardFs включают в себя библиотеки vfs_server, vfs_fs, vfat и lwext4. Исполняемый файл VfsNet включает в себя библиотеки vfs_server, vfs_imp и dnet_imp.

Каждый из этих исполняемых файлов имеет собственные значения аргументов и переменных окружения по умолчанию.

При необходимости можно самостоятельно собрать исполняемый файл VFS с нужной функциональностью.

Файлы описаний VFS

В директории /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/kl/ находятся следующие файлы VFS:

  • VfsRamFs.edl, VfsSdCardFs.edl, VfsNet.edl и VfsEntity.edl и сгенерированные из них заголовочные файлы с транспортным кодом;
  • Vfs.cdl и сгенерированный Vfs.cdl.h;
  • Vfs*.idl и сгенерированные из них заголовочные файлы с транспортным кодом.

В начало
[Topic vfs_overview]

Создание IPC-канала до VFS

Рассмотрим программу Client, использующую файловые системы и сокеты Беркли. Для обработки ее вызовов запустим один процесс VFS (с именем VfsFsnet). В этот процесс будут направляться как "сетевые", так и "файловые" вызовы. Такой подход используется в тех случаях, когда не требуется разделение "файловых" и "сетевых" информационных потоков.

Чтобы взаимодействие процессов Client и VfsFsnet было корректным, имя IPC-канала между ними должно задаваться макросом _VFS_CONNECTION_ID, объявленным в файле vfs/defs.h.

Ниже приводится фрагмент init‑описания для соединения процессов Client и VfsFsnet.

init.yaml

- name: Client

connections:

- target: VfsFsnet

id: {var: _VFS_CONNECTION_ID, include: vfs/defs.h}

- name: VfsFsnet

В начало
[Topic client_and_vfs_ipc_channel]

Сборка исполняемого файла VFS

При сборке исполняемого файла VFS можно включить в него именно ту функциональность, которая требуется, например:

  • реализацию той или иной файловый системы;
  • сетевой стек;
  • сетевой драйвер.

Например, при разделении файловых и сетевых вызовов понадобится сборка "файловой версии" и "сетевой версии" VFS. В некоторых случаях в VFS потребуется включить и сетевой стек, и файловые системы ("полная версия" VFS).

Сборка "файловой версии" VFS

Рассмотрим программу VFS, содержащую только реализацию файловой системы lwext4 и не содержащую сетевого стека. Для сборки такого исполняемого файла необходимо файл с функцией main() скомпоновать с библиотеками vfs_server, vfs_fs и lwext4:

CMakeLists.txt

project (vfsfs)

include (platform/nk)

# Установка флагов компиляции

project_header_default ("STANDARD_GNU_11:YES" "STRICT_WARNINGS:NO")

add_executable (VfsFs "src/vfs.c")

# Компоновка с библиотеками VFS

target_link_libraries (VfsFs

${vfs_SERVER_LIB}

${LWEXT4_LIB}

${vfs_FS_LIB})

# Подготовка VFS для соединения с процессом ramdisk-драйвера

set_target_properties (VfsFs PROPERTIES ${blkdev_ENTITY}_REPLACEMENT ${ramdisk_ENTITY})

Драйвер блочного устройства не может быть скомпонован с VFS, поэтому всегда должен быть запущен как отдельный процесс.

Взаимодействие трех процессов: клиента, "файловой версии" VFS и драйвера блочного устройства

Сборка "сетевой версии" VFS совместно с сетевым драйвером

Рассмотрим программу VFS, содержащую сетевой стек с драйвером и не содержащую реализаций файловых систем. Для сборки такого исполняемого файла необходимо файл с функцией main() скомпоновать с библиотеками vfs_server, vfs_implementation и dnet_implementation.

CMakeLists.txt

project (vfsnet)

include (platform/nk)

# Установка флагов компиляции

project_header_default ("STANDARD_GNU_11:YES" "STRICT_WARNINGS:NO")

add_executable (VfsNet "src/vfs.c")

# Компоновка с библиотеками VFS

target_link_libraries (VfsNet

${vfs_SERVER_LIB}

${vfs_IMPLEMENTATION_LIB}

${dnet_IMPLEMENTATION_LIB})

# Отключение драйвера блочного устройства

set_target_properties (VfsNet PROPERTIES ${blkdev_ENTITY}_REPLACEMENT "")

Библиотека dnet_implementation уже включает в себя сетевой драйвер, поэтому запуск отдельного процесса драйвера не требуется.

Взаимодействие процесса Client с процессом "сетевой версии" VFS

Сборка "сетевой версии" VFS с отдельным сетевым драйвером

Еще один вариант сборки "сетевой версии" VFS – без сетевого драйвера. Сам сетевой драйвер необходимо будет запустить как отдельный процесс. Взаимодействие с драйвером происходит через IPC с помощью библиотеки dnet_client.

Таким образом, необходимо файл с функцией main() скомпоновать с библиотеками vfs_server, vfs_implementation и dnet_client.

CMakeLists.txt

project (vfsnet)

include (platform/nk)

# Установка флагов компиляции

project_header_default ("STANDARD_GNU_11:YES" "STRICT_WARNINGS:NO")

add_executable (VfsNet "src/vfs.c")

# Компоновка с библиотеками VFS

target_link_libraries (VfsNet

${vfs_SERVER_LIB}

${vfs_IMPLEMENTATION_LIB}

${dnet_CLIENT_LIB})

# Отключение драйвера блочного устройства

set_target_properties (VfsNet PROPERTIES ${blkdev_ENTITY}_REPLACEMENT "")

Взаимодействие трех процессов: клиента, "сетевой версии" VFS и сетевого драйвера

Сборка "полной версии" VFS

Если в VFS требуется включить как сетевой стек, так и реализации файловых систем, то при сборке следует использовать библиотеки vfs_server, vfs_implementation, dnet_implementation (или dnet_client – в случае отдельного сетевого драйвера), а также библиотеки реализации файловых систем.

В начало
[Topic vfs_app_build]

Объединение клиента и VFS в один исполняемый файл

Рассмотрим программу Client, использующую сокеты Беркли. Вызовы, которые выполняет Client, должны направляться в VFS. Обычный путь состоит в запуске отдельного процесса VFS и создании IPC-канала. Однако вместо этого можно интегрировать функциональность VFS в сам исполняемый файл Client. Для этого нужно при сборке исполняемого файла Client скомпоновать его с библиотекой vfs_local, которая будет принимать вызовы, а также с библиотеками реализации – vfs_implementation и dnet_implementation.

Локальную компоновку с VFS удобно использовать при отладке. Кроме того, вызовы для работы с сетью могут обрабатываться намного быстрее за счет исключения IPC-вызовов. Тем не менее, изоляция VFS в отдельном процессе и IPC-взаимодействие с ним рекомендуется во всех случаях как более безопасный подход.

Ниже приведен сборочный скрипт исполняемого файла Client.

CMakeLists.txt

project (client)

include (platform/nk)

# Установка флагов компиляции

project_header_default ("STANDARD_GNU_11:YES" "STRICT_WARNINGS:NO")

# Генерация файла Client.edl.h

nk_build_edl_files (client_edl_files NK_MODULE "client" EDL "${CMAKE_SOURCE_DIR}/resources/edl/Client.edl")

add_executable (Client "src/client.c")

add_dependencies (Client client_edl_files)

# Компоновка с библиотеками VFS

target_link_libraries (Client ${vfs_LOCAL_LIB} ${vfs_IMPLEMENTATION_LIB} ${dnet_IMPLEMENTATION_LIB}

Если Client использует файловые системы, то помимо vfs_local, его нужно скомпоновать с библиотекой vfs_fs и реализацией используемой файловой системы. Кроме того, нужно добавить в решение драйвер блочного устройства.

В начало
[Topic client_and_vfs_linked]

Обзор: аргументы и переменные окружения VFS

Аргументы VFS

  • -l <запись в формате fstab>

    Аргумент -l позволяет монтировать файловую систему.

  • -f <путь к файлу fstab>

    Аргумент -f позволяет передать файл с записями в формате fstab для монтирования файловых систем. Файл будет искаться в ROMFS-хранилище. Если переменная UMNAP_ROMFS определена, то файл будет искаться на файловой системе, смонтированной с помощью переменной ROOTFS.

Пример использования аргументов -l и -f

Переменные окружения VFS

  • UNMAP_ROMFS

    Если переменная UNMAP_ROMFS определена, то ROMFS-хранилище будет удалено. Это позволяет сэкономить память и изменить поведение при использовании аргумента -f.

  • ROOTFS = <запись в формате fstab>

    Переменная ROOTFS позволяет монтировать файловую систему в корневой каталог. В комбинации с переменной UNMAP_ROMFS и аргументом -f позволяет искать fstab-файл на монтированной файловой системе, а не в ROMFS-хранилище. Пример использования ROOTFS

  • VFS_CLIENT_MAX_THREADS

    Переменная окружения VFS_CLIENT_MAX_THREADS позволяет в момент запуска VFS переопределить параметр конфигурирования SDK VFS_CLIENT_MAX_THREADS.

  • _VFS_NETWORK_BACKEND=<имя бэкенда>:<имя IPC-канала до VFS>

Переменная _VFS_NETWORK_BACKEND задает используемый для "сетевых" вызовов бэкенд. Можно указать имя стандартного бэкенда: client, server или local, а также имя пользовательского бэкенда. Если используется бэкенд local, то имя IPC-канала не указывается (_VFS_NETWORK_BACKEND=local:). Может быть указано два и больше IPC-канала через запятую.

  • _VFS_FILESYSTEM_BACKEND=<имя бэкенда>:<имя IPC-канала до VFS>

    Переменная _VFS_FILESYSTEM_BACKEND задает используемый для "файловых" вызовов бэкенд. Имя бэкенда и имя IPC-канала до VFS задаются так же, как и для переменной _VFS_NETWORK_BACKEND.

Значения по умолчанию

Для исполняемого файла VfsRamFs:

ROOTFS = ramdisk0,0 / ext4 0

VFS_FILESYSTEM_BACKEND = server:kl.VfsRamFs

Для исполняемого файла VfsSdCardFs:

ROOTFS = mmc0,0 / fat32 0

VFS_FILESYSTEM_BACKEND = server:kl.VfsSdCardFs

-l nodev /tmp ramfs 0

-l nodev /var ramfs 0

Для исполняемого файла VfsNet:

VFS_NETWORK_BACKEND = server:kl.VfsNet

VFS_FILESYSTEM_BACKEND = server:kl.VfsNet

-l devfs /dev devfs 0

В начало
[Topic vfs_args_and_envs_overview]

Монтирование файловой системы при старте

При запуске процесса VFS по умолчанию монтируется только файловая система RAMFS, в корневую директорию. Если требуется монтировать другие файловые системы, это можно сделать не только с помощью вызова mount() после запуска VFS, но и непосредственно при запуске процесса VFS – передав ему нужные аргументы и переменные окружения.

Рассмотрим три примера монтирования файловых систем при запуске VFS. Для передачи аргументов и переменных окружения процессу VFS использована программа Env.

Монтирование с помощью аргумента -l

Простой способ монтировать файловую систему – это передать процессу VFS аргумент -l <запись в формате fstab>.

В этом примере при запуске процесса с именем Vfs1 будут монтированы файловые системы devfs и romfs.

env.c

#include <env/env.h>

#include <stdlib.h>

int main(int argc, char** argv)

{

const char* Vfs1Args[] = {

"-l", "devfs /dev devfs 0",

"-l", "romfs /etc romfs 0"

};

ENV_REGISTER_ARGS("Vfs1", Vfs1Args);

envServerRun();

return EXIT_SUCCESS;

}

Монтирование с помощью fstab из ROMFS

Если при сборке решения добавить fstab-файл, после старта он будет доступен через ROMFS-хранилище. Его можно использовать для монтирования, передав процессу VFS аргумент -f <путь к fstab-файлу>.

В этом примере при запуске процесса с именем Vfs2 будут монтированы файловые системы, заданные через файл fstab, который был добавлен при сборке решения.

env.c

#include <env/env.h>

#include <stdlib.h>

int main(int argc, char** argv)

{

const char* Vfs2Args[] = { "-f", "fstab" };

ENV_REGISTER_ARGS("Vfs2", Vfs2Args);

envServerRun();

return EXIT_SUCCESS;

}

Монтирование с помощью "внешнего" fstab

Пусть fstab-файл находится не в ROMFS-образе решения, а на диске. Чтобы использовать его для монтирования, необходимо передать VFS следующие аргументы и переменные окружения:

  1. ROOTFS. Эта переменная позволяет монтировать в корневую директорию файловую систему, в которой находится fstab-файл.
  2. UNMAP_ROMFS. Если эта переменная определена, ROMFS-хранилище удаляется. В итоге fstab-файл будет искаться на файловой системе, смонтированной с помощью переменной ROOTFS.
  3. -f. Этот аргумент используется, чтобы задать путь к fstab-файлу.

В следующем примере при запуске процесса с именем Vfs3 в корневой каталог будет монтирована файловая система ext2, на которой будет найден файл /etc/fstab для монтирования дополнительных файловых систем. ROMFS-хранилище будет удалено.

env.c

#include <env/env.h>

#include <stdlib.h>

int main(int argc, char** argv)

{

const char* Vfs3Args[] = { "-f", "/etc/fstab" };

const char* Vfs3Envs[] = {

"ROOTFS=ramdisk0,0 / ext2 0",

"UNMAP_ROMFS=1"

};

ENV_REGISTER_PROGRAM_ENVIRONMENT("Vfs3", Vfs3Args, Vfs3Envs);

envServerRun();

return EXIT_SUCCESS;

}

В начало
[Topic mount_on_start]

Разделение файловых и сетевых вызовов с помощью бэкендов VFS

В этом примере: паттерн безопасной разработки, предусматривающий разделение "сетевых" и "файловых" информационных потоков.

Рассмотрим программу Client, использующую файловые системы и сокеты Беркли. Для обработки ее вызовов мы запустим не один, а два отдельных VFS-процесса из исполняемых файлов VfsFirst и VfsSecond. Через переменные окружения мы зададим файловые бэкенды как работающие через канал до VfsFirst, а сетевые бэкенды – как работающие через канал до VfsSecond. Будем использовать стандартные бэкенды client и server. Благодаря этому мы перенаправим "файловые вызовы" Client в VfsFirst, а "сетевые" – в VfsSecond. Чтобы передать процессам переменные окружения, добавим в решение программу Env.

Init-описание решения представлено ниже. Процесс Client будет соединен с процессами VfsFirst и VfsSecond, при этом каждый из трех процессов соединен с процессом Env. Обратите внимание, что имя IPC-канала до процесса Env задается с помощью переменной ENV_SERVICE_NAME.

init.yaml

entities:

- name: Env

- name: Client

connections:

- target: Env

id: {var: ENV_SERVICE_NAME, include: env/env.h}

- target: VfsFirst

id: VFS1

- target: VfsSecond

id: VFS2

- name: VfsFirst

connections:

- target: Env

id: {var: ENV_SERVICE_NAME, include: env/env.h}

- name: VfsSecond

connections:

- target: Env

id: {var: ENV_SERVICE_NAME, include: env/env.h}

Чтобы направить все "файловые" вызовы в VfsFirst, зададим значение переменной окружения _VFS_FILESYSTEM_BACKEND следующим образом:

  • для VfsFirst: _VFS_FILESYSTEM_BACKEND=server:<имя IPC-канала до VfsFirst>;
  • для Client: _VFS_FILESYSTEM_BACKEND=client:<имя IPC-канала до VfsFirst>.

Для направления "сетевых" вызовов в VfsSecond используем аналогичную переменную окружения _VFS_NETWORK_BACKEND:

  • для VfsSecond зададим: _VFS_NETWORK_BACKEND=server:<имя IPC-канала до VfsSecond>;
  • для Client: _VFS_NETWORK_BACKEND=client:<имя IPC-канала до VfsSecond>.

Значение переменных окружения зададим через программу Env, код которой представлен ниже.

env.c

#include <env/env.h>

#include <stdlib.h>

int main(void)

{

const char* vfs_first_envs[] = { "_VFS_FILESYSTEM_BACKEND=server:VFS1" };

ENV_REGISTER_VARS("VfsFirst", vfs_first_envs);

const char* vfs_second_envs[] = { "_VFS_NETWORK_BACKEND=server:VFS2" };

ENV_REGISTER_VARS("VfsSecond", vfs_second_envs);

const char* client_envs[] = { "_VFS_FILESYSTEM_BACKEND=client:VFS1", "_VFS_NETWORK_BACKEND=client:VFS2" };

ENV_REGISTER_VARS("Client", client_envs);

envServerRun();

return EXIT_SUCCESS;

}

В начало
[Topic client_and_two_vfs]

Написание пользовательского бэкенда VFS

В этом примере: изменение логики обработки файловых вызовов с помощью специального бэкенда VFS.

Рассмотрим решение, включающее процессы Client, VfsFirst и VfsSecond. Пусть процесс Client соединен с VfsFirst и VfsSecond с помощью IPC-каналов.

Задача: сделать так, чтобы обращения процесса Client к файловой системе fat32 обрабатывались процессом VfsFirst, а обращения к ext4 обрабатывались VfsSecond. Для решения этой задачи можно использовать механизм бэкендов VFS, причем даже не придется менять код программы Client.

Мы напишем пользовательский бэкенд custom_client, который будет отправлять вызовы по каналу VFS1 или VFS2, в зависимости от того, начинается ли путь к файлу с /mnt1. Для отправки вызовов custom_client будет использовать стандартные бэкенды client, то есть будет являться проксирующим бэкендом.

С помощью аргумента -l мы монтируем fat32 в директорию /mnt1 для процесса VfsFirst и ext4 в /mnt2 для процесса VfsSecond. (Предполагается, что VfsFirst содержит реализацию fat32, а VfsSecond – реализацию ext4.) С помощью переменной окружения _VFS_FILESYSTEM_BACKEND зададим используемые процессами бэкенды (custom_client и server) и IPC-каналы (VFS1 и VFS2).

Наконец, с помощью init-описания зададим имена IPC-каналов: VFS1 и VFS2.

Ниже мы рассмотрим подробнее:

  1. Код бэкенда custom_client.
  2. Компоновка программы Client и бэкенда custom_client.
  3. Код программы Env.
  4. Init-описание.

Написание бэкенда custom_client

Этот файл содержит реализацию проксирующего пользовательского бэкенда, передающего вызовы в один из двух стандартных бэкендов client. Логика выбора бэкенда зависит от используемого пути или от дескриптора файла и управляется дополнительными структурами данных.

backend.c

#include <vfs/vfs.h>

#include <stdio.h>

#include <stdlib.h>

#include <platform/compiler.h>

#include <pthread.h>

#include <errno.h>

#include <string.h>

#include <getopt.h>

#include <assert.h>

/* Код управления файловыми дескрипторами. */

#define MAX_FDS 50

struct entry

{

Handle handle;

bool is_vfat;

};

struct fd_array

{

struct entry entries[MAX_FDS];

int pos;

pthread_rwlock_t lock;

};

struct fd_array fds = { .pos = 0, .lock = PTHREAD_RWLOCK_INITIALIZER };

int insert_entry(Handle fd, bool is_vfat)

{

pthread_rwlock_wrlock(&fds.lock);

if (fds.pos == MAX_FDS)

{

pthread_rwlock_unlock(&fds.lock);

return -1;

}

fds.entries[fds.pos].handle = fd;

fds.entries[fds.pos].is_vfat = is_vfat;

fds.pos++;

pthread_rwlock_unlock(&fds.lock);

return 0;

}

struct entry *find_entry(Handle fd)

{

pthread_rwlock_rdlock(&fds.lock);

for (int i = 0; i < fds.pos; i++)

{

if (fds.entries[i].handle == fd)

{

pthread_rwlock_unlock(&fds.lock);

return &fds.entries[i];

}

}

pthread_rwlock_unlock(&fds.lock);

return NULL;

}

/* Структура пользовательского бэкенда. */

struct context

{

struct vfs wrapper;

pthread_rwlock_t lock;

struct vfs *vfs_vfat;

struct vfs *vfs_ext4;

};

struct context ctx =

{

.wrapper =

{

.dtor = _vfs_backend_dtor,

.disconnect_all_clients = _disconnect_all_clients,

.getstdin = _getstdin,

.getstdout = _getstdout,

.getstderr = _getstderr,

.open = _open,

.read = _read,

.write = _write,

.close = _close,

}

};

/* Реализация методов пользовательского бэкенда. */

static bool is_vfs_vfat_path(const char *path)

{

char vfat_path[5] = "/mnt1";

if (memcmp(vfat_path, path, sizeof(vfat_path)) != 0)

return false;

return true;

}

static void _vfs_backend_dtor(struct vfs *vfs)

{

ctx.vfs_vfat->dtor(ctx.vfs_vfat);

ctx.vfs_ext4->dtor(ctx.vfs_ext4);

}

static void _disconnect_all_clients(struct vfs *self, int *error)

{

(void)self;

(void)error;

ctx.vfs_vfat->disconnect_all_clients(ctx.vfs_vfat, error);

ctx.vfs_ext4->disconnect_all_clients(ctx.vfs_ext4, error);

}

static Handle _getstdin(struct vfs *self, int *error)

{

(void)self;

Handle handle = ctx.vfs_vfat->getstdin(ctx.vfs_vfat, error);

if (handle != INVALID_HANDLE)

{

if (insert_entry(handle, true))

{

*error = ENOMEM;

return INVALID_HANDLE;

}

}

return handle;

}

static Handle _getstdout(struct vfs *self, int *error)

{

(void)self;

Handle handle = ctx.vfs_vfat->getstdout(ctx.vfs_vfat, error);

if (handle != INVALID_HANDLE)

{

if (insert_entry(handle, true))

{

*error = ENOMEM;

return INVALID_HANDLE;

}

}

return handle;

}

static Handle _getstderr(struct vfs *self, int *error)

{

(void)self;

Handle handle = ctx.vfs_vfat->getstderr(ctx.vfs_vfat, error);

if (handle != INVALID_HANDLE)

{

if (insert_entry(handle, true))

{

*error = ENOMEM;

return INVALID_HANDLE;

}

}

return handle;

}

static Handle _open(struct vfs *self, const char *path, int oflag, mode_t mode, int *error)

{

(void)self;

Handle handle;

bool is_vfat = false;

if (is_vfs_vfat_path(path))

{

handle = ctx.vfs_vfat->open(ctx.vfs_vfat, path, oflag, mode, error);

is_vfat = true;

}

else

handle = ctx.vfs_ext4->open(ctx.vfs_ext4, path, oflag, mode, error);

if (handle == INVALID_HANDLE)

return INVALID_HANDLE;

if (insert_entry(handle, is_vfat))

{

if (is_vfat)

ctx.vfs_vfat->close(ctx.vfs_vfat, handle, error);

*error = ENOMEM;

return INVALID_HANDLE;

}

return handle;

}

static ssize_t _read(struct vfs *self, Handle fd, void *buf, size_t count, bool *nodata, int *error)

{

(void)self;

struct entry *found_entry = find_entry(fd);

if (found_entry != NULL && found_entry->is_vfat)

return ctx.vfs_vfat->read(ctx.vfs_vfat, fd, buf, count, nodata, error);

return ctx.vfs_ext4->read(ctx.vfs_ext4, fd, buf, count, nodata, error);

}

static ssize_t _write(struct vfs *self, Handle fd, const void *buf, size_t count, int *error)

{

(void)self;

struct entry *found_entry = find_entry(fd);

if (found_entry != NULL && found_entry->is_vfat)

return ctx.vfs_vfat->write(ctx.vfs_vfat, fd, buf, count, error);

return ctx.vfs_ext4->write(ctx.vfs_ext4, fd, buf, count, error);

}

static int _close(struct vfs *self, Handle fd, int *error)

{

(void)self;

struct entry *found_entry = find_entry(fd);

if (found_entry != NULL && found_entry->is_vfat)

return ctx.vfs_vfat->close(ctx.vfs_vfat, fd, error);

return ctx.vfs_ext4->close(ctx.vfs_ext4, fd, error);

}

/* Конструктор пользовательского бэкенда. ctx.vfs_vfat и ctx.vfs_ext4 инициализируются

* как стандартные бэкенды с именем "client". */

static struct vfs *_vfs_backend_create(Handle client_id, const char *config, int *error)

{

(void)config;

ctx.vfs_vfat = _vfs_init("client", client_id, "VFS1", error);

assert(ctx.vfs_vfat != NULL && "Can't initilize client backend!");

assert(ctx.vfs_vfat->dtor != NULL && "VFS FS backend has not set the destructor!");

ctx.vfs_ext4 = _vfs_init("client", client_id, "VFS2", error);

assert(ctx.vfs_ext4 != NULL && "Can't initilize client backend!");

assert(ctx.vfs_ext4->dtor != NULL && "VFS FS backend has not set the destructor!");

return &ctx.wrapper;

}

/* Регистрация пользовательского бэкенда под именем custom_client. */

static void _vfs_backend(create_vfs_backend_t *ctor, const char **name)

{

*ctor = &_vfs_backend_create;

*name = "custom_client";

}

REGISTER_VFS_BACKEND(_vfs_backend)

Компоновка программы Client и бэкенда custom_client

Написанный бэкенд скомпилируем в библиотеку:

CMakeLists.txt

add_library (backend_client STATIC "src/backend.c")

Готовую библиотеку backend_client скомпонуем с программой Client:

CMakeLists.txt (фрагмент)

add_dependencies (Client vfs_backend_client backend_client)

target_link_libraries (Client

pthread

${vfs_CLIENT_LIB}

"-Wl,--whole-archive" backend_client "-Wl,--no-whole-archive" backend_client

)

Написание программы Env

Чтобы передать процессам аргументы и переменные окружения, используем программу Env.

env.c

#include <env/env.h>

#include <stdlib.h>

int main(int argc, char** argv)

{

/* Монтирование fat32 в /mnt1 для процесса VfsFirst и ext4 в /mnt2 для процесса VfsSecond. */

const char* VfsFirstArgs[] = {

"-l", "ahci0 /mnt1 fat32 0"

};

ENV_REGISTER_ARGS("VfsFirst", VfsFirstArgs);

const char* VfsSecondArgs[] = {

"-l", "ahci1 /mnt2 ext4 0"

};

ENV_REGISTER_ARGS("VfsSecond", VfsSecondArgs);

/* Задание файловых бэкендов. */

const char* vfs_first_args[] = { "_VFS_FILESYSTEM_BACKEND=server:VFS1" };

ENV_REGISTER_VARS("VfsFirst", vfs_first_args);

const char* vfs_second_args[] = { "_VFS_FILESYSTEM_BACKEND=server:VFS2" };

ENV_REGISTER_VARS("VfsSecond", vfs_second_args);

const char* client_fs_envs[] = { "_VFS_FILESYSTEM_BACKEND=custom_client:VFS1,VFS2" };

ENV_REGISTER_VARS("Client", client_fs_envs);

envServerRun();

return EXIT_SUCCESS;

}

Изменение init.yaml

Для IPC-каналов, соединяющих процесс Client с процессами VfsFirst и VfsSecond, необходимо задать те же имена, которые мы указали в переменной окружения _VFS_FILESYSTEM_BACKEND: VFS1 и VFS2.

init.yaml

entities:

- name: vfs_backend.Env

- name: vfs_backend.Client

connections:

- target: vfs_backend.Env

id: {var: ENV_SERVICE_NAME, include: env/env.h}

- target: vfs_backend.VfsFirst

id: VFS1

- target: vfs_backend.VfsSecond

id: VFS2

- name: vfs_backend.VfsFirst

connections:

- target: vfs_backend.Env

id: {var: ENV_SERVICE_NAME, include: env/env.h}

- name: vfs_backend.VfsSecond

connections:

- target: vfs_backend.Env

id: {var: ENV_SERVICE_NAME, include: env/env.h}

В начало
[Topic vfs_backends][Topic sc_using_ipc][Topic sc_ipc_channels]

Обзор: создание IPC-каналов

Есть два способа создания IPC-каналов: статический и динамический.

Статическое создание IPC-каналов проще в реализации, поскольку для него можно использовать init-описание.

Динамическое создание IPC-каналов позволяет изменять топологию взаимодействия процессов "на лету". Это требуется, если неизвестно, какой именно сервер содержит службу, необходимую клиенту. Например, может быть неизвестно, на какой именно накопитель нужно будет записывать данные.

Статическое создание IPC-канала

Статический способ имеет следующие особенности:

  • клиент и сервер находятся в остановленном состоянии в момент создания IPC-канала;
  • создание инициируются родительским процессом, запускающим клиента и сервера (обычно это Einit);
  • созданный IPC-канал невозможно удалить;
  • чтобы получить IPC-дескриптор и идентификатор службы (riid) после создания IPC-канала, клиент и сервер должны использовать интерфейс локатора сервисов (coresrv/sl/sl_api.h).

Динамическое создание IPC-канала

Динамический способ имеет следующие особенности:

  • клиент и сервер уже запущены в момент создания IPC-канала;
  • создание инициируются совместно клиентом и сервером;
  • созданный IPC-канал может быть удален;
  • клиент и сервер получают IPC-дескриптор и идентификатор службы (riid) сразу после успешного создания IPC-канала.
В начало
[Topic ipc_channels_overview]

Создание IPC-каналов с помощью init.yaml

Здесь собраны init-описания, демонстрирующие особенности создания IPC-каналов. Примеры задания свойств и аргументов процессов через init-описания разбираются в отдельной статье.

В примерах в составе KasperskyOS Community Edition может использоваться формат init-описания с макросами (init.yaml.in).

Файл с init-описанием обычно называется init.yaml, хотя может иметь любое имя.

Соединение и запуск процесса-клиента и процесса-сервера

В следующем примере будут запущены два процесса: класса Client и класса Server. Имена процессов не указаны, поэтому они будут совпадать с именами классов процессов. Имена исполняемых файлов также не указаны, они также будут совпадать с именами классов. Процессы будут соединены IPC-каналом с именем server_connection.

init.yaml

entities:

- name: Client

connections:

- target: Server

id: server_connection

- name: Server

В начало
[Topic ipc_channel_create_einit]

Динамическое создание IPC-каналов

При динамическом создании IPC-канала используются функции:

Динамическое создание IPC-канала осуществляется по следующему сценарию:

  1. Запускаются процессы: клиент, сервер и сервер имен.
  2. Сервер подключается к серверу имен с помощью вызова NsCreate() и публикует имя сервера, имя интерфейса и имя службы с помощью вызова NsPublishService().
  3. Клиент подключается к серверу имен с помощью вызова NsCreate() и выполняет поиск имени сервера и имени службы по имени интерфейса с помощью вызова NsEnumServices().
  4. Клиент запрашивает доступ к службе с помощью вызова KnCmConnect(), передавая в качестве аргументов найденные имя сервера и имя службы.
  5. Сервер вызывает функцию KnCmListen() для проверки наличия запросов на доступ к службе.
  6. Сервер принимает запрос клиента на доступ к службе с помощью вызова KnCmAccept(), передавая в качестве аргументов имя клиента и имя службы, которые получены при вызове KnCmListen().

Пункты 2 и 3 могут быть опущены, если клиент заранее знает имя сервера и имя службы.

Сервер может снимать с публикации на сервере имен ранее опубликованные службы с помощью вызова NsUnPublishService().

Сервер может отклонять запросы доступа к службам с помощью вызова KnCmDrop().

Для использования сервера имен политика безопасности решения должна разрешать взаимодействие процесса класса kl.core.NameServer и процессами, между которыми необходимо динамически создавать IPC-каналы.

В начало
[Topic ipc_channel_create_dynamic]

Использование служб из состава KasperskyOS Community Edition

В этом разделе

Добавление службы в решение

В начало
[Topic sc_using_sdk_endpoints]

Добавление службы в решение

Чтобы программа Client могла использовать ту или иную функциональность через механизм IPC, необходимо:

  1. Найти в составе KasperskyOS Community Edition исполняемый файл (условно назовем его Server), реализующий нужную функциональность. (Под функциональностью мы здесь понимаем одну или несколько служб, имеющих самостоятельные IPC-интерфейсы)
  2. Подключить CMake-пакет, содержащий файл Server и его клиентскую библиотеку.
  3. Добавить исполняемый файл Server в образ решения.
  4. Изменить init-описание так, чтобы при старте решения программа Einit запускала новый серверный процесс из исполняемого файла Server и соединяла его IPC-каналом с процессом, запускаемым из файла Client.

    Необходимо указать корректное имя IPC-канала, чтобы транспортные библиотеки могли идентифицировать этот канал и найти его IPC-дескрипторы. Корректное имя IPC-канала, как правило, совпадает с именем класса серверного процесса. VFS при этом является исключением.

  5. Изменить PSL-описание так, чтобы разрешить запуск серверного процесса и IPC-взаимодействие между клиентом и сервером.
  6. Подключить в исходном коде программы Client заголовочный файл с методами сервера.
  7. Скомпоновать программу Client с клиентской библиотекой.

Пример добавления GPIO-драйвера в решение

В составе KasperskyOS Community Edition есть файл gpio_hw, реализующий функциональность GPIO-драйвера.

Следующие команды подключают CMake‑пакет gpio:

.\CMakeLists.txt

...

find_package (gpio REQUIRED COMPONENTS CLIENT_LIB ENTITY)

include_directories (${gpio_INCLUDE})

...

Добавление исполняемого файла gpio_hw в образ решения производится с помощью переменной gpio_HW_ENTITY, имя которой можно найти в конфигурационном файле пакета – /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/lib/cmake/gpio/gpio-config.cmake:

einit\CMakeLists.txt

...

set (ENTITIES Client ${gpio_HW_ENTITY})

...

В init-описание нужно добавить следующие строки:

init.yaml.in

...

- name: client.Client

connections:

- target: kl.drivers.GPIO

id: kl.drivers.GPIO

- name: kl.drivers.GPIO

path: gpio_hw

В PSL-описание нужно добавить следующие строки:

security.psl.in

...

execute src=Einit, dst=kl.drivers.GPIO

{

grant()

}

request src=client.Client, dst=kl.drivers.GPIO

{

grant()

}

response src=kl.drivers.GPIO, dst=client.Client

{

grant()

}

...

В коде программы Client нужно подключить заголовочный файл, в котором объявлены методы GPIO-драйвера:

client.c

...

#include <gpio/gpio.h>

...

Наконец, нужно скомпоновать программу Client с клиентской библиотекой GPIO:

client\CMakeLists.txt

...

target_link_libraries (Client ${gpio_CLIENT_LIB})

...

Для корректной работы GPIO‑драйвера может понадобиться добавить в решение компонент BSP. Чтобы не усложнять этот пример, мы не рассматриваем здесь BSP. Подробнее см. пример gpio_output: /opt/KasperskyOS-Community-Edition-<version>/examples/gpio_output

В начало
[Topic using_sdk_endpoints_examples][Topic sc_using_new_endpoints]

Обзор: структура IPC-сообщения

В KasperskyOS все взаимодействия между процессами статически типизированы. Допустимые структуры IPC-сообщения определяются описанием интерфейсов процесса-получателя сообщения (сервера).

Корректное IPC-сообщение (как запрос, так и ответ) содержит фиксированную часть и арену.

Фиксированная часть сообщения

Фиксированная часть сообщения содержит аргументы фиксированного размера, а также RIID и MID.

Аргументы фиксированного размера – это аргументы любых IDL-типов, кроме типа sequence.

RIID и MID идентифицируют вызываемый интерфейс и метод:

  • RIID (Runtime Implementation ID) является порядковым номером вызываемой службы процесса (начиная с нуля).
  • MID (Method ID) является порядковым номером метода в содержащем его интерфейсе (начиная с нуля).

Тип фиксированной части сообщения генерируется компилятором NK на основе IDL-описания интерфейса. Для каждого метода интерфейса генерируется отдельная структура. Также генерируются типы union для хранения любого запроса к процессу, компоненту или интерфейсу. Подробнее см. Пример генерации транспортных методов и типов.

Арена

Арена представляет собой буфер для хранения аргументов переменного размера (IDL‑тип sequence).

Проверка структуры сообщения модулем безопасности

Перед тем как вызывать связанные с сообщением правила, подсистема Kaspersky Security Module проверяет отправляемое сообщение на корректность. Проверяются как запросы, так и ответы. Если сообщение имеет некорректную структуру, оно будет отклонено без вызова связанных с ним методов моделей безопасности.

Формирование структуры сообщения

В составе KasperskyOS Community Edition поставляются следующие инструменты, позволяющие упростить для разработчика создание и упаковку IPC-сообщения:

Формирование простейшего IPC-сообщения показано в примерах echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/).

В начало
[Topic ipc_message_structure_overview]

Нахождение IPC-дескриптора

Клиентский и серверный IPC-дескрипторы требуется находить, если для используемой службы нет готовых транспортных библиотек (например, вы написали собственную службу). Для самостоятельной работы с IPC-транспортом нужно предварительно инициализировать его с помощью метода NkKosTransport_Init(), передав в качестве второго аргумента IPC-дескриптор используемого канала.

Подробнее см. примеры echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/),

Для использования служб, которые реализованы в исполняемых файлах в составе KasperskyOS Community Edition, нет необходимости находить IPC-дескриптор. Вся работа с транспортом, включая нахождение IPC-дескрипторов, выполняется поставляемыми транспортными библиотеками.

См. примеры gpio_*, net_*, net2_* и multi_vfs_* (/opt/KasperskyOS-Community-Edition-<version>/examples/).

Нахождение IPC-дескриптора при статическом создании канала

При статическом создании IPC-канала как клиент, так и сервер могут сразу после своего запуска узнать свои IPC-дескрипторы с помощью методов ServiceLocatorRegister() и ServiceLocatorConnect(), указав имя созданного IPC-канала.

Например, если IPC-канал имеет имя server_connection, то на клиентской стороне необходимо вызвать:

#include <coresrv/sl/sl_api.h>

Handle handle = ServiceLocatorConnect("server_connection");

На серверной стороне необходимо вызвать:

#include <coresrv/sl/sl_api.h>

nk_iid_t iid;

Handle handle = ServiceLocatorRegister("server_connection", NULL, 0, &iid);

Подробнее см. примеры echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/), а так же заголовочный файл /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/coresrv/sl/sl_api.h.

Нахождение IPC-дескриптора при динамическом создании канала

Как клиент, так и сервер получают свои IPC-дескрипторы сразу при успешном динамическом создании IPC-канала.

Клиентский IPC-дескриптор является одним из выходных (out) аргументов метода KnCmConnect(). Серверный IPC-дескриптор является выходным аргументом метода KnCmAccept(). Подробнее см. заголовочный файл /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/coresrv/cm/cm_api.h.

В начало
[Topic ipc_find_ipc_desc]

Нахождение идентификатора службы (riid)

Идентификатор службы (riid) требуется находить на клиентской стороне, если для используемой службы нет готовых транспортных библиотек (например, вы написали собственную службу). Для вызова методов сервера необходимо на клиентской стороне предварительно вызвать метод инициализации прокси-объекта, передав в качестве третьего аргумента идентификатор службы. Например, для интерфейса Filesystem:

Filesystem_proxy_init(&proxy, &transport.base, riid);

Подробнее см. примеры echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/),

Для использования служб, которые реализованы в исполняемых файлах в составе KasperskyOS Community Edition, нет необходимости находить идентификатор службы. Вся работа с транспортом выполняется поставляемыми транспортными библиотеками.

См. примеры gpio_*, net_*, net2_* и multi_vfs_* (/opt/KasperskyOS-Community-Edition-<version>/examples/).

Нахождение идентификатора службы при статическом создании канала

При статическом создании IPC-канала клиент может узнать идентификатор нужной службы с помощью метода ServiceLocatorGetRiid(), указав дескриптор IPC-канала и полное квалифицированное имя службы. Например, если экземпляр компонента OpsComp содержит службу FS, то на клиентской стороне необходимо вызвать:

#include <coresrv/sl/sl_api.h>

nk_iid_t riid = ServiceLocatorGetRiid(handle, "OpsComp.FS");

Подробнее см. примеры echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/), а так же заголовочный файл /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/coresrv/sl/sl_api.h.

Нахождение идентификатора службы при динамическом создании канала

Клиент получает идентификатор службы сразу при успешном динамическом создании IPC-канала. Клиентский IPC-дескриптор является одним из выходных (out) аргументов метода KnCmConnect(). Подробнее см. заголовочный файл /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/coresrv/cm/cm_api.h.

В начало
[Topic ipc_find_riid]

Пример генерации транспортных методов и типов

При сборке решения компилятор NK на основе EDL-, CDL- и IDL-описаний генерирует набор специальных методов и типов, упрощающих формирование, отправку, прием и обработку IPC-сообщений.

В качестве примера рассмотрим класс процессов Server, предоставляющий службу FS, которая содержит единственный метод Open():

Server.edl

entity Server

/* OpsComp - именованный экземпляр компонента Operations */

components {

OpsComp: Operations

}

Operations.cdl

component Operations

/* FS - локальное имя службы, реализующей интерфейс Filesystem */

endpoints {

FS: Filesystem

}

Filesystem.idl

package Filesystem

interface {

Open(in string<256> name, out UInt32 h);

}

На основе этих описаний будут сгенерированы файлы Server.edl.h, Operations.cdl.h, и Filesystem.idl.h содержащие следующие методы и типы:

Методы и типы, общие для клиента и сервера

  • Абстрактные интерфейсы, содержащие указатели на реализации входящих в них методов.

    В нашем примере будет сгенерирован один абстрактный интерфейс – Filesystem:

    typedef struct Filesystem {

    const struct Filesystem_ops *ops;

    } Filesystem;

    typedef nk_err_t

    Filesystem_Open_fn(struct Filesystem *, const

    struct Filesystem_Open_req *,

    const struct nk_arena *,

    struct Filesystem_Open_res *,

    struct nk_arena *);

    typedef struct Filesystem_ops {

    Filesystem_Open_fn *Open;

    } Filesystem_ops;

  • Набор интерфейсных методов.

    При вызове интерфейсного метода в запросе автоматически проставляются соответствующие значения RIID и MID.

    В нашем примере будет сгенерирован единственный интерфейсный метод Filesystem_Open:

    nk_err_t Filesystem_Open(struct Filesystem *self,

    struct Filesystem_Open_req *req,

    const

    struct nk_arena *req_arena,

    struct Filesystem_Open_res *res,

    struct nk_arena *res_arena)

Методы и типы, используемые только на клиенте

  • Типы прокси-объектов.

    Прокси-объект используется как аргумент интерфейсного метода. В нашем примере будет сгенерирован единственный тип прокси-объекта Filesystem_proxy:

    typedef struct Filesystem_proxy {

    struct Filesystem base;

    struct nk_transport *transport;

    nk_iid_t iid;

    } Filesystem_proxy;

  • Функции для инициализации прокси-объектов.

    В нашем примере будет сгенерирована единственная инициализирующая функция Filesystem_proxy_init:

    void Filesystem_proxy_init(struct Filesystem_proxy *self,

    struct nk_transport *transport,

    nk_iid_t iid)

  • Типы, определяющие структуру фиксированной части сообщения для каждого конкретного метода.

    В нашем примере будет сгенерировано два таких типа: Filesystem_Open_req (для запроса) и Filesystem_Open_res (для ответа).

    typedef struct __nk_packed Filesystem_Open_req {

    __nk_alignas(8)

    struct nk_message base_;

    __nk_alignas(4) nk_ptr_t name;

    } Filesystem_Open_req;

    typedef struct Filesystem_Open_res {

    union {

    struct {

    __nk_alignas(8)

    struct nk_message base_;

    __nk_alignas(4) nk_uint32_t h;

    };

    struct {

    __nk_alignas(8)

    struct nk_message base_;

    __nk_alignas(4) nk_uint32_t h;

    } res_;

    struct Filesystem_Open_err err_;

    };

    } Filesystem_Open_res;

Методы и типы, используемые только на сервере

  • Тип, содержащий все службы компонента, а также инициализирующая функция. (Для каждого компонента сервера.)

    При наличии вложенных компонентов этот тип также содержит их экземпляры, а инициализирующая функция принимает соответствующие им инициализированные структуры. Таким образом, при наличии вложенных компонентов, их инициализацию необходимо начинать с самого вложенного.

    В нашем примере будет сгенерирована структура Operations_component и функция Operations_component_init:

    typedef struct Operations_component {

    struct Filesystem *FS;

    };

    void Operations_component_init(struct Operations_component *self,

    struct Filesystem *FS)

  • Тип, содержащий все службы, предоставляемые сервером непосредственно; все экземпляры компонентов, входящие в сервер; а также инициализирующая функция.

    В нашем примере будет сгенерирована структура Server_entity и функция Server_entity_init:

    #define Server_entity Server_component

    typedef struct Server_component {

    struct : Operations_component *OpsComp;

    } Server_component;

    void Server_entity_init(struct Server_entity *self,

    struct Operations_component *OpsComp)

  • Типы, определяющие структуру фиксированной части сообщения для любого метода конкретного интерфейса.

    В нашем примере будет сгенерировано два таких типа: Filesystem_req (для запроса) и Filesystem_res (для ответа).

    typedef union Filesystem_req {

    struct nk_message base_;

    struct Filesystem_Open_req Open;

    };

    typedef union Filesystem_res {

    struct nk_message base_;

    struct Filesystem_Open_res Open;

    };

  • Типы, определяющие структуру фиксированной части сообщения для любого метода любой службы конкретного компонента.

    При наличии вложенных компонентов эти типы также содержат структуры фиксированной части сообщения для любых методов любых служб, включенных во все вложенные компоненты.

    В нашем примере будет сгенерировано два таких типа: Operations_component_req (для запроса) и Operations_component_res (для ответа).

    typedef union Operations_component_req {

    struct nk_message base_;

    Filesystem_req FS;

    } Operations_component_req;

    typedef union Operations_component_res {

    struct nk_message base_;

    Filesystem_res FS;

    } Operations_component_res;

  • Типы, определяющие структуру фиксированной части сообщения для любого метода любой службы конкретного компонента, экземпляр которого входит в сервер.

    При наличии вложенных компонентов эти типы также содержат структуры фиксированной части сообщения для любых методов любых служб, включенных во все вложенные компоненты.

    В нашем примере будет сгенерировано два таких типа: Server_entity_req (для запроса) и Server_entity_res (для ответа).

    #define Server_entity_req Server_component_req

    typedef union Server_component_req {

    struct nk_message base_;

    Filesystem_req OpsComp_FS;

    } Server_component_req;

    #define Server_entity_res Server_component_res

    typedef union Server_component_res {

    struct nk_message base_;

    Filesystem_res OpsComp_FS;

    } Server_component_res;

  • Dispatch-методы (диспетчеры) для отдельного интерфейса, компонента или класса процессов.

    Диспетчеры анализируют полученный запрос (значения RIID и MID), вызывают реализацию соответствующего метода, после чего сохраняют ответ в буфер. В нашем примере будут сгенерированы диспетчеры Filesystem_interface_dispatch, Operations_component_dispatch и Server_entity_dispatch.

    Диспетчер класса процессов обрабатывает запрос и вызывает методы, реализуемые этим классом. Если запрос содержит некорректный RIID (например, относящийся к другой службе, которой нет у этого класса процессов) или некорректный MID, диспетчер возвращает NK_EOK или NK_ENOENT.

    nk_err_t Server_entity_dispatch(struct Server_entity *self,

    const

    struct nk_message *req,

    const

    struct nk_arena *req_arena,

    struct nk_message *res,

    struct nk_arena *res_arena)

    В специальных случаях можно использовать диспетчеры интерфейса и компонента. Они принимают дополнительный аргумент – ID реализации интерфейса (nk_iid_t). Запрос будет обработан только если переданный аргумент и RIID из запроса совпадают, а MID корректен. В противном случае диспетчеры возвращают NK_EOK или NK_ENOENT.

    nk_err_t Operations_component_dispatch(struct Operations_component *self,

    nk_iid_t iidOffset,

    const

    struct nk_message *req,

    const

    struct nk_arena *req_arena,

    struct nk_message *res,

    struct nk_arena *res_arena)

    nk_err_t Filesystem_interface_dispatch(struct Filesystem *impl,

    nk_iid_t iid,

    const

    struct nk_message *req,

    const

    struct nk_arena *req_arena,

    struct nk_message *res,

    struct nk_arena *res_arena)

В начало
[Topic transport_code_overview]