Referência da API Trusty

O Trusty oferece APIs para desenvolver duas classes de apps e serviços:

  • Apps e serviços confiáveis executados no processador TEE
  • Apps normais e não confiáveis que são executados no processador principal e usam os serviços fornecidos por apps confiáveis

A API Trusty geralmente descreve o sistema de comunicação entre processos (IPC) do Trusty, incluindo comunicações com o mundo não seguro. O software executado no processador principal pode usar APIs Trusty para se conectar a apps e serviços confiáveis e trocar mensagens arbitrárias com eles, assim como um serviço de rede por IP. Cabe ao app determinar o formato de dados e a semântica dessas mensagens usando um protocolo no nível do app. A entrega confiável de mensagens é garantida pela infraestrutura Trusty (na forma de drivers executados no processador principal), e a comunicação é totalmente assíncrona.

Portas e canais

As portas são usadas por apps do Trusty para expor endpoints de serviço na forma de um caminho nomeado ao qual os clientes se conectam. Isso fornece um ID de serviço simples e baseado em string para os clientes usarem. A convenção de nomenclatura é do estilo DNS reverso, por exemplo, com.google.servicename.

Quando um cliente se conecta a uma porta, ele recebe um canal para interagir com um serviço. O serviço precisa aceitar uma conexão de entrada e, quando o faz, também recebe um canal. Em essência, as portas são usadas para procurar serviços e, em seguida, a comunicação ocorre por um par de canais conectados (ou seja, instâncias de conexão em uma porta). Quando um cliente se conecta a uma porta, uma conexão simétrica bidirecional é estabelecida. Usando esse caminho full-duplex, clientes e servidores podem trocar mensagens arbitrárias até que qualquer lado decida encerrar a conexão.

Somente apps confiáveis do lado seguro ou módulos do kernel Trusty podem criar portas. Os apps executados no lado não seguro (no mundo normal) só podem se conectar a serviços publicados pelo lado seguro.

Dependendo dos requisitos, um app confiável pode ser um cliente e um servidor ao mesmo tempo. Um app confiável que publica um serviço (como um servidor) pode precisar se conectar a outros serviços (como um cliente).

API Handle

Os identificadores são números inteiros não assinados que representam recursos, como portas e canais, de forma semelhante aos descritores de arquivos no UNIX. Depois que os identificadores são criados, eles são colocados em uma tabela de identificadores específica do app e podem ser referenciados mais tarde.

Um autor da chamada pode associar dados privados a um identificador usando o método set_cookie().

Métodos na API Handle

Os identificadores só são válidos no contexto de um app. Um app não pode transmitir o valor de um identificador para outros apps, a menos que seja especificado explicitamente. Um valor de identificador só pode ser interpretado comparando-o com o INVALID_IPC_HANDLE #define,, que um app pode usar como uma indicação de que um identificador é inválido ou não definido.

Associa os dados privados fornecidos pelo autor da chamada a um identificador especificado.

long set_cookie(uint32_t handle, void *cookie)

[in] handle: qualquer identificador retornado por uma das chamadas de API

[in] cookie: ponteiro para dados arbitrários do espaço do usuário no app Trusty

[retval]: NO_ERROR em caso de sucesso, código de erro < 0, caso contrário

Essa chamada é útil para processar eventos quando eles ocorrem mais tarde, depois que o identificador foi criado. O mecanismo de processamento de eventos fornece o identificador e o cookie de volta ao manipulador de eventos.

É possível aguardar eventos para identificadores usando a chamada wait().

wait()

Aguarda que um evento ocorra em um determinado identificador por um período especificado.

long wait(uint32_t handle_id, uevent_t *event, unsigned long timeout_msecs)

[in] handle_id: qualquer identificador retornado por uma das chamadas de API

[out] event: um ponteiro para a estrutura que representa um evento ocorrido nesse identificador

[in] timeout_msecs: um valor de tempo limite em milissegundos. Um valor de -1 é um tempo limite infinito.

[retval]: NO_ERROR se um evento válido ocorreu em um intervalo de tempo limite especificado. ERR_TIMED_OUT se um tempo limite especificado expirou, mas nenhum evento ocorreu. < 0 para outros erros.

Após o sucesso (retval == NO_ERROR), a chamada wait() preenche uma estrutura uevent_t especificada com informações sobre o evento que ocorreu.

typedef struct uevent {
    uint32_t handle; /* handle this event is related to */
    uint32_t event;  /* combination of IPC_HANDLE_POLL_XXX flags */
    void    *cookie; /* cookie associated with this handle */
} uevent_t;

O campo event contém uma combinação dos seguintes valores:

enum {
  IPC_HANDLE_POLL_NONE    = 0x0,
  IPC_HANDLE_POLL_READY   = 0x1,
  IPC_HANDLE_POLL_ERROR   = 0x2,
  IPC_HANDLE_POLL_HUP     = 0x4,
  IPC_HANDLE_POLL_MSG     = 0x8,
  IPC_HANDLE_POLL_SEND_UNBLOCKED = 0x10,
   more values[TBD]
};

