הפנייה ל-Trusty API

Trusty מספק ממשקי API לפיתוח של שתי קטגוריות של אפליקציות ושירותים:

  • אפליקציות ושירותים מהימנים שפועלים במעבד ה-TEE
  • אפליקציות רגילות ולא מהימנות שפועלות במעבד הראשי ומשתמשות בשירותים שסופקו על ידי אפליקציות מהימנות

ממשק ה-API של Trusty מתאר באופן כללי את מערכת התקשורת בין תהליכים (IPC) של Trusty, כולל תקשורת עם העולם הלא מאובטח. תוכנה שפועלת במעבד הראשי יכולה להשתמש ב-Trusty APIs כדי להתחבר לאפליקציות ולשירותים מהימנים ולבצע איתם החלפת הודעות שרירותיות, בדיוק כמו שירות רשת ב-IP. האפליקציה קובעת את פורמט הנתונים והסמנטיקה של ההודעות האלה באמצעות פרוטוקול ברמת האפליקציה. התשתית הבסיסית של Trusty (בצורת מנהלי התקנים שפועלים במעבד הראשי) מבטיחה העברה מהימנה של הודעות, והתקשורת היא אסינכרונית לחלוטין.

יציאות וערוצים

אפליקציות Trusty משתמשות בשערים כדי לחשוף נקודות קצה של שירותים בצורה של נתיב בעל שם שאליו הלקוחות מתחברים. כך מקבלים מזהה שירות פשוט שמבוסס על מחרוזת, שתוכלו להשתמש בו בלקוחות. מוסכמת מתן השמות היא לפי סגנון DNS הפוך, למשל com.google.servicename.

כשלקוח מתחבר לשקע, הוא מקבל ערוץ ליצירת אינטראקציה עם שירות. השירות צריך לקבל חיבור נכנס, וכשהוא עושה זאת, גם הוא מקבל ערוץ. בעיקרון, יציאות משמשות לחיפוש שירותים, ולאחר מכן מתבצעת תקשורת דרך זוג ערוצי תקשורת מחוברים (כלומר, אירועי חיבור ביציאה). כשלקוח מתחבר לשקע, נוצר חיבור סימטרי דו-כיווני. בעזרת הנתיב הזה, לקוחות ושרתים יכולים להחליף הודעות שרירותיות עד שאחד מהצדדים יחליט לנתק את החיבור.

רק אפליקציות מהימנות בצד המאובטח או מודולים של ליבה מהימנים יכולים ליצור יציאות. אפליקציות שפועלות בצד הלא מאובטח (בעולם הרגיל) יכולות להתחבר רק לשירותים שפורסמו על ידי הצד המאובטח.

בהתאם לדרישות, אפליקציה מהימנה יכולה להיות גם לקוח וגם שרת בו-זמנית. יכול להיות שאפליקציה מהימנה שמפרסמת שירות (כשרת) תצטרך להתחבר לשירותים אחרים (כלקוח).

Handle API

כינויים הם מספרים שלמים ללא סימן שמייצגים משאבים כמו יציאות וערוצים, בדומה למאפייני קובץ ב-UNIX. אחרי שיוצרים את כינויי המשתמש, הם מועברים לטבלת כינויים ספציפית לאפליקציה, וניתן להפנות אליהם מאוחר יותר.

מבצע קריאה יכול לשייך נתונים פרטיים לכינוי באמצעות השיטה set_cookie().

שיטות ב-Handle API

כינויים תקפים רק בהקשר של אפליקציה. אפליקציה לא צריכה להעביר את הערך של כינוי לאפליקציות אחרות, אלא אם צוין זאת במפורש. צריך לפרש את ערך הכינוי רק על ידי השוואה שלו לערך INVALID_IPC_HANDLE #define,, שאותו אפליקציה יכולה להשתמש בו כדי לציין שהכינוי לא חוקי או לא הוגדר.

שיוך של הנתונים הפרטיים שסופקו על ידי מבצע הקריאה לחשבון שמוגדר.

long set_cookie(uint32_t handle, void *cookie)

[in] handle: כל אחיזה שמוחזרת על ידי אחת מהקריאות ל-API

[in] cookie: הפניה לנתונים שרירותיים במרחב המשתמש באפליקציית Trusty

[retval]: NO_ERROR במקרה של הצלחה, < 0 קוד שגיאה במקרה אחר

הקריאה הזו שימושית לטיפול באירועים שמתרחשים במועד מאוחר יותר אחרי שנוצר ה-handle. מנגנון טיפול האירועים מספק את ה-handle ואת קובץ ה-cookie שלו בחזרה לגורם המטפל באירועים.

