Contents
Description of entities, components, and interfaces (EDL, CDL, IDL)
All entities, components and interfaces used in the solution must be statically described using the EDL, CDL and IDL languages. The corresponding descriptions are saved in the *.edl
, *.cdl
and *.idl
files, respectively.
Description files are used when building a solution for the following purposes:
- Generating header files containing transport methods and types
- Building a security module
Entity-Component-Interface model
Each entity can provide one or more interaction interfaces. Multiple interfaces can be combined into a component. Components can be embedded into other components. All entities, components and interfaces used in the solution must be statically described.
In KasperskyOS, there are three types of static description files:
- An EDL file contains a description of the entity in the Entity Definition Language (EDL): its name, components and interfaces, and other information.
- A CDL file contains a description of the component in the Component Definition Language,hereinafter referred to as CDL. The component description includes its name, set of included components and implemented interfaces.
- An IDL file contains a description of the package containing one interface in the Interface Definition Language (IDL). The description includes the package name, a declaration of the interface included in it, and a declaration of the types and named constants.
To ensure flexibility, an entity can contain several instances of one component. Multiple entities can include instances of the same component.
An entity does not have to contain any component and does not have to implement an interface. If this is the case, it does not provide functionality that is available to other entities.
The Server entity includes instances of the Terminal and Serial components containing various implementations of the Console interface.
Page topEDL
Each entity in KasperskyOS must be described in the Entity Definition Language (EDL), in a separate file named <entity name>.edl
.
The name of an EDL file must match the name of the entity that it describes.
An EDL file contains the following sections – the order is important:
- Name of the entity. Required section beginning with the reserved word
entity
, which is followed by the entity name.The entity name must begin with an uppercase letter and must not contain an underscore (_).
- Security interface used by the entity. This section is optional. It must be added only if the entity uses a security interface. It is declared with the reserved word
security
, which should be followed by the full name of the interface. - List of implementations of interfaces that are provided by the entity. This is optional, and is described in the
interfaces
section. Each interface implementation is specified with a separate string in the following format:interfaces {
<interface implementation name>:<interface name>
}
An entity can contain several implementations of one interface. All implemented interfaces must be described in the IDL language in IDL files.
The interface implementation name must not contain an underscore (_).
- List of instances of components included in the entity. This is optional, and is described in the
components
section. Each component instance is specified with a separate string in the following format:components {
<component instance name>:<component name>
}
For each specified component, a separate file named
<component name>.cdl
needs to be created, containing a description of the component in the CDL language. Multiple instances of the same component can be added to an entity, and each instance can have a separate state (see the example ofUartDriver
entity below).The component instance name must not contain an underscore (_).
The interfaces
and components
sections are optional and are added only for server entities. Implementations of interfaces declared in the interfaces
section and implementations that are included in components from the components
section can be used by client entities.
EDL supports single-line comments and multi-line C++-style comments:
/* This is a comment
And this, too */
// Another comment
Examples of EDL files
At its simplest, the entity does not use a security interface and does not provide functionality to other entities, like the hello
entity from the hello example. An EDL description for such an entity contains only the reserved word entity
and the entity name.
Hello.edl
// Entity name: Hello
entity Hello
In the following example, the EFoo
entity contains a single interface implementation and does not use a security interface.
Efoo.edl
// Entity name: Efoo
entity Efoo
// The entity contains a named implementation of the IFool interface. Implementation name: foo.
interfaces {
foo : IFoo
}
In the following example, the UartDriver
entity contains two instances of the UartComp
component: one for each UART device.
The UartDriver
entity does not use a security interface.
UartDriver.edl
// Entity name: UartDriver
entity UartDriver
// uart0 and uart1 are the names of instances of the UartComp component
// that are responsible for two different devices
components {
uart0: UartComp
uart1: UartComp
}
CDL
Each component used in the solution must be described in the CDL language, in a separate <component name>.cdl
file.
The name of a CDL file must match the name of the component that it describes.
A CDL file includes the following sections:
- The name of the component. The reserved word
component
is placed before the component name.The component name must begin with an uppercase letter and must not contain an underscore (_).
- Security interface within this component. This section is optional. It must be added only if the component contains a security interface. It is declared with the reserved word
security
, which should be followed by the full name of the interface. - A list of interface implementations included in this component. Interfaces are declared in the
interfaces
section in which each interface implementation is specified with a separate string in the following format:interfaces {
<interface implementation name>:<interface name>
}
A component can contain several implementations of one interface. All implemented interfaces must be described in the IDL language in IDL files.
The interface implementation name must not contain an underscore (_).
- The list of instances of components embedded in this component. This section is optional. It should be added if the component contains embedded components. Components are declared in the
components
section in which each component instance is specified with a separate string in the following format:components {
<component instance name>:<component name>
}
For each specified component, a separate file named
<component name>.cdl
needs to be created, containing a description of the component in the CDL language. Multiple instances of the same component can be added to a component, and each instance can have a separate state.The component instance name must not contain an underscore (_).
CDL supports single-line comments and multi-line C++-style comments.
Examples of CDL files
At its simplest, the component contains a single interface implementation similar to the ping
component from the echo example.
Ping.cdl
/* Component name: Ping */
component Ping
/* The component contains a named implementation of the IPing interface. Implementation name: pingimpl.*/
components {
pingimpl: IPing
}
In the following example, the CoFoo
component contains implementations of two interfaces declared in two different packages named Foo
and Baz
(i.e. in the Foo.idl
and Bar.idl
files):
CoFoo.cdl
/* Component name: CoFoo */
component CoFoo
interfaces {
/* The component contains an implementation of the Foo interface. Implementation name: foo.*/
foo: Foo
/* The component contains three different implementations of the Bar interface. Names of the implementations: bar1, bar2 and bar3.*/
bar1: Bar
bar2: Bar
bar3: Bar
}
In the following example, the CoFoo
component contains a single interface implementation and an embedded component.
CoFoo.cdl
/* Component name: CoFoo */
component CoFoo
interfaces {
/* The component contains an implementation of the Foo interface. Implementation name: foo.*/
foo: Foo
}
components {
/* The component contains an instance of the CoBar component. Instance name: bar.*/
bar: CoBar
}
IDL
All interfaces implemented in a solution must be described in IDL files.
Only one interface can be declared in each IDL file.
The name of an IDL file must match the name of the interface that it describes.
An IDL file contains the following sections:
- Interface name.
The only mandatory section.
The interface name must begin with an uppercase letter and must not contain an underscore (_).
A name declaration has the following format:
package <name of the described interface>
An interface name can be composite. In this case, it is used to generate a path to the IDL file, for example:
/* Module.idl located at: a/b/c/Module.idl */
package a.b.c.Module
- Import external packages.
A package import statement has the following format:
import <name of external package>
It enables you to enter the following elements of an external package into the scope of an IDL file: named constants and user-defined types, including structures and variant types.
- Declaring structures, variant types, named constants, and synonyms.
- Declaration of interface methods.
An interface methods declaration has the following syntax:
interface {
<Declaration of methods>
}
The curly brackets contain declarations of methods that have the following syntax:
<Method name> (<arguments>)
The method name must not contain an underscore (_). It is recommended to begin the names of methods with an uppercase letter.
Arguments are divided into input (
in
) and output (out
) and are separated with a comma. Example interface declaration:interface {
// Declaration of Ping method
Ping(in UInt32 value, out UInt32 result);
}
It is not recommended to use
out
arguments to return codes of errors that occur when a request is being handled by the server entity. Instead, it is recommended to use specializederror
arguments. For more details, refer to Managing errors in IDL.
IDL supports single-line comments and multi-line C++-style comments.
Examples of IDL files
In the simplest case, an IDL description contains only a name and interface declaration:
Ping.idl
/* Ping is the interface name */
package Ping
/* Declaration of interface methods */
interface {
// Declaration of Ping method
Ping(in UInt32 value, out UInt32 result);
}
Example of a more complex IDL description:
Foo.idl
// Name declaration
package Foo
// Import statements
import Bar1
import Bar2
// Declaration of named constant
const UInt32 MaxStringSize = 1024;
// Declaration of synonym for user-defined type
typedef sequence <Char, MaxStringSize> String;
// Declaration of structure
struct BazInfo {
Char x;
array <String, 100> y;
sequence <sequence <UInt32, 100>, 200> y;
}
// Declaration of interface methods
interface {
Baz(in BazInfo a, out UInt32 b);
}
IDL data types
Primitive IDL data types
IDL supports the following primitive data types:
SInt8
,SInt16
,SInt32
,SInt64
(signed integer);UInt8
,UInt16
,UInt32
,UInt64
(unsigned integer);Handle
(handle).
Example declarations of primitive variable types:
SInt32 value;
UInt8 ch;
Handle ResID;
For primitive types (except the Handle
type), you can declare a named constant using the reserved word const
:
const UInt32 c0 = 0;
const UInt32 c1 = 1;
Let us now examine complex (composite) data types: union
(variant type), struct
(structure), array
and sequence
.
union type
The union
variant type, similar to the normal union in C, allows storing different types of values within one memory area. However, when transmitting an IPC message, the union
type is provided with an additional tag
field that lets you define which union field is used in the transmitted value. For this reason, the union
type is also called a union with a tag. Example declaration of a union
variable type:
union foo {
UInt32 bar;
UInt8 baz;
}
A variant type declaration is a top-level declaration.
struct type
Structures in the IDL language are analogous to structures in C. Example structure declaration:
struct BazInfo {
UInt64 x;
UInt64 y;
}
A structure declaration is a top-level declaration.
array type
An array
has a constant size that is defined during declaration:
array <ElementType, ElementsCount>
In IDL, arrays are anonymous and may be declared directly when declaring other complex types, and when declaring interfaces.
The reserved word typedef
is used to assign a name to a declared array type, for example:
typedef array <SInt8, 1024> String;
sequence type
A sequence
resembles a variable-sized array. When declaring a sequence, the maximum number of elements is specified. However, you can actually transmit fewer elements. In this case, only an amount of memory necessary for the transmitted elements will be used.
Similarly to arrays, sequences are anonymous and may be declared directly when declaring interfaces, and when declaring other complex types:
sequence <ElementType, ElementsCount>
Example declaration of a named sequence:
typedef sequence <SInt8, 1024> String;
Embedded complex types
Complex types can be used when declaring other complex types. Arrays
and sequences
can be declared directly inside another composite type:
struct BazInfo {
UInt64 x;
array <UInt8, 100> y;
sequence <sequence <UInt32, 100>, 200> z;
}
The union
or struct
types must be declared prior to use:
union foo {
UInt32 value1;
UInt8 value2;
}
struct bar {
UInt32 a;
UInt8 b;
}
struct BazInfo {
foo x;
bar y;
}
Type synonym declaration (typedef)
The reserved word typedef
declares a name that, within its scope, is a synonym for the specified type. For example:
typedef array <UInt32, 20> value;
The typedef struct
and typedef union
constructs are invalid. A new name for a structure or variant type can be entered only if they were previously declared:
union foo {
UInt32 value1;
UInt8 value2;
}
typedef foo bar;
A typedef
declaration does not introduce a new data type; it declares new names for already existing types.
Managing errors in IDL
It is not recommended to use out
arguments in an IDL description to return codes of logical errors occurring when a request is handled by the server entity to the client entity. Instead, it is recommended to use error
arguments.
A logical error is an error that occurs when the server entity is handling a request and must be returned to the client entity. For example, if a client entity attempts to open a non-existent file, the file system server returns a logical error. Internal errors of the server entity such as memory shortage or invalid statement execution are not logical errors.
Use of error
arguments enables the following:
- No need to forward all
out
arguments of methods in a response message from the server entity to the client entity if an error is returned. Instead, a response message will contain onlyerror
arguments if an error code needs to be returned. - Bind methods of security models not only to response forwarding events but also to error return events.
The IDL language and NK compiler provide two ways to use error
arguments:
- Simplified error handling
This is employed by the NK compiler by default.
When using simplified error handling, the methods of interfaces can have no more than one
error
argument of theUInt16
type. The values of the returnederror
argument of the method are packaged into a result code of the corresponding client interface method. - Extended error handling
Extended error handling requires the use of the
--extended-errors
parameter of the NK compiler.When this is employed, methods of an interface can have multiple
error
arguments of any type. The values of returnederror
arguments of a method are provided in the internal fields of the response message structure.
The types and macros for handling errors are provided in the file named /opt/KasperskyOS-Community-Edition-<version>/toolchain/include/nk/types.h
Simplified error handling
When employing simplified error handling, the methods of interfaces can have no more than one error
argument of the UInt16
type named status
.
In case of an error:
- Implementations of methods on the server entity side must use the
NK_ELOGIC_MAKE()
macro to package the error code and return its result. - The corresponding client interface methods return the error code (logical or transport) packaged into a value of the
nk_err_t
type. TheNK_ELOGIC_CHECK()
andNK_ETRANSPORT_CHECK()
macros are used to check it for logical errors and transport errors, respectively.
If the request is successfully handled:
- Implementations of methods on the server entity side must put the results into the response message fields corresponding to the
out
arguments and returnNK_EOK
. - The corresponding client interface methods also return
NK_EOK
.
For example, let's examine an IDL description of an interface containing one method:
Connection.idl
typedef UInt16 ErrorCode
// error code example
const ErrorCode ESendFailed = 10;
interface {
Send(in Handle handle,
in Packet packet,
out UInt32 sentLen,
// the Send method has one UInt16-type error argument named status
error ErrorCode status
);
}
Implementation of the server entity:
server.c
...
// Send method implementation
nk_err_t Send_impl(struct Connection *self,
const struct Connection_Send_req *req,
const struct nk_arena* req_arena,
struct Connection_Send_res* res,
struct nk_arena* res_arena)
{
if (...) {
// if the request is successfully handled, put the result into an out argument and return NK_EOK
res->sentLen = value;
return NK_EOK;
} else {
// if an error occurs, return the result of the NK_ELOGIC_MAKE macro
return NK_ELOGIC_MAKE(Connection_ESendFailed);
}
Implementation of the client entity:
client.c
// the values of the returned error argument of the method are packaged into a result code of the corresponding client interface method
nk_err_t ret = Connection_Send(ctx,
&req, &req_arena,
&res, NK_NULL);
if (ret == NK_EOK) {
// if there are no errors, res contains the values of out arguments
return res.sentLen;
} else {
if (NK_ELOGIC_CHECK(ret)) {
// handle the logical error
} else {
// handle the transport error
}
}
Extended error handling
When employing extended error handling, the methods of interfaces can have more than one error
argument of any type.
In case of an error:
- Implementations of methods on the server entity side must do the following:
- Use the
nk_err_reset()
macro to set the error flag in a response message. - Put error codes into the response message fields corresponding to
error
arguments. - return
NK_EOK
.
- Use the
- The corresponding client interface methods return the code of the transport error or
NK_EOK
if there is no transport error. Thenk_msg_check_err()
macro is used to check for logical errors in a response message. The values of returnederror
arguments of a method are provided in the internal fields of the response message structure.
If the request is successfully handled:
- Implementations of methods on the server entity side must put the results into the response message fields corresponding to the
out
arguments and returnNK_EOK
. - The corresponding client interface methods also return
NK_EOK
.
For example, let's examine an IDL description of an interface containing one method:
Connection.idl
typedef UInt16 ErrorCode
interface {
Send(in Handle handle,
in Packet packet,
out UInt32 sentLen,
// the Send method has one UInt16-type error argument named status
error ErrorCode status
);
}
The union
type generated by the NK compiler for the constant part of a response message for this method contains out
arguments and error
arguments:
Connection.idl.h
...
struct Connection_Send_res {
union {
struct {
struct nk_message base_;
nk_uint32_t sentLen;
};
struct {
struct nk_message base_;
nk_uint32_t sentLen;
} res_;
struct kl_Connection_Send_err {
struct nk_message base_;
nk_uint16_t status
} err_;
};
};
...
Implementation of the server entity:
server.c
...
// Send method implementation
nk_err_t Send_impl(struct Connection *self,
const struct Connection_Send_req *req,
const struct nk_arena* req_arena,
struct Connection_Send_res* res,
struct nk_arena* res_arena)
{
if (...) {
// if the request is successfully handled, put the result into an out argument and return NK_EOK
res->sentLen = value;
return NK_EOK;
} else {
// if an error occurs, call the nk_err_reset() macro to set the error flag in the response message
nk_err_reset(res)
// complete the fields of err arguments in the res->err_ structure
res->err_.status = 10;
// return NK_EOK
return NK_EOK;
}
Implementation of the client entity:
client.c
nk_err_t ret = Connection_Send(ctx,
&req, &req_arena,
&res, NK_NULL);
if (ret == NK_EOK) {
if (nk_msg_check_err(res)) {
// handle the logical error
// the values of returned error arguments of a method are provided in the internal fields of the response message structure
return res.err_.status;
} else {
// if there are no errors, res contains the values of out arguments
return res.sentLen;
}
} else {
// handle the transport error
}
Composite names of entities, components and interfaces
For entities, components and interfaces, you can use composite names containing one or more dots. In this case, the part of the name that comes after the last dot is called the short name.
For example, if the full name of an entity is kl.VfsEntity
, its short name is VfsEntity
. An interface with the full name main.security.Verify
has the short name Verify
.
If a name is not composite (for example, server
) – it is considered to be both the full name and the short name.
Names and paths of description files
The name of an EDL file must match the short name of the entity that it describes. In this case, the composite name is used to indicate the path to the EDL file. For example, kl.VfsEntity
must be described in the kl/VfsEntity.edl
file.
This same rule applies to components and interfaces: the name of a CDL file must match the short name of the component, and the name of an IDL file must match the short name of the interface. However, the composite name is used to indicate the path to a CDL file or IDL file. For example, the net.Updater
component must be described in the net/Updater.cdl
file, and the interface with the full name main.security.Verify
must be described in the main/security/Verify.idl
file.
The short names of entities, components, and interfaces must not contain an underscore (_). The underscore character is allowed in segments of a composite name.
Names of executable files for starting entities
When a solution is started, the Einit entity starts other entities as follows: in ROMFS (in the solution image), it runs the executable file with the name that matches the short name of the entity being started. For example, the Client
and net.Client
entities will be started from the executable file named Client
by default. To start an entity from an executable file with a different name when the solution is started, use the reserved word path
in the init description.
Full names in descriptions
Only the full names of entities, components and interfaces are used in EDL-, CDL- and IDL descriptions, and in the init description and security configuration. As an example, let us examine the already mentioned kl.VfsEntity
, net.Updater
component and main.security.Verify
interface.
Example EDL description of kl.VfsEntity
:
VfsEntity.edl
entity kl.VfsEntity
...
Example CDL description of the net.Updater
component:
Updater.cdl
component net.Updater
...
Example init description:
init.yaml
entities:
- name: kl.VfsEntity
...
Example security configuration:
security.psl
...
/* Declaration of "kl.VfsEntity" entity. */
use EDL kl.VfsEntity;
...
/* Configuration of requests for calling the Check method of any "Verify" interface implementations. */
request interface=main.security.Verify, method=Check { grant () }
...
Names of NK-generated methods and types
The names of generated types and methods are based on the full names of entities, components and interfaces. For example, the net.Updater
component will have the net_Updater_component
structure, the net_Updater_component_init
function, the net_Updater_component_dispatch
dispatcher, as well as the net_Updater_component_req
request structure and net_Updater_component_res
response structure.
Einit entity
One of the most important entities in KasperskyOS is the entity named Einit, which is the first entity started by the operating system kernel when the image is loaded. In most solutions built on KasperskyOS, the Einit
entity starts all other entities included in the solution, which means that it serves as the initializing entity.
The toolkit provided in KasperskyOS Community Edition includes the einit tool, which lets you generate the code of the Einit
entity in C based on the init.yaml
file (also known as the init description). The Einit
entity created using the einit
script performs the following initializing functions:
- creates all entities included in the solution;
- creates required connections (IPC channels) between entities;
- copies information about the entity's connections into the environment of each entity;
- starts entities.
The standard way of using the einit
tool is to integrate an einit call into one of the steps of the build script. As a result, the einit
tool uses the init.yaml
file to generate the einit.c
file containing the Einit
entity code. In one of the following steps of the build script, compile the einit.c
file into the executable file of the Einit
entity and include it into the solution image.
You are not required to create static description files for the Einit
entity. These files are included in the KasperskyOS Community Edition toolkit and are automatically connected during a solution build. However, the Einit
entity must be described in the security.psl
file.
init.yaml file
The init.yaml
file (init description) is used by the einit
tool to generate source code of the Einit initializing entity. This file contains data in YAML format. This data identifies the following:
- Entities that are started when KasperskyOS is loaded.
- IPC channels that are used by entities to interact with each other.
This data consists of a dictionary with the entities
key containing a list of dictionaries of entities. Entity dictionary keys are presented in the table below.
Entity dictionary keys in an init description
Key |
Required |
Description |
---|---|---|
|
Yes |
Name of the entity |
|
No |
Entity ID, which coincides with the entity name by default. Each entity must have a unique ID. You can start multiple entities with the same name but different IDs. |
|
No |
Name of the executable file in ROMFS (in the solution image) from which the entity will be started. By default, the entity will be started from a file in ROMFS with a name that matches the short name of the entity. For example, the You can start multiple entities with the same name from different executable files. However, the IDs of these entities must be different. |
|
No |
Dictionary key containing a list of dictionaries of the IPC channels of the entity. This list defines the statically created IPC channels whose client handles will be owned by the entity. The list is empty by default. (In addition to statically created IPC channels, entities may use dynamically created IPC channels.) |
Entity IPC channel dictionary keys are presented in the table below.
Entity IPC channel dictionary keys in an init description
Key |
Required |
Description |
---|---|---|
|
Yes |
IPC channel ID, which can be defined as a specific value or as a link such as
Each IPC channel must have a unique ID. (The IPC channel ID is used by entities to receive an IPC handle.) |
|
Yes |
ID of the entity that will own the server handle of the IPC channel. |
Example init descriptions
In the provided examples, the file containing the init description is named init.yaml
, but it can have any name.
init.yaml
# init description of the solution containing the client entity and server entity
entities:
# The Client entity will send requests to the Server entity.
- name: Client
connections:
# ID of the server entity to which the Client entity will
# send requests
- target: Server
# ID of the IPC channel for exchanging IPC messages
# between entities
id: server_connection
# The Server entity will perform the server role
# (will respond to requests from the Client entity).
- name: Server
init.yaml
# init description in which the IPC channel ID is defined by a link
entities:
- name: Client
connections:
- target: Server
# IPC channel ID is in the SERVER_CONN constant
# in the src/example.h file
id: {var: SERVER_CONN, include: src/example.h}
- name: Server
init.yaml
# init description that defines the names of executable files that
# will be used to start the entities named Client, ClientServer and
# MainServer
entities:
- name: Client
path: cl
connections:
- target: ClientServer
id: server_connection_cs
- name: ClientServer
path: csr
connections:
- target: MainServer
id: server_connection_ms
- name: MainServer
path: msr
init.yaml
# init description in which the MainServer and BkServer entities
# will be started from one executable file
entities:
- name: Client
connections:
- id: server_connection_ms
target: MainServer
- id: server_connection_bs
target: BkServer
- name: MainServer
path: srv
- name: BkServer
path: srv
init.yaml
# init description that will start two
# Server entities with different IDs from the same executable
# file
entities:
- name: Client
connections:
- id: server_connection_us
# Server entity ID
target: UserServer
- id: server_connection_ps
# Server entity ID
target: PrivilegedServer
- task: UserServer
name: Server
- task: PrivilegedServer
name: Server
IPC basics in KasperskyOS
In KasperskyOS, the only means of interprocess communication (IPC) is exchange of messages.
Types of messages and roles of entities
Messaging in KasperskyOS is built on a client-server model.
When two entities interact, one of them is the client (client entity), and the other is the server (server entity). The client initiates the interaction by sending a request message (or simply "request"). The server receives the request and responds by sending a response message (or simply "response") to the client.
IPC channels
To enable two entities to exchange messages, an IPC channel, also referred to as "channel" or "connection", must be established between them. Each entity can have multiple connections, acting as a client for some entities while acting as a server for others.
System calls
KasperskyOS provides three system calls for IPC message exchange: Call
, Recv
and Reply
. The corresponding functions are declared in the syscalls.h
file:
Call()
is used by the client to send a request and receive a response. It receives an IPC handle, buffer containing the request, and buffer for the response.Recv()
is used by the server to receive a request. It receives an IPC handle and buffer for the request.Reply()
is used by the server to send a response. It receives an IPC handle and buffer for the response.
The Call()
, Recv()
and Reply()
functions return rcOk
or an error code.
The Call()
and Recv()
system calls are locking calls, meaning that messages are exchanged according to the rendezvous principle:
- The server thread that executes the
Recv()
call remains locked until a request is received. - The client thread that executes the
Call()
call remains locked until a response is received from the server.
System calls are rarely used in entity code. It is recommended that you instead use more convenient NK-generated methods that execute system calls.
IPC channels
IPC channels connect entities with each other and are used for IPC messaging.
The channel is provided by a pair of IPC handles (client and server), which are linked to each other.
Two entities linked by an IPC channel
A channel is directional. The entity associated with the client handle of the channel (client entity) can send requests and receive responses over this channel. The entity associated with the server handle of the channel (server entity) can receive requests and send responses over this channel.
An entity can have multiple connections (channels) with other entities, acting as a client in some connections while acting as a server in others.
Entity A is a client for the C entity and a server for the B entity. Designations: cl_1, cl_2 – client IPC handles; srv_1, srv_2 – server IPC handles.
Creating and using channels
IPC channels can be created statically or dynamically.
Statically created IPC channels are created by the Einit
entity when the solution is started. The code of the Einit
entity is generated based on the init description that specifies all channels (connections) that need to be created.
In addition to statically created IPC channels, entities may use dynamically created IPC channels.
To send and receive an IPC message over a specific channel, the corresponding values of IPC handles (client and server) must be known. Service Locator is used to obtain the values of IPC handles.
Dynamically created IPC channels
The capability to dynamically create IPC channels allows you to change the topology of interaction between entities on the fly. This is necessary if it is unknown which specific server entity will become the resource provider required by a client entity. For example, you may not know which specific drive you will need to write data to.
In contrast to a statically created IPC channel, a dynamically created IPC channel has the following characteristics:
- It is created between entities that have been started.
- It is created jointly by the client entity and server entity.
- It can be deleted.
- It involves the use of a name server (
kl.core.NameServer
entity), which facilitates the transfer of information about available interface implementations from server entities to client entities.
A dynamically created IPC channel uses the following functions:
- Name Server interface
- Connection Manager
The Name Server interface is provided for the user in the following files:
coresrv/ns/ns_api.h
is a header file of thelibkos
library;coresrv/ns/NameServer.idl
is an IDL description of the IPC interface of the Name Server.
The Connection Manager is provided for the user in the following files:
coresrv/cm/cm_api.h
is a header file of thelibkos
library;services/cm/CM.idl
is an IDL description of the Connection Manager's IPC interface.
An IPC channel is dynamically created according to the following scenario:
- A client entity, server entity and name server are started.
- The server entity connects to the name server by using the
NsCreate()
function and publishes the server entity name, interface name, and interface implementation name by using theNsPublishService()
function. - The client entity uses the
NsCreate()
function to connect to the name server and then uses theNsEnumServices()
function to search for the name of the server entity and the name of the interface implementation based on the interface name. - The client entity uses the
KnCmConnect()
function to request access to the interface implementation and passes the found server entity name and interface implementation name as arguments to the function. - The server entity calls the
KnCmListen()
function to check if the client entity sent a request to access the provided interface implementation. - The server entity accepts the request to access the provided interface implementation by using the
KnCmAccept()
function, to which it passes the client entity name and interface implementation name received from theKnCmListen()
function as arguments.
To use a name server, the solution security policy must allow the kl.core.NameServer
entity to interact with entities for which IPC channels can be dynamically created.
The server entity can use the NsUnPublishService()
function to unpublish interface implementations that were previously published on the name server.
The server entity can use the KnCmDrop()
function to reject requests to access interface implementations.
ns_api.h (fragment)
/**
* Attempts to connect to name server "name"
* for the specified number of msecs. If the "name" parameter has the value
* RTL_NULL, the function attempts to connect to name server "ns"
* (default name server). Output parameter "ns" contains the handle
* of the connection with the name server.
* If successful, the function returns rcOk, otherwise it returns an error code.
*/
Retcode NsCreate(const char *name, rtl_uint32_t msecs, NsHandle *ns);
/**
* Publishes the interface implementation on the name server.
* The "ns" parameter defines the handle for the connection with the name server.
* The "server" parameter defines the name of the server entity (for example, ata).
* The "type" parameter defines the name of the published interface (for example, IBlkDev).
* The "service" parameter defines the name of the published interface implementation
* (for example, blkdev.blkdev).
* If successful, the function returns rcOk, otherwise it returns an error code.
*/
Retcode NsPublishService(NsHandle ns, const char *type, const char *server,
const char *service);
/**
* Unpublishes the interface implementation on the name server.
* The "ns" parameter defines the handle for the connection with the name server.
* The "server" parameter defines the name of the server entity. The "type" parameter defines the name
* of the published interface. The "service" parameter defines the name of the implementation
* of the published interface.
* If successful, the function returns rcOk, otherwise it returns an error code.
*/
Retcode NsUnPublishService( NsHandle ns, const char *type, const char *server,
const char *service);
/**
* Enumerates the interface implementations published on the
* name server. The "ns" parameter defines the handle for the connection with the name server.
* The "type" parameter defines the name of the required interface. The "index" parameter
* defines the index for enumerating the interface implementations.
* Output parameter "server" contains the name of the server entity providing the
* interface implementation. Output parameter "service" contains the name of the
* interface implementation.
* For example, IBlkDev interface implementations are enumerated
* as follows.
* rc = NsEnumServices(ns, "IBlkDev", 0, outServerName, outServiceName);
* rc = NsEnumServices(ns, "IBlkDev", 1, outServerName, outServiceName);
* ...
* rc = NsEnumServices(ns, "IBlkDev", N, outServerName, outServiceName);
* Function calls with index incrementation continue until
* the function returns rcResourceNotFound.
* If successful, the function returns rcOk, otherwise it returns an error code.
*/
Retcode NsEnumServices(NsHandle ns, const char *type, unsigned index,
char *server, char *service);
cm_api.h (fragment)
/**
* Requests access to the interface implementation named "service"
* provided by server entity "server". The "msecs" parameter defines the
* timeout period (in milliseconds) for the server entity to accept the request. Output
* parameter "endpoint" contains the client IPC handle. Output parameter
* "rsid" contains the identifier of the interface implementation.
* If successful, the function returns rcOk, otherwise it returns an error code.
*/
Retcode KnCmConnect(const char *server, const char *service,
rtl_uint32_t msecs, Handle *endpoint,
rtl_uint32_t *rsid);
/**
* Checks if there is a request to access the interface implementation
* named "filter". If the "filter" parameter has the value RTL_NULL,
* it checks if there is a request to access any
* interface implementation. The "msecs" parameter defines the request timeout period
* in milliseconds. Output parameter "client" contains the name
* of the client entity. Output parameter "service" contains the name of the
* interface implementation.
* If successful, the function returns rcOk, otherwise it returns an error code.
*/
Retcode KnCmListen(const char *filter, rtl_uint32_t msecs, char *client,
char *service);
/**
* Rejects the request to access the interface implementation
* named "service" by client entity "client".
* If successful, the function returns rcOk, otherwise it returns an error code.
*/
Retcode KnCmDrop(const char *client, const char *service);
/**
* Accepts the request to access the interface implementation
* named "service" by client entity "client". The "rsid" parameter defines
/* interface implementation identifier. The "listener" parameter defines
* the listener handle. If the "listener" parameter has the value
* INVALID_HANDLE, a new listener handle is created.
* Output parameter "endpoint" contains the server IPC handle.
* If successful, the function returns rcOk, otherwise it returns an error code.
*/
Retcode KnCmAccept(const char *client, const char *service, rtl_uint32_t rsid,
Handle listener, Handle *endpoint);
The filter
parameter of the KnCmListen()
function is reserved and does not affect the behavior of the function. The behavior of the function corresponds to the RTL_NULL
value of the filter
parameter.
The listener handle is a server IPC handle with expanded rights. It is created when the KnCmAccept()
function is called with the INVALID_HANDLE
argument in the listener
parameter. If a listener handle is passed to the KnCmAccept()
function when it is called, the created server IPC handle will provide the capability to receive requests over all IPC channels associated with this listener handle. (The first IPC channel associated with the listener handle is created when the KnCmAccept()
function is called with the INVALID_HANDLE
argument in the listener
parameter. The second and subsequent IPC channels associated with the listener handle are created during the second and subsequent calls of the KnCmAccept()
function, to which the listener handle obtained during the first call is passed.)
The listener handle is also used when statically creating IPC channels. It is created when the KnHandleConnectEx()
function is called with the INVALID_HANDLE
argument in the srListener
parameter. If a listener handle is passed to the KnHandleConnectEx()
function when it is called, the created server IPC handle will provide the capability to receive requests over all IPC channels associated with this listener handle. A listener handle is associated with multiple IPC channels if multiple IPC channels with identical names are created for one server entity.
If the dynamically created IPC channel is no longer required, its client and server handles should be deleted. The IPC channel can be created again if necessary.
Page topMessaging overview
Let us examine two entities (client and server) that have an IPC channel established between them. Let cl
be the client IPC handle of this channel while sr
is the server IPC handle of this channel.
The code provided below is intended to demonstrate the IPC mechanism. System calls are not normally used directly in entity code. To allow a convenient exchange of messages, special NK-generated methods are provided, which use system calls.
Client entity code:
client.c
…
// Get the client IPC handle cl using Service Locator
…
// Send request
Call(cl, &RequestBuffer, &ResponseBuffer);
…
Server entity code:
server.c
…
// Get the server IPC handle sr using Service Locator
…
// Receive request
Recv(sr, &RequestBuffer);
…
// Process request
…
// Send response
Reply(sr, &ResponseBuffer);
…
Messages are exchanged as follows:
- One of the client threads executes the
Call()
system call, passing as arguments thecl
handle (client handle of the utilized channel), the pointer to the buffer containing the request message, and the pointer to the buffer for the response. - The request message is sent to Kaspersky Security System to be checked. If Kaspersky Security System returns an "allowed" decision, we proceed to step 3. Otherwise, the
Call()
is terminated with thercSecurityDisallow
error code, and we proceed to step 9. - If the server is waiting for a request from this client—i.e. the server has executed the
Recv()
call, passingsr
as the first argument—we proceed to step 4. Otherwise, the client thread remains locked until one of the server threads executes aRecv()
system call with the firstsr
argument. - The request message is copied to the address space of the server. The server thread is unlocked, and the
Recv()
call is terminated with anrcOk
code. - The server processes the received message. The client thread remains locked.
- The server executes the
Reply()
system call, passing as arguments thesr
handle and the pointer to the buffer with the response message. - The response message is sent to Kaspersky Security System to be checked. If Kaspersky Security System returns an "allowed" decision, we proceed to step 8. Otherwise, the
Call()
andReply()
calls are terminated with anrcSecurityDisallow
- The response message is copied to the address space of the client. The server thread is unlocked, and the
Reply()
call is terminated with anrcOk
code. The client thread is unlocked, and theCall()
is terminated with anrcOk
code. - The exchange is complete.
If an error occurs when sending the request (insufficient memory, invalid message format, etc.), the threads are unlocked and the Call()
and Reply()
calls return an error code.
Service Locator
Service Locator is a small library that lets you:
- find out the value of an IPC handle based on the connection name;
- find out the value of the interface ID (RIID) based on the name of the interface implementation.
The values of the IPC handle and RIID are required for calling a specific interface of a specific server entity. These values are used when initializing a transport.
To use Service Locator, you need to include the sl_api.h
file in the entity code:
Main functions of Service Locator
Client-side functions:
ServiceLocatorConnect()
receives the connection name and returns the client IPC handle corresponding to this connection (channel).ServiceLocatorGetRiid()
receives the IPC handle and interface implementation name in the format<component instance name>.<interface implementation name>
. Returns a corresponding RIID (sequence number of the interface implementation).
The connection name is specified in the init.yaml
file, the component instance name is specified in the EDL file, and the interface implementation name is specified in the CDL file.
Server-side functions:
ServiceLocatorRegister()
receives the connection name and returns the server IPC handle corresponding to this connection (channel). If the channel is part of a group, the function returns the listener handle of this group.
Example use of Service Locator
Examine the next solution consisting of two entities: Client
and Server
.
The client entity does not implement any interface.
Client.edl
entity Client
The server entity contains an instance of the Ping
component. Instance name: ping_comp
.
Server.edl
entity Server
components {
ping_comp: Ping
}
The Ping
component contains a named implementation of the Ping
interface declared in the Ping.idl
file. Implementation name: ping_impl
.
Ping.cdl
component Ping
interfaces {
ping_impl: Ping
}
(For brevity, the Ping.idl
file is not provided.)
In the init description, we indicate that the Client
and Server
entities must be connected through a channel named server_connection
:
init.yaml
entities:
- name: Client
connections:
- target: Server
id: server_connection
- name: Server
Use of Service Locator on the client side:
client.c
…
/* Connect Service Locator */
…
/* Get client IPC handle "server_connection" channel */
Handle handle = ServiceLocatorConnect("server_connection");
…
/* Get ID (RIID) of ping_impl implementation contained in ping_comp instance */
nk_iid_t riid = ServiceLocatorGetRiid(handle, "ping_comp.ping_impl");
…
Use Service Locator on the server side:
server.c
…
/* Connect Service Locator */
…
nk_iid_t iid;
/* Get server IPC handle of "server_connection" channel */
Handle handle = ServiceLocatorRegister("server_connection", NULL, 0, &iid);
…
Channel groups
The channels in which an entity serves as a server may be combined into one or more groups.
Each group of channels has its own listener handle.
Two IPC channels have the same name, "second_connection_to_A", and are combined into a group. Another channel, named "first_connection_to_A", is not included in this group. Designations: cl_1, cl_2, cl_3 – client IPC handles; srv_1 – server IPC handle; ls_1 – listener IPC handle.
The listener handle allows the server to receive requests immediately over all channels in the group. There is no need to create a separate thread for each channel. It is enough to execute one Recv()
system call in one server thread by specifying a listener handle.
Creating a group of channels using an init description
For channels to form a group, they must connect to the same server entity and have identical names. For example, to produce the system of channels depicted above, the following init description can be used:
init.yaml
entities:
# Entity A acts as a server, so its list of connections is empty.
- name: A
# Entity B will be connected with entity A via two different channels.
- name: B
connections:
- target: A
id: first_connection_to_A
- target: A
id: second_connection_to_A
# Entity C will be connected with entity A via a channel named
# "second_connection_to_A". Two channels with identical names will be combined
# into a group: on the entity A side, they will have the same
# IPC handle (listener handle ls_1).
- name: C
connections:
- target: A
id: second_connection_to_A
Messaging within a channel group
Let us look at how messages are exchanged for the group of channels described above.
The client entities B
and C
receive a client handle value based on the name of the first_connection_to_A
connection and send a request:
entity_B.c, entity_C.c
…
// Receive client IPC handle cl corresponding to "second_connection_to_A".
Handle cl = ServiceLocatorConnect("second_connection_to_A");
…
// Send request
Call(cl, &RequestBuffer, &ResponseBuffer);
…
Both channels being used have the same name: second_connection_to_A
. However, by this name the entities B
and C
will receive different handle values for that name: entity B
will receive the value cl_2
, whereas entity C
will receive the value cl_3
.
Server entity A
receives the listener handle value ls_1
. Entity A
awaits requests over two channels at the same time (from B
and from C
), using the ls_1
handle. After receiving and processing the request, entity A
sends a response using the ls_1
handle. The response will be sent to the client that initiated the request:
entity_A.c
…
nk_iid_t iid;
// Receive listener handle ls_1 corresponding to
"second_connection_to_A"
Handle ls_1 = ServiceLocatorRegister("second_connection_to_A", NULL, 0, &iid);
…
// Wait for the request from entity B or C
Recv(ls_1, &RequestBuffer);
…
// Process received request
…
// Send response to client from which request was received
Reply(ls_1, &ResponseBuffer);
…
IPC message structure
In KasperskyOS, all interactions between entities have statically defined types. The permissible structures of an IPC message are defined in the IDL description of the interfaces of the entity that receives the message (server).
A correct IPC message (request and response) contains a constant part and an arena.
Constant part of a message
The constant part of a message contains arguments of a fixed size, and the RIID and MID.
Fixed-size arguments can be arguments of any IDL types except the sequence
type.
The RIID and MID identify the interface and method being called:
- The RIID (Runtime Implementation ID) is the number of the entity interface implementation being called, starting at zero.
- The MID (Method ID) is the number of the method within the interface that contains it, starting at zero.
The type of the constant part of the message is generated by the NK compiler based on the IDL description of the interface. A separate structure is generated for each interface method. Union
types are also generated for storing any request to a server, component or interface.
For example, for the Ping
method of the Ping
interface (the Ping
component of the Server
entity in the echo example), the NK compiler will create the Ping_Ping_req
type for the constant part of the request and the Ping_Ping_res
type for the constant part of the response. The following union
types will also be generated:
Ping_req
andPing_res
are constant parts of the request and response for any method of thePing
interface.Ping_component_req
andPing_component_res
are constant parts of the request and response for any method of any interface whose implementation is included in thePing
component.If embedded components are present, these types also contain structures of the constant part of a message for any method of any interface whose implementations are included in all embedded components. For more details, refer to Generated methods and types.
Server_entity_req
andServer_entity_res
are the constant parts of the request and response for any method of any interface whose implementation is included in any component whose instance is included in theServer
entity.
Arena
The arena is a buffer for storing variable-size arguments (sequence
IDL type).
Validating a message in Kaspersky Security System
The Kaspersky Security Module verifies that the structure of the message being sent is correct. Requests and responses are both validated. If the message has an incorrect structure, it will be rejected without calling the security model methods associated with it.
Forming a message structure
KasperskyOS Community Edition includes the following tools that make it easier for the developer to create and package an IPC message:
- The
transport-kos
library for working with NkKosTransport. - The NK compiler that lets you generate special methods and types.
The echo example shows the creation of a simple IPC message.
NkKosTransport
NkKosTransport
is a convenient wrapper for the Call
, Recv
and Reply
system calls. It lets you work separately with messages' constant parts and arenas.
The NkKosTransport
structure and methods for working with it are declared in the transport-kos.h
file.
To initialize the transport, it is sufficient to call the NkKosTransport_Init()
function by specifying the IPC handle of the channel that should be used to transmit messages (handle
):
…
NkKosTransport transport;
NkKosTransport_Init(&transport, handle, NK_NULL, 0);
A channel has two IPC handles: client and server. Therefore, when initializing the transport, you need to specify the client handle for the utilized channel on the client side and the server handle on the server side.
The functions nk_transport_call()
, nk_transport_recv()
and nk_transport_reply()
declared in transport.h
(included in the transport-kos.h
file) are used to call the transport.
The nk_transport_call()
function is intended for sending a request and receiving a response:
/* The constant part (req) and arena of the request (req_arena) must be initialized
* with the actual input arguments of the method being called. The constant part (req)
* must also contain the RIID and MID values.
* The nk_transport_call() function generates a request and executes the Call system call.
* The response received from the server is inserted into res (constant part of the response) and
* res_arena (response arena). */
nk_transport_call(&transport.base, (struct nk_message *)&req, &req_arena, (struct nk_message *)&res, &res_arena);
When using a generated interface method (for example, the IPing_Ping
method from the echo example) on the client side, the corresponding values of the RIID and MID are automatically inserted into the request, after which the nk_transport_call()
function is called.
The nk_transport_recv()
function is intended for receiving a request:
/* The nk_transport_recv () function executes the Recv system call.
* The request received from the client is inserted into req (constant part of the response) and
* req_arena (response arena). */
nk_transport_recv(&transport.base, (struct nk_message *)&req, &req_arena);
The nk_transport_reply()
function is intended for sending a response:
/* The constant part (res) and request arena (res_arena) must be initialized
* with the actual output arguments of the server method being called.
* The nk_transport_reply() function generates a response and executes a Reply system call. */
nk_transport_reply(&transport.base, (struct nk_message *)&res, &res_arena);
Generated methods and types
When building a solution, the NK compiler uses the EDL-, CDL- and IDL descriptions to generate a set of special methods and types that simplify the creation, forwarding, receipt and processing of IPC messages.
Examine the static description of the Server
entity from the echo example. This description consists of three files: Server.edl
, Ping.cdl
and Ping.idl
:
Server.edl
/* Description of the Server entity. */
entity Server
/* pingComp is a named instance of the Ping component. */
components {
pingComp: Ping
}
Ping.cdl
/* Description of the Ping component. */
component Ping
/* pingImpl is the Ping interface implementation. */
interfaces {
pingImpl: Ping
}
Ping.idl
/* Description of the Ping interface. */
package Ping
interface {
Ping(in UInt32 value, out UInt32 result);
}
These files will be used to generate the files named Server.edl.h
, Ping.cdl.h
, and Ping.idl.h
, which contain the following methods and types:
Methods and types that are common to the client and server
- Abstract interfaces containing the pointers to the implementations of the methods included in them.
In our example, one abstract interface (
Ping
) will be generated:struct Ping_ops {
nk_err_t (*Ping)(struct Ping *,
const struct Ping_req *,
const struct nk_arena *,
struct Ping_res *,
struct nk_arena *); };
struct Ping {
const struct Ping_ops *ops;
};
- Set of interface methods.
When calling an interface method, corresponding values of the RIID and MID are automatically inserted into the request, after which the
nk_transport_call()
function is called.In our example, a single
Ping_Ping
interface method will be generated:nk_err_t Ping_Ping(struct Ping *,
const struct Ping_Ping_req *,
const struct nk_arena *,
struct Ping_Ping_res *,
struct nk_arena *);
Methods and types used only on the client
- Types of proxy objects.
A proxy object is used as an argument in an interface method. In our example, a single
Ping_proxy
proxy object type will be generated:struct Ping_proxy {
struct Ping base;
struct nk_transport *transport;
nk_iid_t iid;
};
- Functions for initializing proxy objects.
In our example, the single initializing function
Ping_proxy_init
will be generated:void Ping_proxy_init(struct Ping_proxy *, struct nk_transport *, nk_iid_t);
- Types that define the structure of the constant part of a message for each specific method.
In our example, two such types will be generated:
Ping_Ping_req
(for a request) andPing_Ping_res
(for a response).struct Ping_Ping_req {
struct nk_message base_;
nk_uint32_t value;
};
struct Ping_Ping_res {
struct nk_message base_;
nk_uint32_t result;
};
Methods and types used only on the server
- Type containing implementations of all component interfaces, and the initializing function. (For each server component.)
If there are embedded components, this type also contains their instances, and the initializing function takes their corresponding initialized structures. Therefore, if embedded components are present, their initialization must begin with the most deeply embedded component.
In our example, the
Ping_component
structure andPing_component_init
function will be generated:struct Ping_component {
struct Ping *pingImpl;
};
void Ping_component_init(struct Ping_component *, struct Ping *);
- Type containing implementations of all interfaces provided directly by the server entity; all instances of components included in the server entity; and the initializing function.
In our example, the
Server_entity
structure andServer_entity_init
function will be generated:struct Server_entity {
struct Ping_component *pingComp;
};
void Server_entity_init(struct Server_entity *, struct Ping_component *);
- Types that define the structure of the constant part of a message for any method of a specific interface.
In our example, two such types will be generated:
Ping_req
(for a request) andPing_res
(for a response).union Ping_req {
struct nk_message base_;
struct Ping_Ping_req Ping;
};
union Ping_res {
struct nk_message base_;
struct Ping_Ping_res Ping;
};
- Types that define the structure of the constant part of a message for any method of any interface whose implementation is included in the specific component.
If embedded components are present, these types also contain structures of the constant part of a message for any method of any interface whose implementations are included in all embedded components.
In our example, two such types will be generated:
Ping_component_req
(for a request) andPing_component_res
(for a response).union Ping_component_req {
struct nk_message base_;
union Ping_req pingImpl;
};
union Ping_component_res {
struct nk_message base_;
union Ping_res pingImpl;
};
- Types that define the structure of the constant part of a message for any method of any interface whose implementation is included in any component whose instance is included in the server entity.
If embedded components are present, these types also contain structures of the constant part of a message for any method of any interface whose implementations are included in all embedded components.
In our example, two such types will be generated:
Server_entity_req
(for a request) andServer_entity_res
(for a response).union Server_entity_req {
struct nk_message base_;
union Ping_req pingComp_pingImpl;
};
union Server_entity_res {
struct nk_message base_;
union Ping_res pingComp_pingImpl;
};
- Dispatch methods (dispatchers) for a separate interface, component, or the entity.
Dispatchers analyze the received request (the RIID and MID values), call the implementation of the corresponding method, and then save the response in the buffer. In our example, three dispatchers will be generated:
Ping_dispatch
,Ping_component_dispatch
, andServer_entity_dispatch
.The entity dispatcher handles the request and calls the methods implemented by this entity. If the request contains an incorrect RIID (for example, an RIID for a different interface implementation that this entity does not have) or an incorrect MID, the dispatcher returns
NK_EOK
orNK_ENOENT
.nk_err_t Server_entity_dispatch(struct Server_entity *,
const union Server_entity_req *,
const struct nk_arena *,
union Server_entity_res *,
struct nk_arena *);
In special cases, you can use dispatchers of the interface and the component. They take an additional argument: interface implementation ID (
nk_iid_t
). The request will be handled only if the passed argument and RIID from the request match, and if the MID is correct. Otherwise, the dispatchers returnNK_EOK
orNK_ENOENT
.nk_err_t Ping_dispatch(struct Ping *,
nk_iid_t,
const union Ping_req *,
const struct nk_arena *,
union Ping_res *,
struct nk_arena *);
nk_err_t Ping_component_dispatch(struct Ping_component *,
nk_iid_t,
const union Ping_component_req *,
const struct nk_arena *,
union Ping_component_res *,
struct nk_arena *);
Tools for building a solution
This section contains a description of the scripts, tools, compilers and build templates provided in KasperskyOS Community Edition.
Scripts and compilers
This section contains a description of the scripts, tools and compilers provided in KasperskyOS Community Edition.
Build scripts and tools
KasperskyOS Community Edition includes the following build scripts and tools:
- nk-gen-c
The NK compiler (
nk-gen-c
) generates the set of transport methods and types based on the EDL, CDL and IDL descriptions of applications, components and interfaces. The transport methods and types are needed for generating, sending, receiving and processing IPC messages. - nk-psl-gen-c
The
nk-psl-gen-c
compiler generates the source code of the Kaspersky Security System security module based on the solution security policy file (security.psl
) and the EDL descriptions of applications included in the solution. - einit
The
einit
tool lets you automate the creation of code of theEinit
initializing application. This application is the first to start when KasperskyOS is loaded. Then it starts the other applications and creates channels (connections) between them. - makekss
The
makekss
script creates the Kaspersky Security System security module for the KasperskyOS kernel. - makeimg
The
makeimg
script creates the final boot image of the KasperskyOS-based solution with all applications to be started and the Kaspersky Security System module.
nk-gen-c
The NK compiler (nk-gen-c
) generates the set of transport methods and types based on the EDL, CDL and IDL descriptions of the process classes, components and interfaces. The transport methods and types are needed for generating, sending, receiving and processing IPC messages.
Transport methods and types are generated with fully qualified names. The full name of the process class/component/interface is used as prefixes in names (declared in the corresponding EDL-, CDL- or IDL file) by replacing dots with underscores (_
).
The NK compiler receives the EDL, CDL or IDL file and creates the following files:
H
file containing a declaration and implementation of transport methods and types.D
file that lists the dependencies of the createdC
file. This file can be used for building automation using themake
tool.
Syntax for using the NK compiler:
nk-gen-c [-I PATH][-o PATH][--types][--interface][--client][--server][--extended-errors][--enforce-alignment-check][--help][--version] FILE
Parameters:
FILE
Path to the EDL-, CDL- or IDL description of the process class, component or interface for which you need to generate transport methods and types.
-I PATH
Path to the folder containing auxiliary files required for generating transport methods and types. By default, these files are located in the folder:
/opt/KasperskyOS-Community-Edition-<version>/sysroot-arm-kos/include
.It may also be used for adding other folders to search for the files required for generating the methods and types.
To indicate more than one folder. you can use several
-I
switches.-o PATH
Path to an existing folder where files containing transport methods and types will be created.
-h, --help
Displays the Help text.
--version
Displays the
nk-gen-c
version.--enforce-alignment-check
Enables mandatory alignment checks for queries to memory, even if this check is disabled for the target platform. If these checks are enabled, the NK compiler adds additional alignment checks to the code of the IPC message validators.
By default, memory query alignment check settings are defined for each platform in the file named
system.platform
.--extended-errors
Enables extended error handling in the code of client and server methods.
Selective generation
To reduce the amount of code generated by the NK compiler, you can use selective generation flags. For example, it is convenient to use the --server
flag for applications that implement interfaces, and to use the --client
flag for applications that are clients of the interfaces.
If no selective generation flag is specified, the NK compiler will create all transport types and methods that are possible for the specified file.
Selective generation flags for descriptions of interfaces (IDL files):
--types
The compiler will create only files that contain all constants and types, including redefined (
typedef
), from the input IDL file, and the types from imported IDL files that are used in the types of the input file.However, constants and redefined types from imported IDL files will not be explicitly included in the generated files. If you need to use types from imported files in code, you need to separately generate H-files for each such IDL file.
--interface
The compiler will generate files created with the
--types
flag, and the structures of request and response messages for all methods of this interface.--client
The compiler will generate files created with the
--interface
flag, and the client proxy objects and functions of their initialization for all methods of this interface.--server
The compiler will generate files created with the
--interface
flag, and the types and methods of the dispatcher of this interface.
Selective generation flags for descriptions of components (CDL files) and process classes (EDL files):
--types
The compiler will generate files created with the
--types
flag for all interfaces used in this component/process class.However, only the types that are used in arguments of interface methods will be explicitly included in the generated files.
--interface
The compiler will generate files created with the
--types
flag for this component/process class, and files generated with the--interface
flag for all interfaces used in this component/process class.--client
The compiler will generate files created with the
--interface
flag, and the client proxy objects and functions of their initialization for all interfaces used in this component/process class.--server
The compiler will generate files created with the
--interface
flag, and the types and methods of the dispatcher of this component/process class and the types and methods of dispatchers for all interfaces used in this component/process class.
nk-psl-gen-c
The nk-psl-gen-c
compiler generates the source code of the Kaspersky Security System security module based on the solution security policy file (security.psl) and the EDL descriptions of process classes included in the solution. This code is used by the makekss script.
The nk-psl-gen-c
compiler also lets you generate and run code of test scenarios written in the PAL language for the solution security policy.
Syntax for using the nk-psl-gen-c
compiler:
nk-psl-gen-c [-I PATH][-o PATH][--audit PATH][--tests ARG][--help][--version] FILE
Parameters:
FILE
Path to the PSL description of the solution security policy (
security.psl
)-I,--include-dir PATH
Path to the folder containing auxiliary files required for generating transport methods and types. By default, these files are located in the folder:
/opt/KasperskyOS-Community-Edition-<version>/sysroot-arm-kos/include
.The
nk-psl-gen-c
compiler will require access to all EDL descriptions of process classes listed in the security configuration, and will require access to the CDL- or IDL descriptions of their components and interfaces. To enable thenk-psl-gen-c
compiler to find these descriptions, you need to pass the paths to these descriptions using the-I
switch.To indicate more than one folder. you can use several
-I
switches.-o,--output PATH
Path to the created file containing the security module code.
-t, --tests ARG
Flag for controlling code generation and starting test scenarios for the solution security policy. Possible values:
skip
– code of test scenarios is not generated. This value is used by default if the--tests
flag is not indicated.generate
– code of test scenarios is generated but is not compiled and is not executed.run
– code of test scenarios is generated, compiled using thegcc
compiler, and executed.
-a, --audit PATH
Path to the created file containing the code of the audit decoder.
-h, --help
Displays the Help text.
--version
Displays the
nk-psl-gen-c
version.
einit
The einit
tool lets you automate the creation of code of the Einit initializing application. This application is the first to start when KasperskyOS is loaded. Then it starts the other applications and creates channels (connections) between them.
The einit
tool receives the init description file (init.yaml
by default) and creates a .c file containing the code of the Einit
initializing application. Then the Einit
application must be built using the C compiler that is provided in KasperskyOS Community Edition.
Syntax for using the einit tool:
einit -I PATH -o PATH [--help] FILE
Parameters:
FILE
Path to the
init.yaml
file containing descriptions of process classes and connections.-I PATH
Path to the folder containing auxiliary files required for generating the initializing application. By default, these files are located in the folder:
/opt/KasperskyOS-Community-Edition-<version>/sysroot-arm-kos/include
.-o, --out-file PATH
Path to the created .c file containing the code of the initializing application.
-h, --help
Displays the Help text.
makekss
The makekss
script creates the security module of Kaspersky Security System.
The script calls the nk-psl-gen-c compiler to generate the source code of the security module, then compiles the resulting code by calling the C compiler that is provided in KasperskyOS Community Edition.
The script obtains the file containing a description of the solution security policy (security.psl
by default) and creates the ksm.module
security module file.
Syntax for using the makekss script:
makekss --target=ARCH --module=PATH --with-nk="PATH" --with-nktype="TYPE" --with-nkflags="FLAGS" [--output="PATH"][--help][--with-cc="PATH"][--with-cflags="FLAGS"] FILE
Parameters:
FILE
Path to the security configuration file (
.psl
).--target=ARCH
Architecture for which the build is intended.
--module=-lPATH
Path to the
ksm_kss
library. This key is passed to the C compiler for linking to this library.--with-nk=PATH
Path to the
nk-psl-gen-c
compiler that will be used to generate the source code of the security module. By default, the compiler is located in/opt/KasperskyOS-Community-Edition-<version>/toolchain/bin/nk-psl-gen-c
.--with-nktype="TYPE"
Indicates the type of NK compiler that will be used. To use the
nk-psl-gen-c
compiler, indicate thepsl
type.--with-nkflags="FLAGS"
Parameters used when calling the
nk-psl-gen-c
compiler.The
nk-psl-gen-c
compiler will require access to all EDL descriptions of process classes listed in the security configuration, and will require access to the CDL- or IDL descriptions of their components and interfaces. To enable thenk-psl-gen-c
compiler to find these descriptions, you need to pass the paths to these descriptions in the--with-nkflags
parameter by using the-I
switch of thenk-psl-gen-c
compiler.--output=PATH
Path to the created security module file.
--with-cc=PATH
Path to the C compiler that will be used to build the security module. The compiler provided in KasperskyOS Community Edition is used by default.
--with-cflags=FLAGS
Parameters used when calling the C compiler.
-h, --help
Displays the Help text.
makeimg
The makeimg
script creates the final boot image of the KasperskyOS-based solution with all applications to be started and the Kaspersky Security System module.
The script receives a list of files, including the executable files of all applications that need to be added to ROMFS of the loaded image, and creates the following files:
- Solution image
- Solution image without character tables (
.stripped
) - Solution image with debug character tables (
.dbg.syms
)
Syntax for using the makeimg script:
makeimg --target=ARCH --sys-root=PATH
--with-toolchain=PATH --ldscript=PATH --img-src=PATH
--img-dst=PATH
--with-init=PATH
[--with-extra-asflags=FLAGS][--with-extra-ldflags=FLAGS][--help] FILES
Parameters:
FILES
List of paths to files, including the executable files of all applications that need to be added to romfs.
The security module (
ksm.module
) must be explicitly specified, or else it will not be included in the solution image. TheEinit
application does not need to be indicated because it will be automatically included in the solution image.--target=ARCH
Architecture for which the build is intended.
--sys-root=PATH
Path to the root directory sysroot. By default, this directory is located in
/opt/KasperskyOS-Community-Edition-<version>/sysroot-arm-kos/
.--with-toolchain=PATH
Path to the set of auxiliary tools required for the solution build. By default, these tools are located in
/opt/KasperskyOS-Community-Edition-<version>/toolchain/
.--ldscript=PATH
Path to the linker script required for the solution build. By default, this script is located in
/opt/KasperskyOS-Community-Edition-<version>/libexec/arm-kos/
.--img-src=PATH
Path to the precompiled KasperskyOS kernel not containing the romfs. By default, the kernel is located in
/opt/KasperskyOS-Community-Edition-<version>/libexec/arm-kos/
.--img-dst=PATH
Path to the created image file.
--with-init=PATH
Path to the executable file of the
Einit
initializing application.--with-extra-asflags=FLAGS
Additional flags for the AS Assembler.
--with-extra-ldflags=FLAGS
Additional flags for the LD Linker.
-h, --help
Displays the Help text.
Cross compilers
Properties of KasperskyOS cross compilers
The cross compilers included in KasperskyOS Community Edition support processors that have the arm
architecture.
The KasperskyOS Community Edition toolchain includes the following tools for cross compilation:
- GCC:
arm-kos-gcc
arm-kos-g++
- Binutils:
- AS Assembler:
arm-kos-as
- LD Linker:
arm-kos-ld
- AS Assembler:
In addition to standard macros, an additional macro __KOS__=1
is defined in GCC. Using this macro lets you simplify porting of the software code to KasperskyOS, and also simplifies development of platform-independent applications.
To view the list of standard macros of GCC, run the following command:
echo '' | arm-kos-gcc -dM -E -
Linker operation specifics
When building the executable file of an application, by default the linker links the following libraries in the specified order:
- libc – standard C library.
- libm – library that implements the mathematical functions of the standard C language library.
- libvfs_stubs – library that contains stubs of I/O functions (for example,
open
,socket
,read
,write
). - libkos – library consisting of two parts. The first part provides the C interface for accessing KasperskyOS kernel functions. It is available through the header files in the
coresrv
folder, for example:#include <coresrv/vmm/vmm_api.h>
. The second part of the libkos library is a wrapper over the first part and contains additional synchronization functions:mutex
,semaphore
,event
. Other libraries (including libc) interact with the kernel through the libkos library. - libenv – client library of the subsystem for configuring the environment of applications (environmental variables, arguments of the
main
function, and custom configurations). - libsrvtransport-u – internal library with the implementation of transport of interprocess communication between KasperskyOS kernel services.
Preparing the solution's boot image
To automate the process of preparing the solution's boot image, you need to configure a build system. You can base this system on the build system implemented in the examples.
This section describes various methods for configuring a build system to prepare a solution's boot image.
Using a Makefile template from the contents of KasperskyOS Community Edition
To simplify the process of preparing the solution's boot image using the make
build system, you can use the build.mk
template from KasperskyOS Community Edition. The template file is located at the following path:
/opt/KasperskyOS-Community-Edition-<version>/common/build.mk
To prepare a make
build system using the build.mk
template, do the following in the Makefile
build script:
- Specify the value of the
targets
variable. In the variable value, list all applications included in the solution, separating them with a space. You are not required to specify theEinit
orkl.core.Core
applications because they are processed separately. - For each application specified in the
targets
variable, specify the values for the following variables:<application-name>-objects
– list of object files of the application. You must list all object files.The names of object files are taken from the names of the entity source files according to the following rules:
*.c → *.o
*.idl → *.idl.o
*.cdl → *.cdl.o
*.edl → *.edl.o
<application-name>-ldflags
– list of flags passed to the linker. If the entity uses the virtual file system, you must pass the flags specified in theLIBVFS_REMOTE
variable.<application-name>-base
– application load address in hexadecimal. If this variable is not specified, the address is assigned automatically. Use this variable for debugging the application.
This same address is passed to the debugger using the following command, which can be added to the .gdbinit file:
add-symbol-file <application-name> <application-load-address>
- If the ROMFS partition must contain additional files, define the value for the
ROMFS-FILES
variable. In the variable value, list the files while separating them with blank spaces. If you are using the virtual file system, you must pass the file specified in theVFS_ENTITY
variable. - Add a statement for importing the build.mk template using the following command:
include /opt/KasperskyOS-Community-Edition-<version>/common/build.mk
The build.mk
template file has the following build targets:
- sim (default target) – start the solution's boot image using the QEMU emulator.
When started, the QEMU emulator may capture the mouse and will indicate this in the title bar of the emulator window.
- Kos-image – build the solution's boot image to be started on the target hardware platform.
- gdbsim – start the solution's boot image with the capability for debugging. After QEMU emulation is started, it awaits the start of the debugger. For this, call the gdb target in a different command line.
Make sure that TCP/IP port 1234 is open by using the
netstat -anput
command, for example.Port 1234 is monitored by the gdbserver program, which is used for remote debugging of applications and is part of the QEMU emulator.
When using the gdb debugger, you must use hardware breakpoints (hbreak). The QEMU emulator used in the examples is started with the
-enable-kvm
key, which makes it impossible to use regular breakpoints. - gdb – start the debugger. After starting the debugger for applications that require debugging, run the following commands:
add-symbol-file <application-name> <application-load-address>
target remote localhost:1234
Example
This example uses the make build system. In addition to the actions executed in the build.mk template, in the build script you must specify the Hello
application and the list of object files of this application. The load address is specified for solution debugging purposes.
Makefile
# List of applications for the build.
targets = hello
# List of object files of the Hello application
hello-objects = hello.o hello.edl.o
# Load address of the Hello entity (in hex)
hello-base = 800000
# Include template with general build rules.
include ../common/build.mk
To start the make build system, run the make hello
command.
To run the hello example while in the folder /opt/KasperskyOS-Community-Edition-<version>/examples/hello
, run the make
command.
Using CMake from the contents of KasperskyOS Community Edition
The CMake
build automation system supports cross compilation of applications. To perform cross compilation using CMake
, specify the path to a file with the build system extension (toolchain.cmake
).
To cross compile an application for KasperskyOS, define the value of the variable: DCMAKE_TOOLCHAIN_FILE=/opt/KasperskyOS-Community-Edition-<version>/toolchain/share/toolchain.cmake
KasperskyOS Community Edition includes the platform
library containing a set of ready-to-use scripts for the CMake
system.
To prepare an application for debugging:
- In the CMakeLists.txt file, define the
LINK_FLAGS
parameter value for the application that you want to debug as follows:set_target_properties (<application-name> PROPERTIES LINK_FLAGS "-Ttext <text-section-address>")
The script automatically creates .gdbinit files. The set of commands for the GDB debugger are contained within .gdbinit files. This set of commands determines which address the GDB debugger will use to load entities for debugging.
- Build the application.
Using your own build system
You can use other build systems or implement your own build system to prepare the solution's boot image.
To prepare the solution's boot image, the build system must include the following actions:
- Generate code of the transport methods and types used to create, send, receive and process IPC messages between entities that are included in the solution.
Use the NK compiler for this purpose. In command arguments, relay the path to the files containing EDL-, CDL- and IDL descriptions of entities, components and interfaces.
- Build all entities included in the solution.
To do so, use the cross compilers that are included in KasperskyOS Community Edition.
- Build an Einit initializing entity.
Use the einit tool to generate the code of the Einit entity. In the command arguments, pass the path to the init description file (
init.yaml
by default).Then the
Einit
entity must be built using the C compiler that is provided in KasperskyOS Community Edition. - Build the kernel module with Kaspersky Security System.
To do so, use the makekss script. In the command arguments, relay the path to the security configuration file (
security.psl
by default). - Create an image of the solution.
To do so, use the makeimg script. In the command arguments, pass the executable ELF files of entities, the kernel module with Kaspersky Security System, the KasperskyOS kernel image, and any additional files.
Deploying the solution's boot image on target devices
To deploy the solution's boot image on the target device:
- Connect the data storage drive from which you plan to run the solution's boot image on target devices.
- Find the block device corresponding to the connected drive, for example, by using the following command:
fdisk -l
- If required, on the storage drive, create a new partition on which the solution's boot image will be deployed by using the
fdisk
tool, for example. - If there is no file system on the partition, create one by using the
mkfs
tool, for example.You can use any file system that is supported by the GRUB 2 bootloader.
- Mount the data drive.
mkdir /mnt/kos_device && mount /dev/sdXY /mnt/kos_device
Here,
/mnt/kos_device
is the mount point,/dev/sdXY
is the block device name,X
is the letter corresponding to the connected drive, andY
is the partition number. - Install the GRUB 2 operating system bootloader on the drive.
To install GRUB 2, run the following command:
grub-install --force --removable \
--boot-directory=/mnt/kos_device/boot /dev/sdX
Here,
/mnt/kos_device
is the mount point,/dev/sdX
is the block device name, andX
is the letter corresponding to the connected drive. - Copy the boot image of the solution to the root directory of the mounted drive.
- In the
/mnt/kos_device/boot/grub/grub.cfg
file, add themenuentry
section that points to the solution's boot image.menuentry "KasperskyOS" {
multiboot /my_kasperskyos.img
boot
}
- Unmount the drive.
umount /mnt/kos_device
Here,
/mnt/kos_device
is the mount point.
After performing these actions, you can start KasperskyOS from this drive.
Page top