IPC_HANDLE_POLL_NONE: nenhum evento está pendente. O autor da chamada precisa reiniciar a espera.

IPC_HANDLE_POLL_ERROR: ocorreu um erro interno não especificado

IPC_HANDLE_POLL_READY: depende do tipo de identificador, conforme abaixo:

  • Para portas, esse valor indica que há uma conexão pendente.
  • Para canais, esse valor indica que uma conexão assíncrona (consulte connect()) foi estabelecida.

Os eventos a seguir são relevantes apenas para canais:

  • IPC_HANDLE_POLL_HUP: indica que um canal foi fechado por um par
  • IPC_HANDLE_POLL_MSG: indica que há uma mensagem pendente para esse canal
  • IPC_HANDLE_POLL_SEND_UNBLOCKED: indica que um autor da chamada bloqueado anteriormente pode tentar enviar uma mensagem de novo. Consulte a descrição de send_msg() para saber mais.

Um gerenciador de eventos precisa estar preparado para processar uma combinação de eventos especificados, já que vários bits podem ser definidos ao mesmo tempo. Por exemplo, em um canal, é possível ter mensagens pendentes e uma conexão fechada por um peer ao mesmo tempo.

A maioria dos eventos é fixa. Elas persistem enquanto a condição subjacente persistir. Por exemplo, todas as mensagens pendentes são recebidas e as solicitações de conexão pendentes são processadas. A exceção é o caso do evento IPC_HANDLE_POLL_SEND_UNBLOCKED, que é limpo após uma leitura, e o app tem apenas uma chance de processá-lo.

Os identificadores podem ser destruídos chamando o método close().

close()

Destrói o recurso associado ao identificador especificado e o remove da tabela de identificadores.

long close(uint32_t handle_id);

[in] handle_id: identificador para destruir

[retval]: 0 em caso de sucesso. Caso contrário, um erro negativo

API Server

Um servidor começa criando uma ou mais portas nomeadas que representam os endpoints do serviço. Cada porta é representada por um identificador.

Métodos na API Server

port_create()

Cria uma porta de serviço nomeada.

long port_create (const char *path, uint num_recv_bufs, size_t recv_buf_size,
uint32_t flags)

[in] path: o nome da string da porta (conforme descrito acima). Esse nome precisa ser exclusivo no sistema. As tentativas de criar uma duplicação falham.

[in] num_recv_bufs: o número máximo de buffers que um canal nessa porta pode pré-alocar para facilitar a troca de dados com o cliente. Os buffers são contados separadamente para dados que vão em ambas as direções. Portanto, especificar 1 aqui significa que 1 buffer de envio e 1 de recebimento são pré-alocados. Em geral, o número de buffers necessários depende do contrato de protocolo de nível superior entre o cliente e o servidor. O número pode ser tão baixo quanto 1 no caso de um protocolo muito síncrono (enviar mensagem, receber resposta antes de enviar outra). Mas o número pode ser maior se o cliente espera enviar mais de uma mensagem antes que uma resposta seja mostrada (por exemplo, uma mensagem como prólogo e outra como o comando real). Os conjuntos de buffer alocados são por canal. Portanto, duas conexões (canais) separadas teriam conjuntos de buffer separados.

[in] recv_buf_size: tamanho máximo de cada buffer individual no conjunto de buffers acima. Esse valor depende do protocolo e limita o tamanho máximo da mensagem que você pode trocar com o peer.

[in] flags: uma combinação de flags que especifica o comportamento adicional da porta

Esse valor precisa ser uma combinação dos seguintes valores:

IPC_PORT_ALLOW_TA_CONNECT: permite uma conexão de outros apps seguros

IPC_PORT_ALLOW_NS_CONNECT: permite uma conexão do mundo não seguro

[retval]: identificador da porta criada se não for negativo ou um erro específico se for negativo

Em seguida, o servidor pesquisa a lista de identificadores de porta para conexões recebidas usando a chamada wait(). Ao receber uma solicitação de conexão indicada pelo bit IPC_HANDLE_POLL_READY definido no campo event da estrutura uevent_t, o servidor precisa chamar accept() para concluir o estabelecimento de uma conexão e criar um canal (representado por outro identificador) que pode ser consultado para mensagens recebidas.

accept()

Aceita uma conexão de entrada e recebe um identificador de um canal.

long accept(uint32_t handle_id, uuid_t *peer_uuid);

[in] handle_id: identificador que representa a porta à qual um cliente se conectou

[out] peer_uuid: ponteiro para uma estrutura uuid_t a ser preenchida com o UUID do app cliente de conexão. Ele é definido como todos os zeros se a conexão tiver origem no mundo não seguro.

