Introduce libfsverity

From the 'fsverity' program, split out a library 'libfsverity'.
Currently it supports computing file measurements ("digests"), and
signing those file measurements for use with the fs-verity builtin
signature verification feature.

Rewritten from patches by Jes Sorensen <jsorensen@fb.com>.
I made a lot of improvements, e.g.:

- Separated library and program source into different directories.
- Drastically improved the Makefile.
- Added 'make check' target and rules to build test programs.
- In the shared lib, only export the functions intended to be public.
- Prefixed global functions with "libfsverity_" so that they don't cause
  conflicts when the library is built as a static library.
- Made library error messages be sent to a user-specified callback
  rather than always be printed to stderr.
- Keep showing OpenSSL error messages.
- Stopped abort()ing in library code, when possible.
- Made libfsverity_digest use native endianness.
- Moved file_size into the merkle_tree_params.
- Made libfsverity_get_hash_name() just return the static strings.
- Made some variables in the API uint32_t instead of uint16_t.
- Shared parse_hash_alg_option() between cmd_enable and cmd_sign.
- Lots of other fixes.

(Folded in a couple Makefile fixes from Jes.)

Reviewed-by: Jes Sorensen <jsorensen@fb.com>
Signed-off-by: Eric Biggers <ebiggers@google.com>
This commit is contained in:
Eric Biggers
2020-05-25 13:45:31 -07:00
parent 64b919b111
commit 5cd90ca608
20 changed files with 1056 additions and 439 deletions
+140
View File
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* The 'fsverity enable' command
*
* Copyright 2018 Google LLC
*/
#include "fsverity.h"
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <sys/ioctl.h>
static bool read_signature(const char *filename, u8 **sig_ret,
u32 *sig_size_ret)
{
struct filedes file = { .fd = -1 };
u64 file_size;
u8 *sig = NULL;
bool ok = false;
if (!open_file(&file, filename, O_RDONLY, 0))
goto out;
if (!get_file_size(&file, &file_size))
goto out;
if (file_size <= 0) {
error_msg("signature file '%s' is empty", filename);
goto out;
}
if (file_size > 1000000) {
error_msg("signature file '%s' is too large", filename);
goto out;
}
sig = xmalloc(file_size);
if (!full_read(&file, sig, file_size))
goto out;
*sig_ret = sig;
*sig_size_ret = file_size;
sig = NULL;
ok = true;
out:
filedes_close(&file);
free(sig);
return ok;
}
enum {
OPT_HASH_ALG,
OPT_BLOCK_SIZE,
OPT_SALT,
OPT_SIGNATURE,
};
static const struct option longopts[] = {
{"hash-alg", required_argument, NULL, OPT_HASH_ALG},
{"block-size", required_argument, NULL, OPT_BLOCK_SIZE},
{"salt", required_argument, NULL, OPT_SALT},
{"signature", required_argument, NULL, OPT_SIGNATURE},
{NULL, 0, NULL, 0}
};
/* Enable fs-verity on a file. */
int fsverity_cmd_enable(const struct fsverity_command *cmd,
int argc, char *argv[])
{
struct fsverity_enable_arg arg = { .version = 1 };
u8 *salt = NULL;
u8 *sig = NULL;
struct filedes file;
int status;
int c;
while ((c = getopt_long(argc, argv, "", longopts, NULL)) != -1) {
switch (c) {
case OPT_HASH_ALG:
if (!parse_hash_alg_option(optarg, &arg.hash_algorithm))
goto out_usage;
break;
case OPT_BLOCK_SIZE:
if (!parse_block_size_option(optarg, &arg.block_size))
goto out_usage;
break;
case OPT_SALT:
if (!parse_salt_option(optarg, &salt, &arg.salt_size))
goto out_usage;
arg.salt_ptr = (uintptr_t)salt;
break;
case OPT_SIGNATURE:
if (sig != NULL) {
error_msg("--signature can only be specified once");
goto out_usage;
}
if (!read_signature(optarg, &sig, &arg.sig_size))
goto out_err;
arg.sig_ptr = (uintptr_t)sig;
break;
default:
goto out_usage;
}
}
argv += optind;
argc -= optind;
if (argc != 1)
goto out_usage;
if (arg.hash_algorithm == 0)
arg.hash_algorithm = FS_VERITY_HASH_ALG_DEFAULT;
if (arg.block_size == 0)
arg.block_size = get_default_block_size();
if (!open_file(&file, argv[0], O_RDONLY, 0))
goto out_err;
if (ioctl(file.fd, FS_IOC_ENABLE_VERITY, &arg) != 0) {
error_msg_errno("FS_IOC_ENABLE_VERITY failed on '%s'",
file.name);
filedes_close(&file);
goto out_err;
}
if (!filedes_close(&file))
goto out_err;
status = 0;
out:
free(salt);
free(sig);
return status;
out_err:
status = 1;
goto out;
out_usage:
usage(cmd, stderr);
status = 2;
goto out;
}
+65
View File
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* The 'fsverity measure' command
*
* Copyright 2018 Google LLC
*/
#include "fsverity.h"
#include <fcntl.h>
#include <sys/ioctl.h>
/* Display the measurement of the given verity file(s). */
int fsverity_cmd_measure(const struct fsverity_command *cmd,
int argc, char *argv[])
{
struct fsverity_digest *d = NULL;
struct filedes file;
char digest_hex[FS_VERITY_MAX_DIGEST_SIZE * 2 + 1];
char _hash_alg_name[32];
const char *hash_alg_name;
int status;
int i;
if (argc < 2)
goto out_usage;
d = xzalloc(sizeof(*d) + FS_VERITY_MAX_DIGEST_SIZE);
for (i = 1; i < argc; i++) {
d->digest_size = FS_VERITY_MAX_DIGEST_SIZE;
if (!open_file(&file, argv[i], O_RDONLY, 0))
goto out_err;
if (ioctl(file.fd, FS_IOC_MEASURE_VERITY, d) != 0) {
error_msg_errno("FS_IOC_MEASURE_VERITY failed on '%s'",
file.name);
filedes_close(&file);
goto out_err;
}
filedes_close(&file);
ASSERT(d->digest_size <= FS_VERITY_MAX_DIGEST_SIZE);
bin2hex(d->digest, d->digest_size, digest_hex);
hash_alg_name = libfsverity_get_hash_name(d->digest_algorithm);
if (!hash_alg_name) {
sprintf(_hash_alg_name, "ALG_%u", d->digest_algorithm);
hash_alg_name = _hash_alg_name;
}
printf("%s:%s %s\n", hash_alg_name, digest_hex, argv[i]);
}
status = 0;
out:
free(d);
return status;
out_err:
status = 1;
goto out;
out_usage:
usage(cmd, stderr);
status = 2;
goto out;
}
+163
View File
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* The 'fsverity sign' command
*
* Copyright 2018 Google LLC
*/
#include "fsverity.h"
#include <fcntl.h>
#include <getopt.h>
static bool write_signature(const char *filename, const u8 *sig, u32 sig_size)
{
struct filedes file;
bool ok;
if (!open_file(&file, filename, O_WRONLY|O_CREAT|O_TRUNC, 0644))
return false;
ok = full_write(&file, sig, sig_size);
ok &= filedes_close(&file);
return ok;
}
enum {
OPT_HASH_ALG,
OPT_BLOCK_SIZE,
OPT_SALT,
OPT_KEY,
OPT_CERT,
};
static const struct option longopts[] = {
{"hash-alg", required_argument, NULL, OPT_HASH_ALG},
{"block-size", required_argument, NULL, OPT_BLOCK_SIZE},
{"salt", required_argument, NULL, OPT_SALT},
{"key", required_argument, NULL, OPT_KEY},
{"cert", required_argument, NULL, OPT_CERT},
{NULL, 0, NULL, 0}
};
static int read_callback(void *file, void *buf, size_t count)
{
errno = 0;
if (!full_read(file, buf, count))
return errno ? -errno : -EIO;
return 0;
}
/* Sign a file for fs-verity by computing its measurement, then signing it. */
int fsverity_cmd_sign(const struct fsverity_command *cmd,
int argc, char *argv[])
{
struct filedes file = { .fd = -1 };
u8 *salt = NULL;
struct libfsverity_merkle_tree_params tree_params = { .version = 1 };
struct libfsverity_signature_params sig_params = {};
struct libfsverity_digest *digest = NULL;
char digest_hex[FS_VERITY_MAX_DIGEST_SIZE * 2 + 1];
u8 *sig = NULL;
size_t sig_size;
int status;
int c;
while ((c = getopt_long(argc, argv, "", longopts, NULL)) != -1) {
switch (c) {
case OPT_HASH_ALG:
if (!parse_hash_alg_option(optarg,
&tree_params.hash_algorithm))
goto out_usage;
break;
case OPT_BLOCK_SIZE:
if (!parse_block_size_option(optarg,
&tree_params.block_size))
goto out_usage;
break;
case OPT_SALT:
if (!parse_salt_option(optarg, &salt,
&tree_params.salt_size))
goto out_usage;
tree_params.salt = salt;
break;
case OPT_KEY:
if (sig_params.keyfile != NULL) {
error_msg("--key can only be specified once");
goto out_usage;
}
sig_params.keyfile = optarg;
break;
case OPT_CERT:
if (sig_params.certfile != NULL) {
error_msg("--cert can only be specified once");
goto out_usage;
}
sig_params.certfile = optarg;
break;
default:
goto out_usage;
}
}
argv += optind;
argc -= optind;
if (argc != 2)
goto out_usage;
if (tree_params.hash_algorithm == 0)
tree_params.hash_algorithm = FS_VERITY_HASH_ALG_DEFAULT;
if (tree_params.block_size == 0)
tree_params.block_size = get_default_block_size();
if (sig_params.keyfile == NULL) {
error_msg("Missing --key argument");
goto out_usage;
}
if (sig_params.certfile == NULL)
sig_params.certfile = sig_params.keyfile;
if (!open_file(&file, argv[0], O_RDONLY, 0))
goto out_err;
if (!get_file_size(&file, &tree_params.file_size))
goto out_err;
if (libfsverity_compute_digest(&file, read_callback,
&tree_params, &digest) != 0) {
error_msg("failed to compute digest");
goto out_err;
}
if (libfsverity_sign_digest(digest, &sig_params,
&sig, &sig_size) != 0) {
error_msg("failed to sign digest");
goto out_err;
}
if (!write_signature(argv[1], sig, sig_size))
goto out_err;
ASSERT(digest->digest_size <= FS_VERITY_MAX_DIGEST_SIZE);
bin2hex(digest->digest, digest->digest_size, digest_hex);
printf("Signed file '%s' (%s:%s)\n", argv[0],
libfsverity_get_hash_name(tree_params.hash_algorithm),
digest_hex);
status = 0;
out:
filedes_close(&file);
free(salt);
free(digest);
free(sig);
return status;
out_err:
status = 1;
goto out;
out_usage:
usage(cmd, stderr);
status = 2;
goto out;
}
+232
View File
@@ -0,0 +1,232 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* fs-verity userspace tool
*
* Copyright 2018 Google LLC
*/
#include "fsverity.h"
#include <limits.h>
#include <unistd.h>
static const struct fsverity_command {
const char *name;
int (*func)(const struct fsverity_command *cmd, int argc, char *argv[]);
const char *short_desc;
const char *usage_str;
} fsverity_commands[] = {
{
.name = "enable",
.func = fsverity_cmd_enable,
.short_desc = "Enable fs-verity on a file",
.usage_str =
" fsverity enable FILE\n"
" [--hash-alg=HASH_ALG] [--block-size=BLOCK_SIZE] [--salt=SALT]\n"
" [--signature=SIGFILE]\n"
}, {
.name = "measure",
.func = fsverity_cmd_measure,
.short_desc =
"Display the measurement of the given verity file(s)",
.usage_str =
" fsverity measure FILE...\n"
}, {
.name = "sign",
.func = fsverity_cmd_sign,
.short_desc = "Sign a file for fs-verity",
.usage_str =
" fsverity sign FILE OUT_SIGFILE --key=KEYFILE\n"
" [--hash-alg=HASH_ALG] [--block-size=BLOCK_SIZE] [--salt=SALT]\n"
" [--cert=CERTFILE]\n"
}
};
static void show_all_hash_algs(FILE *fp)
{
u32 alg_num = 1;
const char *name;
fprintf(fp, "Available hash algorithms:");
while ((name = libfsverity_get_hash_name(alg_num++)) != NULL)
fprintf(fp, " %s", name);
putc('\n', fp);
}
static void usage_all(FILE *fp)
{
int i;
fputs("Usage:\n", fp);
for (i = 0; i < ARRAY_SIZE(fsverity_commands); i++)
fprintf(fp, " %s:\n%s\n", fsverity_commands[i].short_desc,
fsverity_commands[i].usage_str);
fputs(
" Standard options:\n"
" fsverity --help\n"
" fsverity --version\n"
"\n", fp);
show_all_hash_algs(fp);
}
static void usage_cmd(const struct fsverity_command *cmd, FILE *fp)
{
fprintf(fp, "Usage:\n%s", cmd->usage_str);
}
void usage(const struct fsverity_command *cmd, FILE *fp)
{
if (cmd)
usage_cmd(cmd, fp);
else
usage_all(fp);
}
#define PACKAGE_VERSION "v1.0"
#define PACKAGE_BUGREPORT "linux-fscrypt@vger.kernel.org"
static void show_version(void)
{
static const char * const str =
"fsverity " PACKAGE_VERSION "\n"
"Copyright 2018 Google LLC\n"
"License GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl.html>.\n"
"This is free software: you are free to change and redistribute it.\n"
"There is NO WARRANTY, to the extent permitted by law.\n"
"\n"
"Report bugs to " PACKAGE_BUGREPORT ".\n";
fputs(str, stdout);
}
static void handle_common_options(int argc, char *argv[],
const struct fsverity_command *cmd)
{
int i;
for (i = 1; i < argc; i++) {
const char *arg = argv[i];
if (*arg++ != '-')
continue;
if (*arg++ != '-')
continue;
if (!strcmp(arg, "help")) {
usage(cmd, stdout);
exit(0);
} else if (!strcmp(arg, "version")) {
show_version();
exit(0);
} else if (!*arg) /* reached "--", no more options */
return;
}
}
static const struct fsverity_command *find_command(const char *name)
{
int i;
for (i = 0; i < ARRAY_SIZE(fsverity_commands); i++)
if (!strcmp(name, fsverity_commands[i].name))
return &fsverity_commands[i];
return NULL;
}
bool parse_hash_alg_option(const char *arg, u32 *alg_ptr)
{
char *end;
unsigned long n = strtoul(arg, &end, 10);
if (*alg_ptr != 0) {
error_msg("--hash-alg can only be specified once");
return false;
}
/* Specified by number? */
if (n > 0 && n < INT32_MAX && *end == '\0') {
*alg_ptr = n;
return true;
}
/* Specified by name? */
*alg_ptr = libfsverity_find_hash_alg_by_name(arg);
if (*alg_ptr)
return true;
error_msg("unknown hash algorithm: '%s'", arg);
show_all_hash_algs(stderr);
return false;
}
bool parse_block_size_option(const char *arg, u32 *size_ptr)
{
char *end;
unsigned long n = strtoul(arg, &end, 10);
if (*size_ptr != 0) {
error_msg("--block-size can only be specified once");
return false;
}
if (n <= 0 || n >= INT_MAX || !is_power_of_2(n) || *end != '\0') {
error_msg("Invalid block size: %s. Must be power of 2", arg);
return false;
}
*size_ptr = n;
return true;
}
bool parse_salt_option(const char *arg, u8 **salt_ptr, u32 *salt_size_ptr)
{
if (*salt_ptr != NULL) {
error_msg("--salt can only be specified once");
return false;
}
*salt_size_ptr = strlen(arg) / 2;
*salt_ptr = xmalloc(*salt_size_ptr);
if (!hex2bin(arg, *salt_ptr, *salt_size_ptr)) {
error_msg("salt is not a valid hex string");
return false;
}
return true;
}
u32 get_default_block_size(void)
{
long n = sysconf(_SC_PAGESIZE);
if (n <= 0 || n >= INT_MAX || !is_power_of_2(n)) {
fprintf(stderr,
"Warning: invalid _SC_PAGESIZE (%ld). Assuming 4K blocks.\n",
n);
return 4096;
}
return n;
}
static void print_libfsverity_error(const char *msg)
{
error_msg("%s", msg);
}
int main(int argc, char *argv[])
{
const struct fsverity_command *cmd;
libfsverity_set_error_callback(print_libfsverity_error);
if (argc < 2) {
error_msg("no command specified");
usage_all(stderr);
return 2;
}
cmd = find_command(argv[1]);
handle_common_options(argc, argv, cmd);
if (!cmd) {
error_msg("unrecognized command: '%s'", argv[1]);
usage_all(stderr);
return 2;
}
return cmd->func(cmd, argc - 1, argv + 1);
}
+43
View File
@@ -0,0 +1,43 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Private header for the 'fsverity' program
*
* Copyright 2018 Google LLC
*/
#ifndef PROGRAMS_FSVERITY_H
#define PROGRAMS_FSVERITY_H
#include "utils.h"
#include "../common/fsverity_uapi.h"
/* The hash algorithm that 'fsverity' assumes when none is specified */
#define FS_VERITY_HASH_ALG_DEFAULT FS_VERITY_HASH_ALG_SHA256
/*
* Largest digest size among all hash algorithms supported by fs-verity.
* This can be increased if needed.
*/
#define FS_VERITY_MAX_DIGEST_SIZE 64
struct fsverity_command;
/* cmd_enable.c */
int fsverity_cmd_enable(const struct fsverity_command *cmd,
int argc, char *argv[]);
/* cmd_measure.c */
int fsverity_cmd_measure(const struct fsverity_command *cmd,
int argc, char *argv[]);
/* cmd_sign.c */
int fsverity_cmd_sign(const struct fsverity_command *cmd,
int argc, char *argv[]);
/* fsverity.c */
void usage(const struct fsverity_command *cmd, FILE *fp);
bool parse_hash_alg_option(const char *arg, u32 *alg_ptr);
bool parse_block_size_option(const char *arg, u32 *size_ptr);
bool parse_salt_option(const char *arg, u8 **salt_ptr, u32 *salt_size_ptr);
u32 get_default_block_size(void);
#endif /* PROGRAMS_FSVERITY_H */
+210
View File
@@ -0,0 +1,210 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Utility functions for the 'fsverity' program
*
* Copyright 2018 Google LLC
*/
#include "utils.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <unistd.h>
/* ========== Memory allocation ========== */
void *xmalloc(size_t size)
{
void *p = malloc(size);
if (!p)
fatal_error("out of memory");
return p;
}
void *xzalloc(size_t size)
{
return memset(xmalloc(size), 0, size);
}
void *xmemdup(const void *mem, size_t size)
{
return memcpy(xmalloc(size), mem, size);
}
char *xstrdup(const char *s)
{
return xmemdup(s, strlen(s) + 1);
}
/* ========== Error messages and assertions ========== */
static void do_error_msg(const char *format, va_list va, int err)
{
fputs("ERROR: ", stderr);
vfprintf(stderr, format, va);
if (err)
fprintf(stderr, ": %s", strerror(err));
putc('\n', stderr);
}
void error_msg(const char *format, ...)
{
va_list va;
va_start(va, format);
do_error_msg(format, va, 0);
va_end(va);
}
void error_msg_errno(const char *format, ...)
{
va_list va;
va_start(va, format);
do_error_msg(format, va, errno);
va_end(va);
}
__noreturn void fatal_error(const char *format, ...)
{
va_list va;
va_start(va, format);
do_error_msg(format, va, 0);
va_end(va);
abort();
}
__noreturn void assertion_failed(const char *expr, const char *file, int line)
{
fatal_error("Assertion failed: %s at %s:%d", expr, file, line);
}
/* ========== File utilities ========== */
bool open_file(struct filedes *file, const char *filename, int flags, int mode)
{
file->fd = open(filename, flags, mode);
if (file->fd < 0) {
error_msg_errno("can't open '%s' for %s", filename,
(flags & O_ACCMODE) == O_RDONLY ? "reading" :
(flags & O_ACCMODE) == O_WRONLY ? "writing" :
"reading and writing");
return false;
}
file->name = xstrdup(filename);
return true;
}
bool get_file_size(struct filedes *file, u64 *size_ret)
{
struct stat stbuf;
if (fstat(file->fd, &stbuf) != 0) {
error_msg_errno("can't stat file '%s'", file->name);
return false;
}
*size_ret = stbuf.st_size;
return true;
}
bool full_read(struct filedes *file, void *buf, size_t count)
{
while (count) {
int n = read(file->fd, buf, min(count, INT_MAX));
if (n < 0) {
error_msg_errno("reading from '%s'", file->name);
return false;
}
if (n == 0) {
error_msg("unexpected end-of-file on '%s'", file->name);
return false;
}
buf += n;
count -= n;
}
return true;
}
bool full_write(struct filedes *file, const void *buf, size_t count)
{
while (count) {
int n = write(file->fd, buf, min(count, INT_MAX));
if (n < 0) {
error_msg_errno("writing to '%s'", file->name);
return false;
}
buf += n;
count -= n;
}
return true;
}
bool filedes_close(struct filedes *file)
{
int res;
if (file->fd < 0)
return true;
res = close(file->fd);
if (res != 0)
error_msg_errno("closing '%s'", file->name);
file->fd = -1;
free(file->name);
file->name = NULL;
return res == 0;
}
/* ========== String utilities ========== */
static int hex2bin_char(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return 10 + (c - 'a');
if (c >= 'A' && c <= 'F')
return 10 + (c - 'A');
return -1;
}
bool hex2bin(const char *hex, u8 *bin, size_t bin_len)
{
if (strlen(hex) != 2 * bin_len)
return false;
while (bin_len--) {
int hi = hex2bin_char(*hex++);
int lo = hex2bin_char(*hex++);
if (hi < 0 || lo < 0)
return false;
*bin++ = (hi << 4) | lo;
}
return true;
}
static char bin2hex_char(u8 nibble)
{
ASSERT(nibble <= 0xf);
if (nibble < 10)
return '0' + nibble;
return 'a' + (nibble - 10);
}
void bin2hex(const u8 *bin, size_t bin_len, char *hex)
{
while (bin_len--) {
*hex++ = bin2hex_char(*bin >> 4);
*hex++ = bin2hex_char(*bin & 0xf);
bin++;
}
*hex = '\0';
}
+44
View File
@@ -0,0 +1,44 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Utility functions for programs
*
* Copyright 2018 Google LLC
*/
#ifndef PROGRAMS_UTILS_H
#define PROGRAMS_UTILS_H
#include "../common/libfsverity.h"
#include "../common/common_defs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void *xmalloc(size_t size);
void *xzalloc(size_t size);
void *xmemdup(const void *mem, size_t size);
char *xstrdup(const char *s);
__printf(1, 2) __cold void error_msg(const char *format, ...);
__printf(1, 2) __cold void error_msg_errno(const char *format, ...);
__printf(1, 2) __cold __noreturn void fatal_error(const char *format, ...);
__cold __noreturn void assertion_failed(const char *expr,
const char *file, int line);
#define ASSERT(e) ({ if (!(e)) assertion_failed(#e, __FILE__, __LINE__); })
struct filedes {
int fd;
char *name; /* filename, for logging or error messages */
};
bool open_file(struct filedes *file, const char *filename, int flags, int mode);
bool get_file_size(struct filedes *file, u64 *size_ret);
bool full_read(struct filedes *file, void *buf, size_t count);
bool full_write(struct filedes *file, const void *buf, size_t count);
bool filedes_close(struct filedes *file);
bool hex2bin(const char *hex, u8 *bin, size_t bin_len);
void bin2hex(const u8 *bin, size_t bin_len, char *hex);
#endif /* PROGRAMS_UTILS_H */