אפשר להמתין לאירועים באמצעות הקריאה wait().

wait()

הפונקציה ממתינה לאירוע שיתרחש בכינוי נתון למשך פרק זמן מסוים.

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

[in] handle_id: כל אחיזה שמוחזרת על ידי אחת מהקריאות ל-API

[out] event: הפניה למבנה שמייצג אירוע שהתרחש ב-handle הזה

[in] timeout_msecs: ערך זמן קצוב לתפוגה באלפיות השנייה. הערך 1- מציין זמן קצוב לתפוגה אינסופי.

[retval]: NO_ERROR אם אירוע תקין התרחש במרווח הזמן שצוין לתפוגה, ERR_TIMED_OUT אם מרווח הזמן שצוין לתפוגה חלף אבל לא התרחש אירוע, < 0 לשגיאות אחרות

אם הפעולה תתבצע בהצלחה (retval == NO_ERROR), הקריאה ל-wait() תמלא מבנה uevent_t מסוים במידע על האירוע שהתרחש.

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;

השדה event מכיל שילוב של הערכים הבאים:

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 – אין אירועים בהמתנה בפועל, והמבצע צריך להתחיל מחדש את ההמתנה

IPC_HANDLE_POLL_ERROR – אירעה שגיאה פנימית לא ידועה

IPC_HANDLE_POLL_READY – תלוי בסוג הכינוי, באופן הבא:

  • ביציאות, הערך הזה מציין שיש חיבור בהמתנה
  • בערוצים, הערך הזה מציין שהתחברתם באופן אסינכרוני (ראו connect()).

האירועים הבאים רלוונטיים רק לערוצים:

  • IPC_HANDLE_POLL_HUP – מציין שערוץ נסגר על ידי משתמש אחר
  • IPC_HANDLE_POLL_MSG – סימן לכך שיש הודעה בהמתנה בערוץ הזה
  • IPC_HANDLE_POLL_SEND_UNBLOCKED – מציין שמתקשר שחסמתם בעבר את האפשרות לשלוח הודעות עשוי לנסות לשלוח הודעה שוב (פרטים נוספים מופיעים בתיאור של send_msg())

פונקציית הטיפול באירועים צריכה להיות מוכנה לטפל בשילוב של אירועים מסוימים, כי יכול להיות שיותקבעו כמה ביטים בו-זמנית. לדוגמה, יכול להיות שבאותו ערוץ יהיו הודעות בהמתנה וחיבורים שנסגרו על ידי עמיתים.

רוב האירועים הם אירועים 'דביקים'. הן נשמרות כל עוד התנאי הבסיסי נמשך (לדוגמה, כל ההודעות בהמתנה מתקבלות ובקשות החיבור בהמתנה מטופלות). היוצא מן הכלל הוא האירוע IPC_HANDLE_POLL_SEND_UNBLOCKED, שמתבטל לאחר קריאה, ולאפליקציה יש רק הזדמנות אחת לטפל בו.

אפשר למחוק את ה-handles על ידי קריאה לשיטה close().

close()

הפונקציה משמידה את המשאב שמשויך לכינוי שצוין ומסירה אותו מטבלת הכינויים.

long close(uint32_t handle_id);

[in] handle_id: הכינוי שרוצים להשמיד

[retval]: 0 אם הפעולה בוצעה בהצלחה, שגיאה שלילית אחרת

Server API

השרת מתחיל ליצור יציאה בעלת שם אחת או יותר שמייצגת את נקודות הקצה של השירות. כל יציאה מיוצגת על ידי כינוי.

שיטות ב-Server API

port_create()

יצירת יציאת שירות בעלת שם.

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

[in] path: שם המחרוזת של היציאה (כפי שמתואר למעלה). השם הזה צריך להיות ייחודי במערכת. ניסיונות ליצור כפיל ייכשל.

[in] num_recv_bufs: המספר המקסימלי של מאגרי נתונים (buffers) שאפשר להקצות מראש לערוץ ביציאה הזו כדי להקל על החלפת הנתונים עם הלקוח. מאגרי נתונים נספרים בנפרד לנתונים שנשלחים בשני הכיוונים, כך שציון הערך 1 כאן יגרום להקצאה מראש של מאגר נתונים אחד לשליחה ומאגר נתונים אחד לקבלה. באופן כללי, מספר המאגרים שנדרשים תלוי בהסכם הפרוטוקול ברמה גבוהה יותר בין הלקוח לשרת. המספר יכול להיות קטן ככל האפשר במקרה של פרוטוקול סינכרוני מאוד (שליחת הודעה, קבלת תשובה לפני שליחת הודעה נוספת). אבל המספר יכול להיות גדול יותר אם הלקוח מצפה לשלוח יותר מהודעה אחת לפני שתופיע תשובה (למשל, הודעה אחת כמבוא והודעה אחרת כפקודה בפועל). קבוצות המאגרים שהוקצו הן לכל ערוץ, כך שלשני חיבורים (ערוצים) נפרדים יהיו קבוצות מאגרים נפרדות.