[retval]: identificador de um canal (se não for negativo) em que o servidor pode trocar mensagens com o cliente (ou um código de erro, caso contrário)

API do cliente

Esta seção contém os métodos na API Client.

Métodos na API Client

connect()

Inicia uma conexão com uma porta especificada por nome.

long connect(const char *path, uint flags);

[in] path: nome de uma porta publicada por um app do Trusty

[in] flags: especifica um comportamento opcional adicional

[retval]: identificador de um canal em que as mensagens podem ser trocadas com o servidor; erro se negativo

Se nenhum flags for especificado (o parâmetro flags for definido como 0), a chamada connect() inicia uma conexão síncrona para uma porta especificada que retorna imediatamente um erro se a porta não existir e cria um bloqueio até que o servidor aceite uma conexão.

Esse comportamento pode ser alterado especificando uma combinação de dois valores, conforme descrito abaixo:

enum {
IPC_CONNECT_WAIT_FOR_PORT = 0x1,
IPC_CONNECT_ASYNC = 0x2,
};

IPC_CONNECT_WAIT_FOR_PORT: força uma chamada connect() a aguardar se a porta especificada não existir imediatamente na execução, em vez de falhar imediatamente.

IPC_CONNECT_ASYNC: se definido, inicia uma conexão assíncrona. Um app precisa detectar o identificador retornado chamando wait() para um evento de conclusão de conexão indicado pelo bit IPC_HANDLE_POLL_READY definido no campo de evento da estrutura uevent_t antes de iniciar a operação normal.

API Messaging

As chamadas da API Messaging permitem o envio e a leitura de mensagens por uma conexão (canal) estabelecida anteriormente. As chamadas da API Messaging são as mesmas para servidores e clientes.

Um cliente recebe um identificador de canal emitindo uma chamada connect(), e um servidor recebe um identificador de canal de uma chamada accept(), como descrito acima.

Estrutura de uma mensagem do Trusty

Como mostrado abaixo, as mensagens trocadas pela API Trusty têm uma estrutura mínima, deixando que o servidor e o cliente concordem com a semântica do conteúdo real:

/*
 *  IPC message
 */
typedef struct iovec {
        void   *base;
        size_t  len;
} iovec_t;

typedef struct ipc_msg {
        uint     num_iov; /* number of iovs in this message */
        iovec_t  *iov;    /* pointer to iov array */

        uint     num_handles; /* reserved, currently not supported */
        handle_t *handles;    /* reserved, currently not supported */
} ipc_msg_t;

Uma mensagem pode ser composta por um ou mais buffers não contíguos representados por uma matriz de estruturas iovec_t. O Trusty realiza leituras e gravações de dispersão nesses blocos usando a matriz iov. O conteúdo dos buffers que podem ser descritos pela matriz iov é completamente arbitrário.

Métodos na API Messaging

send_msg()

Envia uma mensagem por um canal especificado.

long send_msg(uint32_t handle, ipc_msg_t *msg);

[in] handle: identificador do canal pelo qual a mensagem será enviada

[in] msg: ponteiro para o ipc_msg_t structure que descreve a mensagem

[retval]: número total de bytes enviados em caso de sucesso. Caso contrário, uma mensagem de erro negativa

Se o cliente (ou servidor) estiver tentando enviar uma mensagem pelo canal e não houver espaço na fila de mensagens de peer de destino, o canal poderá entrar em um estado de envio bloqueado (isso nunca deve acontecer em um protocolo de solicitação/resposta síncrono simples, mas pode acontecer em casos mais complicados) que é indicado pelo retorno de um código de erro ERR_NOT_ENOUGH_BUFFER. Nesse caso, o autor da chamada precisa esperar até que o peer libere algum espaço na fila de recebimento, recuperando as mensagens de processamento e desativação, indicadas pelo bit IPC_HANDLE_POLL_SEND_UNBLOCKED definido no campo event da estrutura uevent_t retornado pela chamada wait().

get_msg()

Recebe metainformações sobre a próxima mensagem em uma fila de mensagens recebidas

de um canal específico.

long get_msg(uint32_t handle, ipc_msg_info_t *msg_info);

[in] handle: identificador do canal em que uma nova mensagem precisa ser recuperada

[out] msg_info: a estrutura de informações da mensagem é descrita da seguinte forma:

typedef struct ipc_msg_info {
        size_t    len;  /* total message length */
        uint32_t  id;   /* message id */
} ipc_msg_info_t;

Cada mensagem recebe um ID exclusivo no conjunto de mensagens pendentes, e o comprimento total de cada mensagem é preenchido. Se configurado e permitido pelo protocolo, pode haver várias mensagens pendentes (abertas) de uma só vez para um canal específico.

[retval]: NO_ERROR em caso de sucesso. Caso contrário, um erro negativo

read_msg()

Lê o conteúdo da mensagem com o ID especificado a partir do deslocamento especificado.

