[PATCH v4 07/16] efi_loader: capsule: add capsule_on_disk support
Heinrich Schuchardt
xypron.glpk at gmx.de
Thu Jul 23 17:50:09 CEST 2020
On 22.07.20 08:05, AKASHI Takahiro wrote:
> Capsule data can be loaded into the system either via UpdateCapsule
> runtime service or files on a file system (of boot device).
> The latter case is called "capsules on disk", and actual updates will
> take place at the next boot time.
>
> In this commit, we will support capsule on disk mechanism.
>
> Please note that U-Boot itself has no notion of "boot device" and
> all the capsule files to be executed will be detected only if they
> are located in a specific directory, \EFI\UpdateCapsule, on a device
> that is identified as a boot device by "BootXXXX" variables.
>
> Signed-off-by: AKASHI Takahiro <takahiro.akashi at linaro.org>
> ---
> common/main.c | 4 +
> include/efi_loader.h | 16 ++
> lib/efi_loader/Kconfig | 22 ++
> lib/efi_loader/efi_capsule.c | 510 +++++++++++++++++++++++++++++++++++
> lib/efi_loader/efi_setup.c | 8 +
> 5 files changed, 560 insertions(+)
>
> diff --git a/common/main.c b/common/main.c
> index 62ab3344e529..71fb749be4f4 100644
> --- a/common/main.c
> +++ b/common/main.c
> @@ -16,6 +16,7 @@
> #include <init.h>
> #include <net.h>
> #include <version.h>
> +#include <efi_loader.h>
>
> static void run_preboot_environment_command(void)
> {
> @@ -50,6 +51,9 @@ void main_loop(void)
> if (IS_ENABLED(CONFIG_USE_PREBOOT))
> run_preboot_environment_command();
>
> + if (IS_ENABLED(CONFIG_EFI_CAPSULE_ON_DISK_EARLY))
> + efi_launch_capsules();
> +
> s = bootdelay_process();
> if (cli_process_fdt(&s))
> cli_secure_boot_cmd(s);
> diff --git a/include/efi_loader.h b/include/efi_loader.h
> index a754fb0ed460..7e00bf3b33f3 100644
> --- a/include/efi_loader.h
> +++ b/include/efi_loader.h
> @@ -808,6 +808,18 @@ efi_status_t EFIAPI efi_query_capsule_caps(
> u64 *maximum_capsule_size,
> u32 *reset_type);
>
> +#ifdef CONFIG_EFI_CAPSULE_ON_DISK
This #ifdef seems unnecessary. No code will invoke efi_launch_capsules()
if CONFIG_EFI_CAPSULE_ON_DISK is not set.
> +#define EFI_CAPSULE_DIR L"\\EFI\\UpdateCapsule\\"
> +
> +/* Hook at initialization */
> +efi_status_t efi_launch_capsules(void);
> +#else
> +static inline efi_status_t efi_launch_capsules(void)
> +{
> + return EFI_SUCCESS;
> +}
> +#endif /* CONFIG_EFI_CAPSULE_ON_DISK */
> +
> #else /* CONFIG_IS_ENABLED(EFI_LOADER) */
>
> /* Without CONFIG_EFI_LOADER we don't have a runtime section, stub it out */
> @@ -824,6 +836,10 @@ static inline void efi_set_bootdev(const char *dev, const char *devnr,
> const char *path) { }
> static inline void efi_net_set_dhcp_ack(void *pkt, int len) { }
> static inline void efi_print_image_infos(void *pc) { }
> +static inline efi_status_t efi_launch_capsules(void)
> +{
> + return EFI_SUCCESS;
> +}
>
> #endif /* CONFIG_IS_ENABLED(EFI_LOADER) */
>
> diff --git a/lib/efi_loader/Kconfig b/lib/efi_loader/Kconfig
> index ee9ebe348ad9..6e35cbe64c7f 100644
> --- a/lib/efi_loader/Kconfig
> +++ b/lib/efi_loader/Kconfig
> @@ -104,6 +104,28 @@ config EFI_RUNTIME_UPDATE_CAPSULE
> Select this option if you want to use UpdateCapsule and
> QueryCapsuleCapabilities API's.
>
> +config EFI_CAPSULE_ON_DISK
> + bool "Enable capsule-on-disk support"
> + select EFI_HAVE_CAPSULE_SUPPORT
> + default n
> + help
> + Select this option if you want to use capsule-on-disk feature,
> + that is, capsules can be fetched and executed from files
> + under a specific directory on UEFI system partition instead of
> + via UpdateCapsule API.
> +
> +config EFI_CAPSULE_ON_DISK_EARLY
> + bool "Initiate capsule-on-disk at U-Boot boottime"
> + depends on EFI_CAPSULE_ON_DISK
> + default y
> + select EFI_SETUP_EARLY
> + help
> + Normally, without this option enabled, capsules will be
> + executed only at the first time of invoking one of efi command.
> + If this option is enabled, capsules will be enforced to be
> + executed as part of U-Boot initialisation so that they will
> + surely take place whatever is set to distro_bootcmd.
> +
> config EFI_DEVICE_PATH_TO_TEXT
> bool "Device path to text protocol"
> default y
> diff --git a/lib/efi_loader/efi_capsule.c b/lib/efi_loader/efi_capsule.c
> index cfe422bee924..2a224546dd11 100644
> --- a/lib/efi_loader/efi_capsule.c
> +++ b/lib/efi_loader/efi_capsule.c
> @@ -10,10 +10,16 @@
> #include <efi_loader.h>
> #include <fs.h>
> #include <malloc.h>
> +#include <mapmem.h>
> #include <sort.h>
>
> const efi_guid_t efi_guid_capsule_report = EFI_CAPSULE_REPORT_GUID;
>
> +#ifdef CONFIG_EFI_CAPSULE_ON_DISK
> +/* for file system access */
> +static struct efi_file_handle *bootdev_root;
> +#endif
> +
> /**
> * get_last_capsule - get the last capsule number
> *
> @@ -166,3 +172,507 @@ efi_status_t EFIAPI efi_query_capsule_caps(
> out:
> return EFI_EXIT(ret);
> }
> +
> +#ifdef CONFIG_EFI_CAPSULE_ON_DISK
> +/**
> + * get_dp_device - retrieve a device path from boot variable
> + * @boot_var: Boot variable name
> + * @device_dp Device path
> + *
> + * Retrieve a device patch from boot variable, @boot_var.
> + *
> + * Return: status code
> + */
> +static efi_status_t get_dp_device(u16 *boot_var,
> + struct efi_device_path **device_dp)
> +{
> + void *buf = NULL;
> + efi_uintn_t size;
> + struct efi_load_option lo;
> + struct efi_device_path *file_dp;
> + efi_status_t ret;
> +
> + size = 0;
> + ret = EFI_CALL(efi_get_variable(boot_var, &efi_global_variable_guid,
> + NULL, &size, NULL));
> + if (ret == EFI_BUFFER_TOO_SMALL) {
> + buf = malloc(size);
> + if (!buf)
> + return EFI_OUT_OF_RESOURCES;
> + ret = EFI_CALL(efi_get_variable(boot_var,
> + &efi_global_variable_guid,
> + NULL, &size, buf));
> + }
> + if (ret != EFI_SUCCESS)
> + return ret;
> +
> + efi_deserialize_load_option(&lo, buf, &size);
> +
> + if (lo.attributes & LOAD_OPTION_ACTIVE) {
> + efi_dp_split_file_path(lo.file_path, device_dp, &file_dp);
> + efi_free_pool(file_dp);
> +
> + ret = EFI_SUCCESS;
> + } else {
> + ret = EFI_NOT_FOUND;
> + }
> +
> + free(buf);
> +
> + return ret;
> +}
> +
> +/**
> + * device_is_present_and_system_part - check if a device exists
> + * @dp Device path
> + *
> + * Check if a device pointed to by the device path, @dp, exists and is
> + * located in UEFI system partition.
> + *
> + * Return: true - yes, false - no
> + */
> +static bool device_is_present_and_system_part(struct efi_device_path *dp)
> +{
> + efi_handle_t handle;
> +
> + handle = efi_dp_find_obj(dp, NULL);
> + if (!handle)
> + return false;
> +
> + return efi_disk_is_system_part(handle);
> +}
> +
> +/**
> + * find_boot_device - identify the boot device
> + *
> + * Identify the boot device from boot-related variables as UEFI
> + * specification describes and put its handle into bootdev_root.
If none of the Boot* variables is defined we should still be able to do
a capsule update. Please, use efi_system_partition as fallback.
> + *
> + * Return: status code
> + */
> +static efi_status_t find_boot_device(void)
> +{
> + char boot_var[9];
> + u16 boot_var16[9], *p, bootnext, *boot_order = NULL;
> + efi_uintn_t size;
> + int i, num;
> + struct efi_simple_file_system_protocol *volume;
> + struct efi_device_path *boot_dev = NULL;
> + efi_status_t ret;
> +
> + /* find active boot device in BootNext */
> + bootnext = 0;
> + size = sizeof(bootnext);
> + ret = EFI_CALL(efi_get_variable(L"BootNext",
Please, avoid EFI_CALL. Use efi_get_variable_int().
> + (efi_guid_t *)&efi_global_variable_guid,
> + NULL, &size, &bootnext));
> + if (ret == EFI_SUCCESS || ret == EFI_BUFFER_TOO_SMALL) {
> + /* BootNext does exist here */
> + if (ret == EFI_BUFFER_TOO_SMALL || size != sizeof(u16)) {
> + printf("BootNext must be 16-bit integer\n");
> + goto skip;
> + }
> + sprintf((char *)boot_var, "Boot%04X", bootnext);
> + p = boot_var16;
> + utf8_utf16_strcpy(&p, boot_var);
We have this type of conversion in multiple places. Both for BootXXXX as
well as for CapsuleXXXX. Both in capsule updates as well as in
try_load_entry().
Please, provide a libary function.
> +
> + ret = get_dp_device(boot_var16, &boot_dev);
> + if (ret == EFI_SUCCESS) {
> + if (device_is_present_and_system_part(boot_dev)) {
> + goto out;
> + } else {
> + efi_free_pool(boot_dev);
> + boot_dev = NULL;
> + }
> + }
> + }
> +
> +skip:
> + /* find active boot device in BootOrder */
> + size = 0;
> + ret = EFI_CALL(efi_get_variable(L"BootOrder", &efi_global_variable_guid,
> + NULL, &size, NULL));
> + if (ret == EFI_BUFFER_TOO_SMALL) {
> + boot_order = malloc(size);
> + if (!boot_order) {
> + ret = EFI_OUT_OF_RESOURCES;
> + goto out;
> + }
> +
> + ret = EFI_CALL(efi_get_variable(
> + L"BootOrder", &efi_global_variable_guid,
> + NULL, &size, boot_order));
> + }
> + if (ret != EFI_SUCCESS)
> + goto out;
> +
> + /* check in higher order */
> + num = size / sizeof(u16);
> + for (i = 0; i < num; i++) {
> + sprintf((char *)boot_var, "Boot%04X", boot_order[i]);
> + p = boot_var16;
> + utf8_utf16_strcpy(&p, boot_var);
> + ret = get_dp_device(boot_var16, &boot_dev);
> + if (ret != EFI_SUCCESS)
> + continue;
> +
> + if (device_is_present_and_system_part(boot_dev))
> + break;
> +
> + efi_free_pool(boot_dev);
> + boot_dev = NULL;
> + }
> +out:
> + if (boot_dev) {
> + u16 *path_str;
> +
> + path_str = efi_dp_str(boot_dev);
> + EFI_PRINT("EFI Capsule: bootdev is %ls\n", path_str);
> + efi_free_pool(path_str);
> +
> + volume = efi_fs_from_path(boot_dev);
> + if (!volume)
> + ret = EFI_DEVICE_ERROR;
> + else
> + ret = EFI_CALL(volume->open_volume(volume,
> + &bootdev_root));
> + efi_free_pool(boot_dev);
> + } else {
> + ret = EFI_NOT_FOUND;
> + }
> + free(boot_order);
> +
> + return ret;
> +}
> +
> +/**
> + * efi_capsule_scan_dir - traverse a capsule directory in boot device
> + * @files: Array of file names
> + * @num: Number of elements in @files
> + *
> + * Traverse a capsule directory in boot device.
> + * Called by initialization code, and returns an array of capsule file
> + * names in @files.
> + *
> + * Return: status code
> + */
> +static efi_status_t efi_capsule_scan_dir(u16 ***files, int *num)
> +{
> + struct efi_file_handle *dirh;
> + struct efi_file_info *dirent;
> + efi_uintn_t dirent_size, tmp_size;
> + int count;
> + u16 **tmp_files;
> + efi_status_t ret;
> +
> + ret = find_boot_device();
> + if (ret == EFI_NOT_FOUND) {
> + EFI_PRINT("EFI Capsule: bootdev is not set\n");
> + *num = 0;
> + return EFI_SUCCESS;
> + } else if (ret != EFI_SUCCESS) {
> + return EFI_DEVICE_ERROR;
> + }
> +
> + /* count capsule files */
> + ret = EFI_CALL((*bootdev_root->open)(bootdev_root, &dirh,
> + EFI_CAPSULE_DIR,
> + EFI_FILE_MODE_READ, 0));
> + if (ret != EFI_SUCCESS) {
> + *num = 0;
> + return EFI_SUCCESS;
> + }
> +
> + dirent_size = 256;
> + dirent = malloc(dirent_size);
> + if (!dirent)
> + return EFI_OUT_OF_RESOURCES;
> +
> + count = 0;
> + while (1) {
> + tmp_size = dirent_size;
> + ret = EFI_CALL((*dirh->read)(dirh, &tmp_size, dirent));
> + if (ret == EFI_BUFFER_TOO_SMALL) {
> + dirent = realloc(dirent, tmp_size);
> + if (!dirent) {
> + ret = EFI_OUT_OF_RESOURCES;
> + goto err;
> + }
> + dirent_size = tmp_size;
> + ret = EFI_CALL((*dirh->read)(dirh, &tmp_size, dirent));
> + }
> + if (ret != EFI_SUCCESS)
> + goto err;
> + if (!tmp_size)
> + break;
> +
> + if (!(dirent->attribute & EFI_FILE_DIRECTORY) &&
> + u16_strcmp(dirent->file_name, L".") &&
> + u16_strcmp(dirent->file_name, L".."))
> + count++;
> + }
> +
> + ret = EFI_CALL((*dirh->setpos)(dirh, 0));
> + if (ret != EFI_SUCCESS)
> + goto err;
> +
> + /* make a list */
> + tmp_files = malloc(count * sizeof(*files));
> + if (!tmp_files) {
> + ret = EFI_OUT_OF_RESOURCES;
> + goto err;
> + }
> +
> + count = 0;
> + while (1) {
> + tmp_size = dirent_size;
> + ret = EFI_CALL((*dirh->read)(dirh, &tmp_size, dirent));
> + if (ret != EFI_SUCCESS)
> + goto err;
> + if (!tmp_size)
> + break;
> +
> + if (!(dirent->attribute & EFI_FILE_DIRECTORY) &&
> + u16_strcmp(dirent->file_name, L".") &&
> + u16_strcmp(dirent->file_name, L".."))
> + tmp_files[count++] = u16_strdup(dirent->file_name);
> + }
> + /* ignore an error */
> + EFI_CALL((*dirh->close)(dirh));
> +
> + /* in ascii order */
> + /* FIXME: u16 version of strcasecmp */
> + qsort(tmp_files, count, sizeof(*tmp_files),
> + (int (*)(const void *, const void *))strcasecmp);
> + *files = tmp_files;
> + *num = count;
> + ret = EFI_SUCCESS;
> +err:
> + free(dirent);
> +
> + return ret;
> +}
> +
> +/**
> + * efi_capsule_read_file - read in a capsule file
> + * @filename: File name
> + * @capsule: Pointer to buffer for capsule
> + *
> + * Read a capsule file and put its content in @capsule.
> + *
> + * Return: status code
> + */
> +static efi_status_t efi_capsule_read_file(u16 *filename,
const u16 *filename
> + struct efi_capsule_header **capsule)
> +{
> + struct efi_file_handle *dirh, *fh;
> + struct efi_file_info *file_info = NULL;
> + struct efi_capsule_header *buf = NULL;
> + efi_uintn_t size;
> + efi_status_t ret;
> +
> + ret = EFI_CALL((*bootdev_root->open)(bootdev_root, &dirh,
> + EFI_CAPSULE_DIR,
> + EFI_FILE_MODE_READ, 0));
> + if (ret != EFI_SUCCESS)
> + return ret;
> + ret = EFI_CALL((*dirh->open)(dirh, &fh, filename,
> + EFI_FILE_MODE_READ, 0));
> + /* ignore an error */
> + EFI_CALL((*dirh->close)(dirh));
> + if (ret != EFI_SUCCESS)
> + return ret;
> +
> + /* file size */
> + size = 0;
> + ret = EFI_CALL((*fh->getinfo)(fh, &efi_file_info_guid,
> + &size, file_info));
> + if (ret == EFI_BUFFER_TOO_SMALL) {
> + file_info = malloc(size);
> + if (!file_info) {
> + ret = EFI_OUT_OF_RESOURCES;
> + goto err;
> + }
> + ret = EFI_CALL((*fh->getinfo)(fh, &efi_file_info_guid,
> + &size, file_info));
> + }
> + if (ret != EFI_SUCCESS)
> + goto err;
> + size = file_info->file_size;
> + free(file_info);
> + buf = malloc(size);
> + if (!buf) {
> + ret = EFI_OUT_OF_RESOURCES;
> + goto err;
> + }
> +
> + /* fetch data */
> + ret = EFI_CALL((*fh->read)(fh, &size, buf));
> + if (ret == EFI_SUCCESS) {
> + if (size >= buf->capsule_image_size) {
> + *capsule = buf;
> + } else {
> + free(buf);
> + ret = EFI_INVALID_PARAMETER;
> + }
> + } else {
> + free(buf);
> + }
> +err:
> + EFI_CALL((*fh->close)(fh));
> +
> + return ret;
> +}
> +
> +/**
> + * efi_capsule_delete_file - delete a capsule file
> + * @filename: File name
> + *
> + * Delete a capsule file from capsule directory.
> + *
> + * Return: status code
> + */
> +static efi_status_t efi_capsule_delete_file(u16 *filename)
const u16 *filename
> +{
> + struct efi_file_handle *dirh, *fh;
> + efi_status_t ret;
> +
> + ret = EFI_CALL((*bootdev_root->open)(bootdev_root, &dirh,
> + EFI_CAPSULE_DIR,
> + EFI_FILE_MODE_READ, 0));
> + if (ret != EFI_SUCCESS)
> + return ret;
> + ret = EFI_CALL((*dirh->open)(dirh, &fh, filename,
> + EFI_FILE_MODE_READ, 0));
> + /* ignore an error */
> + EFI_CALL((*dirh->close)(dirh));
> +
> + ret = EFI_CALL((*fh->delete)(fh));
> +
> + return ret;
> +}
> +
> +/**
> + * efi_capsule_scan_done - reset a scan help function
> + *
> + * Reset a scan help function
> + */
> +static void efi_capsule_scan_done(void)
> +{
> + EFI_CALL((*bootdev_root->close)(bootdev_root));
> + bootdev_root = NULL;
> +}
> +
> +/**
> + * arch_efi_load_capsule_drivers - initialize capsule drivers
> + *
> + * Architecture or board specific initialization routine
> + *
> + * Return: status code
> + */
> +efi_status_t __weak arch_efi_load_capsule_drivers(void)
> +{
> + return EFI_SUCCESS;
> +}
> +
> +/**
> + * efi_launch_capsule - launch capsules
> + *
> + * Launch all the capsules in system at boot time.
> + * Called by efi init code
> + *
> + * Return: status codde
> + */
> +efi_status_t efi_launch_capsules(void)
> +{
> + u64 os_indications;
> + efi_uintn_t size;
> + struct efi_capsule_header *capsule = NULL;
> + u16 **files;
> + int nfiles, num, i;
> + char variable_name[12];
> + u16 variable_name16[12], *p;
> + efi_status_t ret;
> +
> + size = sizeof(os_indications);
> + ret = EFI_CALL(efi_get_variable(L"OsIndications",
> + &efi_global_variable_guid,
> + NULL, &size, &os_indications));
> + if (ret != EFI_SUCCESS ||
> + !(os_indications
> + & EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED))
> + return EFI_SUCCESS;
> +
> + num = get_last_capsule();
> +
> + /* Load capsule drivers */
> + ret = arch_efi_load_capsule_drivers();
> + if (ret != EFI_SUCCESS)
> + return ret;
> +
> + /*
> + * Find capsules on disk.
> + * All the capsules are collected at the beginning because
> + * capsule files will be removed instantly.
> + */
> + nfiles = 0;
> + files = NULL;
> + ret = efi_capsule_scan_dir(&files, &nfiles);
> + if (ret != EFI_SUCCESS)
> + return ret;
> + if (!nfiles)
> + return EFI_SUCCESS;
> +
> + /* Launch capsules */
> + for (i = 0, ++num; i < nfiles; i++, num++) {
> + EFI_PRINT("capsule from %ls ...\n", files[i]);
> + if (num > 0xffff)
> + num = 0;
> + ret = efi_capsule_read_file(files[i], &capsule);
> + if (ret == EFI_SUCCESS) {
> + ret = EFI_CALL(efi_update_capsule(&capsule, 1, 0));
> + if (ret != EFI_SUCCESS)
> + printf("EFI Capsule update failed at %ls\n",
> + files[i]);
> +
> + free(capsule);
> + } else {
> + printf("EFI: reading capsule failed: %ls\n",
> + files[i]);
> + }
> + /* create CapsuleXXXX */
> + set_capsule_result(num, capsule, ret);
> +
> + /* delete a capsule either in case of success or failure */
> + ret = efi_capsule_delete_file(files[i]);
> + if (ret != EFI_SUCCESS)
> + printf("EFI: deleting a capsule file failed: %ls\n",
> + files[i]);
> + }
> + efi_capsule_scan_done();
> +
> + for (i = 0; i < nfiles; i++)
> + free(files[i]);
> + free(files);
> +
> + /* CapsuleMax */
> + p = variable_name16;
> + utf8_utf16_strncpy(&p, "CapsuleFFFF", 11);
> + EFI_CALL(efi_set_variable(L"CapsuleMax", &efi_guid_capsule_report,
> + EFI_VARIABLE_BOOTSERVICE_ACCESS |
> + EFI_VARIABLE_RUNTIME_ACCESS,
> + 22, variable_name16));
The variable must be read-only. Please, use efi_set_variable_int.
The variable should be set in efi_setup.c. It does not depend on the
execution of capsule updates.
> +
> + /* CapsuleLast */
> + sprintf(variable_name, "Capsule%04X", num - 1);
> + p = variable_name16;
> + utf8_utf16_strncpy(&p, variable_name, 11);
> + EFI_CALL(efi_set_variable(L"CapsuleLast", &efi_guid_capsule_report,
The variable must be read-only. Please, use efi_set_variable_int.
Best regards
Heinrich
> + EFI_VARIABLE_NON_VOLATILE |
> + EFI_VARIABLE_BOOTSERVICE_ACCESS |
> + EFI_VARIABLE_RUNTIME_ACCESS,
> + 22, variable_name16));
> +
> + return ret;
> +}
> +#endif /* CONFIG_EFI_CAPSULE_ON_DISK */
> diff --git a/lib/efi_loader/efi_setup.c b/lib/efi_loader/efi_setup.c
> index 2fc0c5d091b8..a0eb81f079e1 100644
> --- a/lib/efi_loader/efi_setup.c
> +++ b/lib/efi_loader/efi_setup.c
> @@ -132,6 +132,10 @@ static efi_status_t efi_init_os_indications(void)
> os_indications_supported |=
> EFI_OS_INDICATIONS_CAPSULE_RESULT_VAR_SUPPORTED;
>
> + if (IS_ENABLED(CONFIG_EFI_CAPSULE_ON_DISK))
> + os_indications_supported |=
> + EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED;
> +
> return efi_set_variable_int(L"OsIndicationsSupported",
> &efi_global_variable_guid,
> EFI_VARIABLE_BOOTSERVICE_ACCESS |
> @@ -243,6 +247,10 @@ efi_status_t efi_init_obj_list(void)
> if (ret != EFI_SUCCESS)
> goto out;
>
> + /* Execute capsules after reboot */
> + if (IS_ENABLED(CONFIG_EFI_CAPSULE_ON_DISK) &&
> + !IS_ENABLED(CONFIG_EFI_CAPSULE_ON_DISK_EARLY))
> + ret = efi_launch_capsules();
> out:
> efi_obj_list_initialized = ret;
> return ret;
>
More information about the U-Boot
mailing list