[in] recv_buf_size: הגודל המקסימלי של כל מאגר נתונים זמני בקבוצת המאגרים הזמניים שלמעלה. הערך הזה תלוי בפרוטוקול ומגביל בפועל את גודל ההודעה המקסימלי שאפשר להעביר לשותף

[in] flags: שילוב של דגלים שמציין התנהגות נוספת של השקע

הערך הזה צריך להיות שילוב של הערכים הבאים:

IPC_PORT_ALLOW_TA_CONNECT – מאפשר חיבור מאפליקציות מאובטחות אחרות

IPC_PORT_ALLOW_NS_CONNECT – מאפשר חיבור מהעולם הלא מאובטח

[retval]: הטיפולן של השקע שנוצר אם הערך לא שלילי, או שגיאה ספציפית אם הערך שלילי

לאחר מכן, השרת מבצע סקירה של רשימת הפקדים של היציאות לחיבורים נכנסים באמצעות קריאה ל-wait(). כשמקבלים בקשת חיבור שמסומנת על ידי הביט IPC_HANDLE_POLL_READY שמוגדר בשדה event במבנה uevent_t, השרת צריך לקרוא ל-accept() כדי לסיים את יצירת החיבור וליצור ערוץ (שמיוצג על ידי עוד מאגר) שאפשר לבצע בו סקרים לאיתור הודעות נכנסות.

accept()‎

הפונקציה מאשרת חיבור נכנס ומקבלת כינוי לערוץ.

long accept(uint32_t handle_id, uuid_t *peer_uuid);

[in] handle_id: ה-handle שמייצג את היציאה שאליה הלקוח התחבר

[out] peer_uuid: הפניה למבנה uuid_t שיאוכלס ב-UUID של אפליקציית הלקוח שמתחבר. הוא מוגדר לכל אפס אם החיבור הגיע מהעולם הלא מאובטח

[retval]: ה-handle של הערוץ (אם הוא לא שלילי) שבו השרת יכול להחליף הודעות עם הלקוח (או קוד שגיאה אחרת)

Client API

הקטע הזה מכיל את השיטות ב-Client API.

שיטות ב-Client API

connect()‎

הפעלת חיבור ליציאה שצוינה בשם.

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

[in] path: שם של יציאה שפורסמה על ידי אפליקציה מהימנה

[in] flags: מציין התנהגות אופציונלית נוספת

[retval]: ה-handle של הערוץ שבו אפשר להחליף הודעות עם השרת. אם הערך של retval שלילי, המשמעות היא שזו שגיאה.

אם לא צוינו ערכים של flags (הפרמטר flags מוגדר ל-0), קריאה ל-connect() מתחילה חיבור סינכרוני ליציאה שצוינה, שמחזירה שגיאה באופן מיידי אם היציאה לא קיימת, ויוצרת חסימה עד שהשרת יקבל חיבור אחרת.

אפשר לשנות את ההתנהגות הזו על ידי ציון שילוב של שני ערכים, כפי שמתואר בהמשך:

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

IPC_CONNECT_WAIT_FOR_PORT – מאלץ קריאה ל-connect() להמתין אם היציאה שצוינה לא קיימת באופן מיידי בזמן הביצוע, במקום להיכשל באופן מיידי.

IPC_CONNECT_ASYNC – אם ההגדרה מוגדרת, מתבצע חיבור אסינכרוני. כדי להתחיל לפעול באופן רגיל, האפליקציה צריכה לבצע סקירה (poll) של ה-handle המוחזר על ידי קריאה ל-wait() לאירוע של השלמת החיבור, שמסומן על ידי הביט IPC_HANDLE_POLL_READY שמוגדר בשדה האירוע של המבנה uevent_t.

Messaging API

הקריאות ל-Messaging API מאפשרות לשלוח ולקרוא הודעות דרך חיבור (ערוץ) שנוצר מראש. הקריאות ל-Messaging API זהות בשרתים ובלקוחות.