long read_msg(uint32_t handle, uint32_t msg_id, uint32_t offset, ipc_msg_t
*msg);

[in] handle: identificador do canal em que a mensagem será lida

[in] msg_id: ID da mensagem a ser lida

[in] offset: deslocamento na mensagem a partir da qual começar a ler

[out] msg: ponteiro para a estrutura ipc_msg_t que descreve um conjunto de buffers para armazenar dados de mensagens de entrada.

[retval]: número total de bytes armazenados nos buffers msg em sucesso. Caso contrário, uma mensagem de erro negativa.

O método read_msg pode ser chamado várias vezes, começando em um deslocamento diferente (não necessariamente sequência) conforme necessário.

put_msg()

Retira uma mensagem com um ID especificado.

long put_msg(uint32_t handle, uint32_t msg_id);

[in] handle: identificador do canal em que a mensagem chegou

[in] msg_id: ID da mensagem que está sendo desativada

[retval]: NO_ERROR em caso de sucesso. Caso contrário, um erro negativo

O conteúdo da mensagem não pode ser acessado depois que ela é desativada e o buffer que ela ocupa é liberado.

API File Descriptor

A API File Descriptor inclui chamadas read(), write() e ioctl(). Todas essas chamadas podem operar em um conjunto predefinido (estático) de descritores de arquivos tradicionalmente representados por números pequenos. Na implementação atual, o espaço do descritor de arquivo é separado do espaço do identificador IPC. A API File Descriptor no Trusty é semelhante a uma API tradicional baseada em descritor de arquivo.

Por padrão, há três descritores de arquivos predefinidos (padrão e conhecidos):

  • 0: entrada padrão. A implementação padrão da entrada padrão fd é uma operação nula, já que não é esperado que apps confiáveis tenham um console interativo. Portanto, ler, gravar ou invocar ioctl() em fd 0 deve retornar um erro ERR_NOT_SUPPORTED.
  • 1 - saída padrão. Os dados gravados na saída padrão podem ser roteados (dependendo do nível de depuração do LK) para UART e/ou um registro de memória disponível no lado não seguro, dependendo da plataforma e da configuração. Os registros e mensagens de depuração não críticos precisam ser enviados para a saída padrão. Os métodos read() e ioctl() são no-ops e precisam retornar um erro ERR_NOT_SUPPORTED.
  • 2 - erro padrão. Os dados gravados no erro padrão precisam ser roteados para o registro de UART ou de memória disponível no lado não seguro, dependendo da plataforma e da configuração. É recomendável gravar apenas mensagens críticas no erro padrão, porque é muito provável que esse fluxo não seja limitado. Os métodos read() e ioctl() são no-ops e precisam retornar um erro ERR_NOT_SUPPORTED.

Embora esse conjunto de descritores de arquivos possa ser estendido para implementar mais fds (para implementar extensões específicas da plataforma), a extensão de descritores de arquivos precisa ser feita com cuidado. A extensão de descritores de arquivos tende a criar conflitos e geralmente não é recomendada.

Métodos na API File Descriptor

read()

Tenta ler até count bytes de dados de um descritor de arquivo especificado.

long read(uint32_t fd, void *buf, uint32_t count);

[in] fd: descritor de arquivo a ser lido

[out] buf: ponteiro para um buffer em que armazenar dados

[in] count: número máximo de bytes a serem lidos

[retval]: número de bytes lidos retornados; caso contrário, uma mensagem de erro negativa

write()

Grava até count bytes de dados no descritor de arquivo especificado.

long write(uint32_t fd, void *buf, uint32_t count);

[in] fd: descritor de arquivo em que gravar

[out] buf: ponteiro para dados a serem gravados

[in] count: número máximo de bytes a serem gravados

[retval]: número de bytes gravados retornado. Caso contrário, um erro negativo

ioctl()

Invoca um comando ioctl especificado para um determinado descritor de arquivo.

long ioctl(uint32_t fd, uint32_t cmd, void *args);

[in] fd: descritor de arquivo em que ioctl() será invocado

[in] cmd: o comando ioctl

[in/out] args: ponteiro para argumentos ioctl()

API Miscellaneous

Métodos na API Miscellaneous

gettime()

Retorna o horário atual do sistema (em nanossegundos).

long gettime(uint32_t clock_id, uint32_t flags, int64_t *time);

[in] clock_id: depende da plataforma. Transmita zero para o padrão

[in] flags: reservado, precisa ser zero

[out] time: ponteiro para um valor int64_t em que armazenar o horário atual

[retval]: NO_ERROR em caso de sucesso. Caso contrário, um erro negativo

nanosleep()

Suspende a execução do app de chamada por um período especificado e a retoma após esse período.

long nanosleep(uint32_t clock_id, uint32_t flags, uint64_t sleep_time)

[in] clock_id: reservado, precisa ser zero

[in] flags: reservado, precisa ser zero

[in] sleep_time: tempo de suspensão em nanossegundos

[retval]: NO_ERROR em caso de sucesso. Caso contrário, um erro negativo

Exemplo de um servidor de apps confiável

O app de exemplo a seguir mostra o uso das APIs acima. O exemplo cria um serviço "eco" que processa várias conexões de entrada e reflete de volta ao autor da chamada todas as mensagens recebidas de clientes originadas do lado seguro ou não seguro.

#include <uapi/err.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <trusty_ipc.h>
#define LOG_TAG "echo_srv"
#define TLOGE(fmt, ...) \
    fprintf(stderr, "%s: %d: " fmt, LOG_TAG, __LINE__, ##__VA_ARGS__)

# define MAX_ECHO_MSG_SIZE 64

static
const char * srv_name = "com.android.echo.srv.echo";

static uint8_t msg_buf[MAX_ECHO_MSG_SIZE];

/*
 *  Message handler
 */
static int handle_msg(handle_t chan) {
  int rc;
  struct iovec iov;
  ipc_msg_t msg;
  ipc_msg_info_t msg_inf;

  iov.iov_base = msg_buf;
  iov.iov_len = sizeof(msg_buf);

  msg.num_iov = 1;
  msg.iov = &iov;
  msg.num_handles = 0;
  msg.handles = NULL;

  /* get message info */
  rc = get_msg(chan, &msg_inf);
  if (rc == ERR_NO_MSG)
    return NO_ERROR; /* no new messages */

  if (rc != NO_ERROR) {
    TLOGE("failed (%d) to get_msg for chan (%d)\n",
      rc, chan);
    return rc;
  }

  /* read msg content */
  rc = read_msg(chan, msg_inf.id, 0, &msg);
  if (rc < 0) {
    TLOGE("failed (%d) to read_msg for chan (%d)\n",
      rc, chan);
    return rc;
  }

  /* update number of bytes received */
  iov.iov_len = (size_t) rc;

  /* send message back to the caller */
  rc = send_msg(chan, &msg);
  if (rc < 0) {
    TLOGE("failed (%d) to send_msg for chan (%d)\n",
      rc, chan);
    return rc;
  }

  /* retire message */
  rc = put_msg(chan, msg_inf.id);
  if (rc != NO_ERROR) {
    TLOGE("failed (%d) to put_msg for chan (%d)\n",
      rc, chan);
    return rc;
  }

  return NO_ERROR;
}

/*
 *  Channel event handler
 */
static void handle_channel_event(const uevent_t * ev) {
  int rc;

  if (ev->event & IPC_HANDLE_POLL_MSG) {
    rc = handle_msg(ev->handle);
    if (rc != NO_ERROR) {
      /* report an error and close channel */
      TLOGE("failed (%d) to handle event on channel %d\n",
        rc, ev->handle);
      close(ev->handle);
    }
    return;
  }
  if (ev->event & IPC_HANDLE_POLL_HUP) {
    /* closed by peer. */
    close(ev->handle);
    return;
  }
}

/*
 *  Port event handler
 */
static void handle_port_event(const uevent_t * ev) {
  uuid_t peer_uuid;

  if ((ev->event & IPC_HANDLE_POLL_ERROR) ||
    (ev->event & IPC_HANDLE_POLL_HUP) ||
    (ev->event & IPC_HANDLE_POLL_MSG) ||
    (ev->event & IPC_HANDLE_POLL_SEND_UNBLOCKED)) {
    /* should never happen with port handles */
    TLOGE("error event (0x%x) for port (%d)\n",
      ev->event, ev->handle);
    abort();
  }
  if (ev->event & IPC_HANDLE_POLL_READY) {
    /* incoming connection: accept it */
    int rc = accept(ev->handle, &peer_uuid);
    if (rc < 0) {
      TLOGE("failed (%d) to accept on port %d\n",
        rc, ev->handle);
      return;
    }
    handle_t chan = rc;
    while (true){
      struct uevent cev;

      rc = wait(chan, &cev, INFINITE_TIME);
      if (rc < 0) {
        TLOGE("wait returned (%d)\n", rc);
        abort();
      }
      handle_channel_event(&cev);
      if (cev.event & IPC_HANDLE_POLL_HUP) {
        return;
      }
    }
  }
}


/*
 *  Main app entry point
 */
int main(void) {
  int rc;
  handle_t port;

  /* Initialize service */
  rc = port_create(srv_name, 1, MAX_ECHO_MSG_SIZE,
    IPC_PORT_ALLOW_NS_CONNECT |
    IPC_PORT_ALLOW_TA_CONNECT);
  if (rc < 0) {
    TLOGE("Failed (%d) to create port %s\n",
      rc, srv_name);
    abort();
  }
  port = (handle_t) rc;

  /* enter main event loop */
  while (true) {
    uevent_t ev;

    ev.handle = INVALID_IPC_HANDLE;
    ev.event = 0;
    ev.cookie = NULL;

    /* wait forever */
    rc = wait(port, &ev, INFINITE_TIME);
    if (rc == NO_ERROR) {
      /* got an event */
      handle_port_event(&ev);
    } else {
      TLOGE("wait returned (%d)\n", rc);
      abort();
    }
  }
  return 0;
}

O método run_end_to_end_msg_test() envia 10.000 mensagens de forma assíncrona para o serviço "echo" e processa as respostas.

static int run_echo_test(void)
{
  int rc;
  handle_t chan;
  uevent_t uevt;
  uint8_t tx_buf[64];
  uint8_t rx_buf[64];
  ipc_msg_info_t inf;
  ipc_msg_t   tx_msg;
  iovec_t     tx_iov;
  ipc_msg_t   rx_msg;
  iovec_t     rx_iov;

  /* prepare tx message buffer */
  tx_iov.base = tx_buf;
  tx_iov.len  = sizeof(tx_buf);
  tx_msg.num_iov = 1;
  tx_msg.iov     = &tx_iov;
  tx_msg.num_handles = 0;
  tx_msg.handles = NULL;

  memset (tx_buf, 0x55, sizeof(tx_buf));

  /* prepare rx message buffer */
  rx_iov.base = rx_buf;
  rx_iov.len  = sizeof(rx_buf);
  rx_msg.num_iov = 1;
  rx_msg.iov     = &rx_iov;
  rx_msg.num_handles = 0;
  rx_msg.handles = NULL;

  /* open connection to echo service */
  rc = sync_connect(srv_name, 1000);
  if(rc < 0)
    return rc;

  /* got channel */
  chan = (handle_t)rc;

  /* send/receive 10000 messages asynchronously. */
  uint tx_cnt = 10000;
  uint rx_cnt = 10000;

  while (tx_cnt || rx_cnt) {
    /* send messages until all buffers are full */
while (tx_cnt) {
    rc = send_msg(chan, &tx_msg);
      if (rc == ERR_NOT_ENOUGH_BUFFER)
      break;  /* no more space */
    if (rc != 64) {
      if (rc > 0) {
        /* incomplete send */
        rc = ERR_NOT_VALID;
}
      goto abort_test;
}
    tx_cnt--;
  }

  /* wait for reply msg or room */
  rc = wait(chan, &uevt, 1000);
  if (rc != NO_ERROR)
    goto abort_test;

  /* drain all messages */
  while (rx_cnt) {
    /* get a reply */
      rc = get_msg(chan, &inf);
    if (rc == ERR_NO_MSG)
        break;  /* no more messages  */
  if (rc != NO_ERROR)
goto abort_test;

  /* read reply data */
    rc = read_msg(chan, inf.id, 0, &rx_msg);
  if (rc != 64) {
    /* unexpected reply length */
    rc = ERR_NOT_VALID;
    goto abort_test;
}

  /* discard reply */
  rc = put_msg(chan, inf.id);
  if (rc != NO_ERROR)
    goto abort_test;
  rx_cnt--;
  }
}

abort_test:
  close(chan);
  return rc;
}

APIs e apps do mundo não seguro

Um conjunto de serviços Trusty, publicados do lado seguro e marcados com o atributo IPC_PORT_ALLOW_NS_CONNECT, é acessível para o kernel e programas de espaço do usuário em execução no lado não seguro.

O ambiente de execução no lado não seguro (kernel e espaço do usuário) é drasticamente diferente do ambiente de execução no lado seguro. Portanto, em vez de uma única biblioteca para os dois ambientes, há dois conjuntos diferentes de APIs. No kernel, a API Client é fornecida pelo driver do kernel trusty-ipc e registra um nó de dispositivo de caractere que pode ser usado por processos do espaço do usuário para se comunicar com serviços executados no lado seguro.

API Trusty IPC Client do espaço do usuário

A biblioteca de API do cliente Trusty IPC do espaço do usuário é uma camada fina sobre o nó do dispositivo fd.

Um programa do espaço do usuário inicia uma sessão de comunicação chamando tipc_connect(), inicializando uma conexão com um serviço do Trusty especificado. Internamente, a chamada tipc_connect() abre um nó de dispositivo especificado para extrair um descritor de arquivo e invocar uma chamada TIPC_IOC_CONNECT ioctl() com o parâmetro argp apontando para uma string que contém um nome de serviço para se conectar.

#define TIPC_IOC_MAGIC  'r'
#define TIPC_IOC_CONNECT  _IOW(TIPC_IOC_MAGIC, 0x80, char *)

O descritor de arquivo resultante só pode ser usado para se comunicar com o serviço para o qual foi criado. O descritor de arquivo precisa ser fechado chamando tipc_close() quando a conexão não for mais necessária.

O descritor de arquivo recebido pela chamada tipc_connect() se comporta como um nó de dispositivo de caractere típico. O descritor de arquivo:

  • Pode ser alternado para o modo sem bloqueio, se necessário
  • Pode ser escrito usando uma chamada write() padrão para enviar mensagens para o outro lado
  • Pode ser consultado (usando chamadas poll() ou select()) para verificar a disponibilidade de mensagens recebidas como um descritor de arquivo regular
  • Pode ser lido para recuperar mensagens recebidas

Um autor da chamada envia uma mensagem para o serviço Trusty executando uma chamada de gravação para o fd especificado. Todos os dados transmitidos para a chamada write() acima são transformados em uma mensagem pelo driver trusty-ipc. A mensagem é enviada para o lado seguro, em que os dados são processados pelo subsistema IPC no kernel do Trusty e encaminhados para o destino adequado e enviados para um loop de eventos do app como um evento IPC_HANDLE_POLL_MSG em um identificador de canal específico. Dependendo do protocolo específico do serviço, o serviço Trusty pode enviar uma ou mais mensagens de resposta que são enviadas de volta para o lado não seguro e colocadas na fila de mensagens do descritor de arquivo de canal adequada para serem recuperadas pela chamada read() do app de espaço do usuário.

tipc_connect()

Abre um nó de dispositivo tipc especificado e inicia uma conexão com um serviço Trusty especificado.

int tipc_connect(const char *dev_name, const char *srv_name);

[in] dev_name: caminho para o nó do dispositivo Trusty IPC a ser aberto

[in] srv_name: nome de um serviço do Trusty publicado para se conectar

[retval]: descritor de arquivo válido em caso de sucesso, -1 caso contrário.

tipc_close()

Fecha a conexão com o serviço Trusty especificado por um descritor de arquivo.

int tipc_close(int fd);

[in] fd: descritor de arquivo aberto anteriormente por uma chamada tipc_connect()

API do cliente Kernel Trusty IPC

A API cliente IPC do kernel Trusty está disponível para drivers do kernel. A API Trusty IPC do espaço do usuário é implementada em cima dessa API.

Em geral, o uso típico dessa API consiste em um autor da chamada criando um objeto struct tipc_chan usando a função tipc_create_channel() e, em seguida, usando a chamada tipc_chan_connect() para iniciar uma conexão com o serviço Trusty IPC em execução no lado seguro. A conexão com o lado remoto pode ser encerrada chamando tipc_chan_shutdown() seguido de tipc_chan_destroy() para limpar os recursos.

Ao receber uma notificação (pelo callback handle_event()) de que uma conexão foi estabelecida, um autor da chamada faz o seguinte:

  • Recebe um buffer de mensagem usando a chamada tipc_chan_get_txbuf_timeout()
  • Cria uma mensagem e
  • Enfileira a mensagem usando o método tipc_chan_queue_msg() para entrega a um serviço Trusty (no lado seguro) ao qual o canal está conectado.

Depois que a fila for concluída, o autor da chamada vai precisar esquecer o buffer de mensagens, porque ele vai retornar ao pool de buffer livre após ser processado pelo lado remoto (para reutilização posterior, para outras mensagens). O usuário só precisa chamar tipc_chan_put_txbuf() se ele não conseguir colocar esse buffer na fila ou se ele não for mais necessário.

Um usuário da API recebe mensagens do lado remoto processando um callback de notificação handle_msg() (chamado no contexto do workqueue rx do trusty-ipc) que fornece um ponteiro para um buffer rx contendo uma mensagem de entrada a ser processada.

Espera-se que a implementação do callback handle_msg() retorne um ponteiro para um struct tipc_msg_buf válido. Ele pode ser o mesmo que o buffer de mensagens recebidas se for processado localmente e não for mais necessário. Como alternativa, pode ser um novo buffer recebido por uma chamada tipc_chan_get_rxbuf() se o buffer de entrada for colocado na fila para processamento posterior. Um buffer rx separado precisa ser rastreado e, eventualmente, liberado usando uma chamada tipc_chan_put_rxbuf() quando ele não for mais necessário.

Métodos na API Client IPC do Kernel Trusty

tipc_create_channel()

Cria e configura uma instância de um canal Trusty IPC para um dispositivo trusty-ipc específico.

struct tipc_chan *tipc_create_channel(struct device *dev,
                          const struct tipc_chan_ops *ops,
                              void *cb_arg);

[in] dev: ponteiro para o trusty-ipc para o qual o canal do dispositivo é criado.

[in] ops: ponteiro para um struct tipc_chan_ops, com callbacks específicos do autor da chamada preenchidos.

[in] cb_arg: ponteiro para dados transmitidos a callbacks tipc_chan_ops

[retval]: Ponteiro para uma instância recém-criada de struct tipc_chan em caso de sucesso, ERR_PTR(err) caso contrário

Em geral, um autor da chamada precisa fornecer dois callbacks que são invocados de forma assíncrona quando a atividade correspondente está ocorrendo.

O evento void (*handle_event)(void *cb_arg, int event) é invocado para notificar um autor da chamada sobre uma mudança de estado do canal.

[in] cb_arg: ponteiro para dados transmitidos para uma chamada tipc_create_channel()

[in] event: um evento que pode ser um dos seguintes valores:

  • TIPC_CHANNEL_CONNECTED: indica uma conexão bem-sucedida ao lado remoto
  • TIPC_CHANNEL_DISCONNECTED: indica que o lado remoto negou a nova solicitação de conexão ou solicitou a desconexão do canal conectado anteriormente.
  • TIPC_CHANNEL_SHUTDOWN: indica que o lado remoto está sendo encerrado, encerrando permanentemente todas as conexões.

O callback struct tipc_msg_buf *(*handle_msg)(void *cb_arg, struct tipc_msg_buf *mb) é invocado para notificar que uma nova mensagem foi recebida em um canal especificado:

  • [in] cb_arg: ponteiro para dados transmitidos para a chamada tipc_create_channel()
  • [in] mb: ponteiro para um struct tipc_msg_buf que descreve uma mensagem recebida
  • [retval]: a implementação do callback deve retornar um ponteiro para um struct tipc_msg_buf que pode ser o mesmo ponteiro recebido como um parâmetro mb se a mensagem for processada localmente e não for mais necessária (ou pode ser um novo buffer recebido pela chamada tipc_chan_get_rxbuf()).

tipc_chan_connect()

Inicia uma conexão com o serviço Trusty IPC especificado.

int tipc_chan_connect(struct tipc_chan *chan, const char *port);

[in] chan: ponteiro para um canal retornado pela chamada tipc_create_chan()

[in] port: ponteiro para uma string que contém o nome do serviço a ser conectado

[retval]: 0 em caso de sucesso, um erro negativo, caso contrário

O autor da chamada é notificado quando uma conexão é estabelecida recebendo um callback handle_event.

tipc_chan_shutdown()

Encerra uma conexão com o serviço IPC do Trusty iniciada anteriormente por uma chamada tipc_chan_connect().

int tipc_chan_shutdown(struct tipc_chan *chan);

[in] chan: ponteiro para um canal retornado por uma chamada tipc_create_chan()

tipc_chan_destroy()

Destrói um canal IPC do Trusty especificado.

void tipc_chan_destroy(struct tipc_chan *chan);

[in] chan: ponteiro para um canal retornado pela chamada tipc_create_chan()

tipc_chan_get_txbuf_timeout()

Obtém um buffer de mensagens que pode ser usado para enviar dados por um canal especificado. Se o buffer não estiver disponível imediatamente, o autor da chamada poderá ser bloqueado pelo tempo limite especificado (em milissegundos).

struct tipc_msg_buf *
tipc_chan_get_txbuf_timeout(struct tipc_chan *chan, long timeout);

[in] chan: ponteiro para o canal em que uma mensagem será enfileirada

[in] chan: tempo limite máximo para aguardar até que o buffer tx fique disponível

[retval]: um buffer de mensagem válido em caso de sucesso, ERR_PTR(err) em caso de erro

tipc_chan_queue_msg()

Enfileira uma mensagem para ser enviada pelos canais Trusty IPC especificados.

int tipc_chan_queue_msg(struct tipc_chan *chan, struct tipc_msg_buf *mb);

[in] chan: ponteiro para o canal em que a mensagem vai ser enfileirada

[in] mb: Ponteiro para a mensagem a ser enfileirada (obtido por uma chamada tipc_chan_get_txbuf_timeout())

[retval]: 0 em caso de sucesso, um erro negativo, caso contrário

tipc_chan_put_txbuf()

Libera o buffer de mensagem Tx especificado obtido anteriormente por uma chamada tipc_chan_get_txbuf_timeout().

void tipc_chan_put_txbuf(struct tipc_chan *chan,
                         struct tipc_msg_buf *mb);

[in] chan: ponteiro para o canal ao qual este buffer de mensagens pertence

[in] mb: ponteiro para o buffer de mensagens a ser liberado

[retval]: Nenhum

tipc_chan_get_rxbuf()

Recebe um novo buffer de mensagens que pode ser usado para receber mensagens no canal especificado.

struct tipc_msg_buf *tipc_chan_get_rxbuf(struct tipc_chan *chan);

[in] chan: ponteiro para um canal ao qual este buffer de mensagens pertence

[retval]: um buffer de mensagem válido em caso de sucesso, ERR_PTR(err) em caso de erro

tipc_chan_put_rxbuf()

Libera um buffer de mensagem especificado recebido anteriormente por uma chamada tipc_chan_get_rxbuf().

void tipc_chan_put_rxbuf(struct tipc_chan *chan,
                         struct tipc_msg_buf *mb);

[in] chan: ponteiro para um canal ao qual este buffer de mensagens pertence

[in] mb: ponteiro para um buffer de mensagem a ser liberado

[retval]: Nenhum