לקוח מקבל כינוי לערוץ על ידי שליחת קריאה ל-connect(), ושרת מקבל כינוי לערוץ מקריאה ל-accept(), כפי שמתואר למעלה.

המבנה של הודעת Trusty

כפי שמוצג בהמשך, להודעות שמתחלפות באמצעות Trusty API יש מבנה מינימלי, והשרת והלקוח צריכים להסכים על הסמנטיקה של התוכן בפועל:

/*
 *  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;

הודעה יכולה להיות מורכבת ממאגר אחד או יותר לא רציף, שמיוצג על ידי מערך של מבנים מסוג iovec_t. Trusty מבצע קריאות וכתיבות של scatter-gather בבלוקים האלה באמצעות מערך iov. התוכן של מאגרים שאפשר לתאר באמצעות מערך iov הוא שרירותי לחלוטין.

שיטות ב-Messaging API

send_msg()‎

שליחת הודעה בערוץ ספציפי.

long send_msg(uint32_t handle, ipc_msg_t *msg);

[in] handle: הכינוי של הערוץ שאליו שולחים את ההודעה

[in] msg: הפניה ל-ipc_msg_t structure שמתאר את ההודעה

[retval]: המספר הכולל של הבייטים שנשלחו במקרה של הצלחה. במקרה אחר, תוחזר שגיאה שלילית.

אם הלקוח (או השרת) מנסה לשלוח הודעה דרך הערוץ ואין מקום בתור ההודעות של עמית היעד, יכול להיות שהערוץ ייכנס למצב של חסימה לשליחה (מצב כזה אמור לעולם לא לקרות בפרוטוקול פשוט של בקשה/תשובה סינכרונית, אבל הוא עשוי לקרות במקרים מורכבים יותר). המצב הזה מצוין על ידי החזרת קוד השגיאה ERR_NOT_ENOUGH_BUFFER. במקרה כזה, מבצע הקריאה צריך להמתין עד שהמכונה השכנה תפנה מקום בתור הקבלה שלה על ידי אחזור ההודעות לטיפול והוצאה משימוש, כפי שמצוין על ידי הביט IPC_HANDLE_POLL_SEND_UNBLOCKED שמוגדר בשדה event של המבנה uevent_t שמוחזר על ידי הקריאה wait().

get_msg()‎

אחזור של מטא-מידע על ההודעה הבאה בתור של הודעות נכנסות

של ערוץ ספציפי.

long get_msg(uint32_t handle, ipc_msg_info_t *msg_info);

[in] handle: הכינוי של הערוץ שבו צריך לאחזר הודעה חדשה

[out] msg_info: מבנה המידע של ההודעה שמתואר באופן הבא:

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

לכל הודעה מוקצה מזהה ייחודי בקבוצת ההודעות שעדיין לא נשלחו, והאורך הכולל של כל הודעה ממולא. אם ההגדרה והפרוטוקול מאפשרים זאת, יכולות להיות כמה הודעות פתוחות בו-זמנית בערוץ מסוים.

[retval]: NO_ERROR אם הפעולה בוצעה בהצלחה, שגיאה שלילית אחרת

read_msg()

קריאת התוכן של ההודעה עם המזהה שצוין, החל מההיסט שצוין.

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

[in] handle: הכינוי של הערוץ שממנו רוצים לקרוא את ההודעה

[in] msg_id: המזהה של ההודעה לקריאה

[in] offset: הזזה בהודעה שממנה מתחילים לקרוא

[out] msg: הפניה למבנה ipc_msg_t שמתאר קבוצה של מאגרים לאחסון נתוני ההודעות הנכנסות

[retval]: המספר הכולל של הבייטים שמאוחסנים במאגרים של msg במקרה של הצלחה, או שגיאה שלילית במקרה אחר

אפשר להפעיל את השיטה read_msg כמה פעמים, החל מערך אופסט שונה (לא בהכרח רציף) לפי הצורך.

put_msg()‎

הסרה של הודעה עם מזהה ספציפי.

long put_msg(uint32_t handle, uint32_t msg_id);

[in] handle: הכינוי של הערוץ שאליו הגיעה ההודעה

[in] msg_id: מזהה ההודעה שיצאה משימוש

[retval]: NO_ERROR אם הפעולה בוצעה בהצלחה, שגיאה שלילית אחרת

אי אפשר לגשת לתוכן של הודעה אחרי שהיא הוצאה משימוש והמאגר שהיא השתמש בו התפנה.

File Descriptor API

ממשק File Descriptor API כולל את הקריאות read(),‏ write() ו-ioctl(). כל הקריאות האלה יכולות לפעול על קבוצה מוגדרת מראש (סטטית) של מתארי קבצים, שמיוצגים בדרך כלל במספרים קטנים. בהטמעה הנוכחית, המרחב של מתאר הקובץ נפרד מהמרחב של ה-handle ל-IPC. ממשק ה-File Descriptor API ב-Trusty דומה לממשק API מסורתי שמבוסס על מתאר קובץ.

כברירת מחדל, יש 3 מתארי קבצים מוגדרים מראש (רגילים וידועים):

  • 0 – קלט רגיל. ברירת המחדל להטמעה של הקלט הרגיל fd היא פעולה ללא תוצאה (כי לא צפוי שאפליקציות מהימנות יהיו עם מסוף אינטראקטיבי), כך שקריאה, כתיבת או הפעלה של ioctl() ב-fd 0 אמורה להחזיר שגיאה מסוג ERR_NOT_SUPPORTED.
  • 1 – פלט רגיל. נתונים שנכתבים לפלט הסטנדרטי יכולים להיות מנותבים (בהתאם לרמת ניפוי הבאגים של LK) ל-UART ו/או ליומן זיכרון שזמין בצד הלא מאובטח, בהתאם לפלטפורמה ולתצורה. יומני ניפוי באגים והודעות לא קריטיות צריכים להופיע בפלט הסטנדרטי. השיטות read() ו-ioctl() הן no-ops, והן אמורות להחזיר שגיאת ERR_NOT_SUPPORTED.
  • 2 – שגיאת תקן. נתונים שנכתבים לשגיאה רגילה צריכים להיות מנותבים ל-UART או ליומן הזיכרון שזמין בצד הלא מאובטח, בהתאם לפלטפורמה ולתצורה. מומלץ לכתוב רק הודעות קריטיות לסטטוס שגיאה רגיל, כי סביר מאוד שהזרם הזה לא ינוטרל. השיטות read() ו-ioctl() הן no-ops, והן אמורות להחזיר שגיאת ERR_NOT_SUPPORTED.

אפשר להרחיב את הקבוצה הזו של מאפייני הקבצים כדי להטמיע עוד fds (כדי להטמיע תוספים ספציפיים לפלטפורמה), אבל צריך להיזהר כשמגדילים את מאפייני הקבצים. הרחבת מתארי הקבצים עלולה לגרום לסכסוכים, ולכן לא מומלץ לעשות זאת בדרך כלל.

שיטות ב-File Descriptor API

read()‎

ניסיון לקרוא עד count בייטים של נתונים מתאר קובץ שצוין.

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

[in] fd: תיאור הקובץ שממנו קוראים

[out] buf: הפניה למאגר נתונים זמני שבו מאוחסנים הנתונים

[in] count: מספר הבייטים המקסימלי לקריאה

[retval]: מספר הבייטים שנקראים. אחרת, שגיאה שלילית

write()

כתיבת עד count בייטים של נתונים למתואר הקובץ שצוין.

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

[in] fd: תיאור הקובץ שאליו רוצים לכתוב

[out] buf: הפניה לנתונים שרוצים לכתוב

[in] count: מספר הבייטים המקסימלי לכתיבה

[retval]: מספר הבייטים שנכתבו שהוחזרו. אחרת, שגיאה שלילית

ioctl()‎

הפעלת פקודה ioctl ספציפית למתואר קובץ נתון.

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

[in] fd: תיאור הקובץ שבו צריך להפעיל את ioctl()

[in] cmd: הפקודה ioctl

[in/out] args: מציין לארגומנטים של ioctl()

ממשקי API שונים

שיטות ב-Miscellaneous API

gettime()‎

הפונקציה מחזירה את שעון המערכת הנוכחי (בנאונואניות).

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

[in] clock_id: תלוי פלטפורמה. יש להעביר אפס לברירת המחדל

[in] flags: שמור, צריך להיות אפס

[out] time: הפניה לערך int64_t שבו יישמר השעה הנוכחית

[retval]: NO_ERROR אם הפעולה בוצעה בהצלחה, שגיאה שלילית אחרת

nanosleep()‎

השהיה של ההרצה של האפליקציה ששלחה את הקריאה למשך פרק זמן מסוים, והמשך ההרצה אחרי פרק הזמן הזה.

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

[in] clock_id: שמור, צריך להיות אפס

[in] flags: שמור, צריך להיות אפס

[in] sleep_time: זמן השינה בננו-שניות

[retval]: NO_ERROR אם הפעולה בוצעה בהצלחה, שגיאה שלילית אחרת

דוגמה לשרת אפליקציות מהימן

באפליקציה לדוגמה הבאה מוצג השימוש בממשקי ה-API שלמעלה. הדוגמה יוצרת שירות 'echo' שמטפל בכמה חיבורים נכנסים ומחזיר לשולח את כל ההודעות שהוא מקבל מהלקוחות שמקורם בצד המאובטח או הלא מאובטח.

#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;
}

השיטה run_end_to_end_msg_test() שולחת 10,000 הודעות באופן אסינכרוני לשירות 'echo' ומטפלת בתשובות.

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;
}

אפליקציות וממשקי API בעולם לא מאובטח

קבוצה של שירותי Trusty, שפורסמו מהצד המאובטח ומסומנים במאפיין IPC_PORT_ALLOW_NS_CONNECT, נגישה לתוכניות של הליבה ומרחב המשתמש שפועלות בצד הלא מאובטח.

סביבת הביצוע בצד הלא מאובטח (ליבה ומרחב משתמש) שונה באופן משמעותי מסביבת הביצוע בצד המאובטח. לכן, במקום ספרייה אחת לשתי הסביבות, יש שתי קבוצות שונות של ממשקי API. בליבה, ממשק ה-API של הלקוח מסופק על ידי מנהל הליבה של trusty-ipc, ומירשם צומת של מכשיר תווים שאפשר להשתמש בו בתהליכים במרחב המשתמש כדי לתקשר עם שירותים שפועלים בצד המאובטח.

User space Trusty IPC Client API

ספריית Trusty IPC Client API במרחב המשתמש היא שכבה דקה מעל צומת המכשיר fd.

תוכנית במרחב המשתמש מתחילה סשן תקשורת באמצעות קריאה ל-tipc_connect(), שמפעילה את החיבור לשירות Trusty שצוין. באופן פנימי, הקריאה tipc_connect() פותחת צומת מכשיר מסוים כדי לקבל מתאר קובץ, ומפעילה קריאה TIPC_IOC_CONNECT ioctl() עם הפרמטר argp שמצביע על מחרוזת שמכילה את שם השירות שאליו רוצים להתחבר.

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

אפשר להשתמש במתאר הקובץ שנוצר רק כדי לתקשר עם השירות שעבורו הוא נוצר. כשהחיבור לא נדרש יותר, צריך לקרוא ל-tipc_close() כדי לסגור את מתאר הקובץ.

מתאר הקובץ שמתקבל בקריאה ל-tipc_connect() מתנהג כצומת אופי טיפוסי של מכשיר. מתאר הקובץ:

  • אפשר להעביר אותו למצב לא חוסם במקרה הצורך
  • אפשר לכתוב אליו באמצעות קריאה רגילה ל-write() כדי לשלוח הודעות לצד השני
  • אפשר לבצע סקרים (באמצעות קריאות poll() או קריאות select()) כדי לבדוק את הזמינות של הודעות נכנסות כמתאר קובץ רגיל
  • ניתן לקרוא אותו כדי לאחזר הודעות נכנסות

מבצע קריאה לכתיבה של fd שצוין כדי לשלוח הודעה לשירות Trusty. כל הנתונים המועברים לקריאה write() שלמעלה הופכים להודעה על ידי הנהג trusty-ipc. ההודעה מועברת לצד המאובטח, שבו הנתונים מטופלים על ידי מערכת המשנה של IPC בליבה של Trusty, מנותבים ליעד המתאים ומועברים לולאת האירועים של האפליקציה כאירוע IPC_HANDLE_POLL_MSG במזהה ערוץ מסוים. בהתאם לפרוטוקול הספציפי של השירות, שירות Trusty עשוי לשלוח הודעה אחת או יותר בתשובה, שיועברו בחזרה לצד הלא מאובטח ויוצבו בתור ההודעות המתאים של מתאר הקובץ של הערוץ, כדי שהקריאה read() של אפליקציית מרחב המשתמש תוכל לאחזר אותן.

tipc_connect()

הפונקציה פותחת צומת מכשיר tipc ספציפי ומתחילה חיבור לשירות Trusty ספציפי.

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

[in] dev_name: הנתיב לצומת של מכשיר Trusty IPC שרוצים לפתוח

[in] srv_name: השם של שירות Trusty שפורסם שאליו רוצים להתחבר

[retval]: מתאר קובץ תקין במקרה של הצלחה, -1 במקרה אחר.

tipc_close()

סגירת החיבור לשירות Trusty שצוין על ידי מתאר קובץ.

int tipc_close(int fd);

[in] fd: תיאור קובץ שנפתח בעבר באמצעות קריאה ל-tipc_connect()

Kernel Trusty IPC Client API

ממשק ה-API של לקוח Trusty IPC בליבה זמין למנהלי הליבה. ממשק ה-API של Trusty IPC במרחב המשתמש מיושם מעל ה-API הזה.

באופן כללי, השימוש הרגיל ב-API הזה מורכב מבעל קריאה שיוצר אובייקט struct tipc_chan באמצעות הפונקציה tipc_create_channel(), ולאחר מכן משתמש בקריאה tipc_chan_connect() כדי ליצור חיבור לשירות Trusty IPC שפועל בצד המאובטח. כדי לסיים את החיבור לצד המרוחק ולנקות את המשאבים, אפשר להפעיל את הפונקציה tipc_chan_shutdown() ואז את הפונקציה tipc_chan_destroy().

כשמתקבלת התראה (דרך קריאת החזרה handle_event()) על כך שהחיבור נוצר בהצלחה, מבצעים את הפעולות הבאות:

  • אחסון הודעות במאגר באמצעות קריאה ל-tipc_chan_get_txbuf_timeout()
  • כותב הודעה,
  • הוספת ההודעה לתור באמצעות השיטה tipc_chan_queue_msg() להעברה לשירות Trusty (בצד המאובטח) שאליו מחובר הערוץ

אחרי שההודעה מצורפת לתור, מבצע הקריאה צריך לשכוח את מאגר ההודעות כי הוא יחזור בסופו של דבר למאגר המאגרים הפנויים אחרי העיבוד בצד המרוחק (כדי שאפשר יהיה לעשות בו שימוש חוזר בהמשך, להודעות אחרות). המשתמש צריך להפעיל את tipc_chan_put_txbuf() רק אם הוא לא מצליח להוסיף את המאגר הזה לתור או אם הוא כבר לא נדרש.

משתמש API מקבל הודעות מהצד המרוחק על ידי טיפול בקריאה חוזרת (callback) של התראה handle_msg() (שנקראת בהקשר של מאגר העבודה rx של trusty-ipc) שמספקת הפניה למאגר rx שמכיל הודעה נכנסת לטיפול.

ההטמעה של הקריאה החוזרת (callback) של handle_msg() אמורה להחזיר הפניה ל-struct tipc_msg_buf תקין. הוא יכול להיות זהה למאגר ההודעות הנכנסות אם הוא מטופל באופן מקומי ולא נדרש יותר. לחלופין, יכול להיות שמדובר במאגר חדש שהתקבל באמצעות קריאה ל-tipc_chan_get_rxbuf() אם המאגר הנכנס נמצא בתור לעיבוד נוסף. צריך לעקוב אחרי מאגר rx מנותק, ובסופו של דבר לשחרר אותו באמצעות קריאה ל-tipc_chan_put_rxbuf() כשלא צריך אותו יותר.

שיטות ב-Kernel Trusty IPC Client API

tipc_create_channel()

יצירת מכונה והגדרה שלה של ערוץ Trusty IPC למכשיר trusty-ipc מסוים.

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

[in] dev: הפניה ל-trusty-ipc שעבורו נוצר ערוץ המכשיר

[in] ops: הפניה ל-struct tipc_chan_ops, עם פונקציות קריאה חוזרת ספציפיות למבצע הקריאה

[in] cb_arg: הפניה לנתונים שמועברים להפניות החזרה (callbacks) של tipc_chan_ops

[retval]: אם הפונקציה מצליחה, הפונקציה מחזירה את הפניה למכונה החדשה שנוצרה של struct tipc_chan, אחרת היא מחזירה את הערך ERR_PTR(err).

באופן כללי, מבצע הקריאה חייב לספק שתי פונקציות קריאה חוזרת (callbacks) שמופעלות באופן אסינכררוני כשהפעילות המתאימה מתרחשת.

האירוע void (*handle_event)(void *cb_arg, int event) מופעל כדי להודיע למבצע הקריאה על שינוי במצב הערוץ.

[in] cb_arg: הפניה לנתונים שהועברו לקריאה של tipc_create_channel()

[in] event: אירוע שיכול להיות אחד מהערכים הבאים:

  • TIPC_CHANNEL_CONNECTED – מציין חיבור מוצלח לצד המרוחק
  • TIPC_CHANNEL_DISCONNECTED – מציין שהצד המרוחק דחה את בקשת החיבור החדשה או ביקש ניתוק מהערוץ שהיה מחובר בעבר
  • TIPC_CHANNEL_SHUTDOWN – מציין שהצד המרוחק נסגר, וכל החיבורים מסתיימים באופן סופי

פונקציית ה-callback‏ struct tipc_msg_buf *(*handle_msg)(void *cb_arg, struct tipc_msg_buf *mb) מופעלת כדי לספק התראה על קבלת הודעה חדשה בערוץ מסוים:

  • [in] cb_arg: הפניה לנתונים שהועברו לקריאה tipc_create_channel()
  • [in] mb: הפניה ל-struct tipc_msg_buf שמתאר הודעה נכנסת
  • [retval]: ההטמעה של פונקציית ה-callback אמורה להחזיר הפניה ל-struct tipc_msg_buf, שיכולה להיות אותה הפניה שהתקבלה כפרמטר mb אם ההודעה מטופלת באופן מקומי ולא נדרשת יותר (או שהיא יכולה להיות מאגר חדש שהתקבל בקריאה tipc_chan_get_rxbuf()).

tipc_chan_connect()

התחלת חיבור לשירות Trusty IPC שצוין.

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

[in] chan: הפניה לערוץ שהוחזר על ידי הקריאה tipc_create_chan()

[in] port: הפניה למחרוזת שמכילה את שם השירות שאליו רוצים להתחבר

[retval]: 0 אם הפעולה בוצעה בהצלחה, שגיאה שלילית אחרת

המתקשר יקבל הודעה על כך שהחיבור הוקם באמצעות קריאה חוזרת (call back) של handle_event.

tipc_chan_shutdown()

הפונקציה מסיימת את החיבור לשירות Trusty IPC שהתחיל בקריאה ל-tipc_chan_connect().

int tipc_chan_shutdown(struct tipc_chan *chan);

[in] chan: הפניה לערוץ שהוחזר על ידי קריאה ל-tipc_create_chan()

tipc_chan_destroy()‎

הורדה מהאוויר של ערוץ Trusty IPC ספציפי.

void tipc_chan_destroy(struct tipc_chan *chan);

[in] chan: הפניה לערוץ שהוחזר על ידי הקריאה tipc_create_chan()

tipc_chan_get_txbuf_timeout()

אחסון הודעות שאפשר להשתמש בו לשליחת נתונים בערוץ מסוים. אם המאגר לא זמין באופן מיידי, יכול להיות שהמבצע הקורא ייחסם למשך זמן הקצאת הזמן שצוין (במילי-שניות).

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

[in] chan: הפניה לערוץ שאליו רוצים להוסיף את ההודעה לתור

[in] chan: זמן קצוב מקסימלי להמתנה עד שהמאגר tx יהיה זמין

[retval]: מאגר הודעות תקין במקרה של הצלחה, ERR_PTR(err) במקרה של שגיאה

tipc_chan_queue_msg()

הוספת הודעה לתור לשליחה דרך ערוצי Trusty IPC שצוינו.

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

[in] chan: הפניה לערוץ שאליו להוסיף את ההודעה לתור

[in] mb: הפניה להודעה שרוצים להוסיף לתור (מתקבלת באמצעות קריאה ל-tipc_chan_get_txbuf_timeout())

[retval]: 0 אם הפעולה בוצעה בהצלחה, שגיאה שלילית אחרת

tipc_chan_put_txbuf()

משחררת את מאגר ההודעות Tx שצוין, שהתקבל בעבר באמצעות קריאה ל-tipc_chan_get_txbuf_timeout().

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

[in] chan: הפניה לערוץ שאליו שייך מאגר ההודעות הזה

[in] mb: הפניה למאגר ההודעות שרוצים לשחרר

[retval]: None

tipc_chan_get_rxbuf()

הפונקציה מקבלת מאגר הודעות חדש שאפשר להשתמש בו כדי לקבל הודעות דרך הערוץ שצוין.

struct tipc_msg_buf *tipc_chan_get_rxbuf(struct tipc_chan *chan);

[in] chan: הפניה לערוץ שאליו שייך מאגר ההודעות הזה

[retval]: מאגר הודעות תקין במקרה של הצלחה, ERR_PTR(err) במקרה של שגיאה

tipc_chan_put_rxbuf()

שחרור של מאגר הודעות ספציפי שהתקבל בעבר באמצעות קריאה ל-tipc_chan_get_rxbuf().

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

[in] chan: הפניה לערוץ שאליו שייך מאגר ההודעות הזה

[in] mb: הפניה למאגר הודעות שרוצים לשחרר

[retval]: None