mirror of
https://github.com/systemd/systemd.git
synced 2025-08-26 17:49:52 +03:00
bootspec: process multi-profile UKIs
This commit is contained in:
@ -23,6 +23,7 @@
|
||||
#include "string-table.h"
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
#include "uki.h"
|
||||
#include "unaligned.h"
|
||||
|
||||
static const char* const boot_entry_type_table[_BOOT_ENTRY_TYPE_MAX] = {
|
||||
@ -48,6 +49,7 @@ static void boot_entry_free(BootEntry *entry) {
|
||||
|
||||
free(entry->id);
|
||||
free(entry->id_old);
|
||||
free(entry->id_without_profile);
|
||||
free(entry->path);
|
||||
free(entry->root);
|
||||
free(entry->title);
|
||||
@ -529,10 +531,18 @@ static int boot_entry_compare(const BootEntry *a, const BootEntry *b) {
|
||||
return r;
|
||||
}
|
||||
|
||||
r = -strverscmp_improved(a->id, b->id);
|
||||
r = -strverscmp_improved(a->id_without_profile ?: a->id, b->id_without_profile ?: b->id);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (a->id_without_profile && b->id_without_profile) {
|
||||
/* The strverscmp_improved() call above already established that we are talking about the
|
||||
* same image here, hence order by profile, if there is one */
|
||||
r = CMP(a->profile, b->profile);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (a->tries_left != UINT_MAX || b->tries_left != UINT_MAX)
|
||||
return 0;
|
||||
|
||||
@ -636,28 +646,30 @@ static int boot_entries_find_type1(
|
||||
static int boot_entry_load_unified(
|
||||
const char *root,
|
||||
const char *path,
|
||||
const char *osrelease,
|
||||
const char *cmdline,
|
||||
unsigned profile,
|
||||
const char *osrelease_text,
|
||||
const char *profile_text,
|
||||
const char *cmdline_text,
|
||||
BootEntry *ret) {
|
||||
|
||||
_cleanup_free_ char *fname = NULL, *os_pretty_name = NULL, *os_image_id = NULL, *os_name = NULL, *os_id = NULL,
|
||||
*os_image_version = NULL, *os_version = NULL, *os_version_id = NULL, *os_build_id = NULL;
|
||||
_cleanup_(boot_entry_free) BootEntry tmp = BOOT_ENTRY_INIT(BOOT_ENTRY_UNIFIED);
|
||||
const char *k, *good_name, *good_version, *good_sort_key;
|
||||
_cleanup_fclose_ FILE *f = NULL;
|
||||
int r;
|
||||
|
||||
assert(root);
|
||||
assert(path);
|
||||
assert(osrelease);
|
||||
assert(osrelease_text);
|
||||
assert(ret);
|
||||
|
||||
k = path_startswith(path, root);
|
||||
if (!k)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path is not below root: %s", path);
|
||||
|
||||
f = fmemopen_unlocked((void*) osrelease, strlen(osrelease), "r");
|
||||
f = fmemopen_unlocked((void*) osrelease_text, strlen(osrelease_text), "r");
|
||||
if (!f)
|
||||
return log_error_errno(errno, "Failed to open os-release buffer: %m");
|
||||
return log_oom();
|
||||
|
||||
r = parse_env_file(f, "os-release",
|
||||
"PRETTY_NAME", &os_pretty_name,
|
||||
@ -685,10 +697,28 @@ static int boot_entry_load_unified(
|
||||
&good_sort_key))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Missing fields in os-release data from unified kernel image %s, refusing.", path);
|
||||
|
||||
_cleanup_free_ char *profile_id = NULL, *profile_title = NULL;
|
||||
if (profile_text) {
|
||||
fclose(f);
|
||||
|
||||
f = fmemopen_unlocked((void*) profile_text, strlen(profile_text), "r");
|
||||
if (!f)
|
||||
return log_oom();
|
||||
|
||||
r = parse_env_file(
|
||||
f, "profile",
|
||||
"ID", &profile_id,
|
||||
"TITLE", &profile_title);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to parse profile data from unified kernel image '%s': %m", path);
|
||||
}
|
||||
|
||||
r = path_extract_filename(path, &fname);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract file name from '%s': %m", path);
|
||||
|
||||
_cleanup_(boot_entry_free) BootEntry tmp = BOOT_ENTRY_INIT(BOOT_ENTRY_UNIFIED);
|
||||
|
||||
r = boot_filename_extract_tries(fname, &tmp.id, &tmp.tries_left, &tmp.tries_done);
|
||||
if (r < 0)
|
||||
return r;
|
||||
@ -696,6 +726,19 @@ static int boot_entry_load_unified(
|
||||
if (!efi_loader_entry_name_valid(tmp.id))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid loader entry name: %s", tmp.id);
|
||||
|
||||
tmp.profile = profile;
|
||||
|
||||
if (profile_id || profile > 0) {
|
||||
tmp.id_without_profile = TAKE_PTR(tmp.id);
|
||||
|
||||
if (profile_id)
|
||||
tmp.id = strjoin(tmp.id_without_profile, "@", profile_id);
|
||||
else
|
||||
(void) asprintf(&tmp.id, "%s@%u", tmp.id_without_profile, profile);
|
||||
if (!tmp.id)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
if (os_id && os_version_id) {
|
||||
tmp.id_old = strjoin(os_id, "-", os_version_id);
|
||||
if (!tmp.id_old)
|
||||
@ -714,13 +757,18 @@ static int boot_entry_load_unified(
|
||||
if (!tmp.kernel)
|
||||
return log_oom();
|
||||
|
||||
tmp.options = strv_new(skip_leading_chars(cmdline, WHITESPACE));
|
||||
tmp.options = strv_new(cmdline_text);
|
||||
if (!tmp.options)
|
||||
return log_oom();
|
||||
|
||||
delete_trailing_chars(tmp.options[0], WHITESPACE);
|
||||
|
||||
tmp.title = strdup(good_name);
|
||||
if (profile_title)
|
||||
tmp.title = strjoin(good_name, " (", profile_title, ")");
|
||||
else if (profile_id)
|
||||
tmp.title = strjoin(good_name, " (", profile_id, ")");
|
||||
else if (profile > 0)
|
||||
(void) asprintf(&tmp.title, "%s (@%u)", good_name, profile);
|
||||
else
|
||||
tmp.title = strdup(good_name);
|
||||
if (!tmp.title)
|
||||
return log_oom();
|
||||
|
||||
@ -740,11 +788,7 @@ static int boot_entry_load_unified(
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Maximum PE section we are willing to load (Note that sections we are not interested in may be larger, but
|
||||
* the ones we do care about and we are willing to load into memory have this size limit.) */
|
||||
#define PE_SECTION_SIZE_MAX (4U*1024U*1024U)
|
||||
|
||||
static int find_sections(
|
||||
static int pe_load_headers_and_sections(
|
||||
int fd,
|
||||
const char *path,
|
||||
IMAGE_SECTION_HEADER **ret_sections,
|
||||
@ -774,92 +818,174 @@ static int find_sections(
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int find_cmdline_section(
|
||||
int fd,
|
||||
const char *path,
|
||||
IMAGE_SECTION_HEADER *sections,
|
||||
PeHeader *pe_header,
|
||||
char **ret_cmdline) {
|
||||
static const IMAGE_SECTION_HEADER* pe_find_profile_section_table(
|
||||
const PeHeader *pe_header,
|
||||
const IMAGE_SECTION_HEADER *sections,
|
||||
unsigned profile,
|
||||
size_t *ret_n_sections) {
|
||||
|
||||
int r;
|
||||
char *cmdline = NULL, *t = NULL;
|
||||
_cleanup_free_ char *word = NULL;
|
||||
assert(pe_header);
|
||||
|
||||
assert(path);
|
||||
/* Looks for the part of the section table that defines the specified profile. If 'profile' is
|
||||
* specified as UINT_MAX this will look for the base profile. */
|
||||
|
||||
if (!ret_cmdline)
|
||||
if (le16toh(pe_header->pe.NumberOfSections) == 0)
|
||||
return NULL;
|
||||
|
||||
assert(sections);
|
||||
|
||||
const IMAGE_SECTION_HEADER
|
||||
*p = sections,
|
||||
*e = sections + le16toh(pe_header->pe.NumberOfSections),
|
||||
*start = profile == UINT_MAX ? sections : NULL,
|
||||
*end;
|
||||
unsigned current_profile = UINT_MAX;
|
||||
|
||||
for (;;) {
|
||||
p = pe_section_table_find(p, e - p, ".profile");
|
||||
if (!p) {
|
||||
end = e;
|
||||
break;
|
||||
}
|
||||
if (current_profile == profile) {
|
||||
end = p;
|
||||
break;
|
||||
}
|
||||
|
||||
if (current_profile == UINT_MAX)
|
||||
current_profile = 0;
|
||||
else
|
||||
current_profile++;
|
||||
|
||||
if (current_profile == profile)
|
||||
start = p;
|
||||
|
||||
p++; /* Continue scanning after the .profile entry we just found */
|
||||
}
|
||||
|
||||
if (!start)
|
||||
return NULL;
|
||||
|
||||
if (ret_n_sections)
|
||||
*ret_n_sections = end - start;
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
static int trim_cmdline(char **cmdline) {
|
||||
assert(cmdline);
|
||||
|
||||
/* Strips leading and trailing whitespace from command line */
|
||||
|
||||
if (!*cmdline)
|
||||
return 0;
|
||||
|
||||
r = pe_read_section_data_by_name(fd, pe_header, sections, ".cmdline", PE_SECTION_SIZE_MAX, (void**) &cmdline, NULL);
|
||||
if (r == -ENXIO) { /* cmdline is optional */
|
||||
*ret_cmdline = NULL;
|
||||
const char *skipped = skip_leading_chars(*cmdline, WHITESPACE);
|
||||
|
||||
if (isempty(skipped)) {
|
||||
*cmdline = mfree(*cmdline);
|
||||
return 0;
|
||||
}
|
||||
if (r < 0)
|
||||
return log_warning_errno(r, "Failed to read .cmdline section of '%s': %m", path);
|
||||
|
||||
word = strdup(cmdline);
|
||||
if (!word)
|
||||
return log_oom();
|
||||
if (skipped != *cmdline) {
|
||||
_cleanup_free_ char *c = strdup(skipped);
|
||||
if (!c)
|
||||
return -ENOMEM;
|
||||
|
||||
/* Quick test to check if there is actual content in the addon cmdline */
|
||||
t = delete_chars(word, NULL);
|
||||
if (isempty(t))
|
||||
*ret_cmdline = NULL;
|
||||
else
|
||||
*ret_cmdline = TAKE_PTR(cmdline);
|
||||
free_and_replace(*cmdline, c);
|
||||
}
|
||||
|
||||
return 0;
|
||||
delete_trailing_chars(*cmdline, WHITESPACE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int find_osrel_section(
|
||||
int fd,
|
||||
const char *path,
|
||||
IMAGE_SECTION_HEADER *sections,
|
||||
PeHeader *pe_header,
|
||||
char **ret_osrelease) {
|
||||
|
||||
int r;
|
||||
|
||||
if (!ret_osrelease)
|
||||
return 0;
|
||||
|
||||
r = pe_read_section_data_by_name(fd, pe_header, sections, ".osrel", PE_SECTION_SIZE_MAX, (void**) ret_osrelease, NULL);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to read .osrel section of '%s': %m", path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int find_uki_sections(
|
||||
/* Maximum PE section we are willing to load (Note that sections we are not interested in may be larger, but
|
||||
* the ones we do care about and we are willing to load into memory have this size limit.) */
|
||||
#define PE_SECTION_SIZE_MAX (4U*1024U*1024U)
|
||||
|
||||
static int pe_find_uki_sections(
|
||||
int fd,
|
||||
const char *path,
|
||||
unsigned profile,
|
||||
char **ret_osrelease,
|
||||
char **ret_profile,
|
||||
char **ret_cmdline) {
|
||||
|
||||
_cleanup_free_ char *osrelease_text = NULL, *profile_text = NULL, *cmdline_text = NULL;
|
||||
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
|
||||
_cleanup_free_ PeHeader *pe_header = NULL;
|
||||
int r;
|
||||
|
||||
r = find_sections(fd, path, §ions, &pe_header);
|
||||
assert(fd >= 0);
|
||||
assert(path);
|
||||
assert(profile != UINT_MAX);
|
||||
assert(ret_osrelease);
|
||||
assert(ret_profile);
|
||||
assert(ret_cmdline);
|
||||
|
||||
r = pe_load_headers_and_sections(fd, path, §ions, &pe_header);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!pe_is_uki(pe_header, sections))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Parsed PE file '%s' is not a UKI.", path);
|
||||
|
||||
r = find_osrel_section(fd, path, sections, pe_header, ret_osrelease);
|
||||
if (r < 0)
|
||||
return r;
|
||||
/* Find part of the section table for this profile */
|
||||
size_t n_psections = 0;
|
||||
const IMAGE_SECTION_HEADER *psections = pe_find_profile_section_table(pe_header, sections, profile, &n_psections);
|
||||
if (!psections && profile != 0) /* Profile not found? (Profile @0 needs no explicit .profile!) */
|
||||
goto nothing;
|
||||
|
||||
r = find_cmdline_section(fd, path, sections, pe_header, ret_cmdline);
|
||||
if (r < 0)
|
||||
return r;
|
||||
/* Find base profile part of section table */
|
||||
size_t n_bsections;
|
||||
const IMAGE_SECTION_HEADER *bsections = ASSERT_PTR(pe_find_profile_section_table(pe_header, sections, UINT_MAX, &n_bsections));
|
||||
|
||||
struct {
|
||||
const char *name;
|
||||
char **data;
|
||||
} table[] = {
|
||||
{ ".osrel", &osrelease_text },
|
||||
{ ".profile", &profile_text },
|
||||
{ ".cmdline", &cmdline_text },
|
||||
};
|
||||
|
||||
FOREACH_ELEMENT(t, table) {
|
||||
const IMAGE_SECTION_HEADER *found;
|
||||
|
||||
/* First look in the profile part of the section table, and if we don't find anything there, look into the base part */
|
||||
found = pe_section_table_find(psections, n_psections, t->name);
|
||||
if (!found) {
|
||||
found = pe_section_table_find(bsections, n_bsections, t->name);
|
||||
if (!found)
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Permit "masking" of sections in the base profile */
|
||||
if (found->VirtualSize == 0)
|
||||
continue;
|
||||
|
||||
r = pe_read_section_data(fd, found, PE_SECTION_SIZE_MAX, (void**) t->data, /* ret_data= */ NULL);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to load contents of section '%s': %m", t->name);
|
||||
}
|
||||
|
||||
if (!osrelease_text)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unified kernel image lacks .osrel data for profile @%u, refusing.", profile);
|
||||
|
||||
if (trim_cmdline(&cmdline_text) < 0)
|
||||
return log_oom();
|
||||
|
||||
*ret_osrelease = TAKE_PTR(osrelease_text);
|
||||
*ret_profile = TAKE_PTR(profile_text);
|
||||
*ret_cmdline = TAKE_PTR(cmdline_text);
|
||||
return 1;
|
||||
|
||||
nothing:
|
||||
*ret_osrelease = *ret_profile = *ret_cmdline = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int find_addon_sections(
|
||||
static int pe_find_addon_sections(
|
||||
int fd,
|
||||
const char *path,
|
||||
char **ret_cmdline) {
|
||||
@ -868,19 +994,36 @@ static int find_addon_sections(
|
||||
_cleanup_free_ PeHeader *pe_header = NULL;
|
||||
int r;
|
||||
|
||||
r = find_sections(fd, path, §ions, &pe_header);
|
||||
assert(fd >= 0);
|
||||
assert(path);
|
||||
|
||||
r = pe_load_headers_and_sections(fd, path, §ions, &pe_header);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = find_cmdline_section(fd, path, sections, pe_header, ret_cmdline);
|
||||
/* If addon cmdline is empty or contains just separators,
|
||||
* don't bother tracking it.
|
||||
* Don't check r because it cannot return <0 if cmdline is empty,
|
||||
* as cmdline is always optional. */
|
||||
if (!ret_cmdline)
|
||||
return log_warning_errno(SYNTHETIC_ERRNO(ENOENT), "Addon %s contains empty cmdline and will be therefore ignored.", path);
|
||||
if (!pe_is_addon(pe_header, sections))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Parse PE file '%s' is not an add-on.", path);
|
||||
|
||||
return r;
|
||||
/* Define early, before the goto below */
|
||||
_cleanup_free_ char *cmdline_text = NULL;
|
||||
|
||||
const IMAGE_SECTION_HEADER *found = pe_section_table_find(sections, le16toh(pe_header->pe.NumberOfSections), ".cmdline");
|
||||
if (!found)
|
||||
goto nothing;
|
||||
|
||||
r = pe_read_section_data(fd, found, PE_SECTION_SIZE_MAX, (void**) &cmdline_text, /* ret_size= */ NULL);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to load contents of section '.cmdline': %m");
|
||||
|
||||
if (trim_cmdline(&cmdline_text) < 0)
|
||||
return log_oom();
|
||||
|
||||
*ret_cmdline = TAKE_PTR(cmdline_text);
|
||||
return 1;
|
||||
|
||||
nothing:
|
||||
*ret_cmdline = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int insert_boot_entry_addon(
|
||||
@ -959,7 +1102,7 @@ static int boot_entries_find_unified_addons(
|
||||
if (!j)
|
||||
return log_oom();
|
||||
|
||||
if (find_addon_sections(fd, j, &cmdline) < 0)
|
||||
if (pe_find_addon_sections(fd, j, &cmdline) <= 0)
|
||||
continue;
|
||||
|
||||
location = strdup(j);
|
||||
@ -1032,19 +1175,13 @@ static int boot_entries_find_unified(
|
||||
return log_error_errno(r, "Failed to open '%s/%s': %m", root, dir);
|
||||
|
||||
FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read %s: %m", full)) {
|
||||
_cleanup_free_ char *j = NULL, *osrelease = NULL, *cmdline = NULL;
|
||||
_cleanup_close_ int fd = -EBADF;
|
||||
|
||||
if (!dirent_is_file(de))
|
||||
continue;
|
||||
|
||||
if (!endswith_no_case(de->d_name, ".efi"))
|
||||
continue;
|
||||
|
||||
if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
|
||||
return log_oom();
|
||||
|
||||
fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOFOLLOW|O_NOCTTY);
|
||||
_cleanup_close_ int fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOFOLLOW|O_NOCTTY);
|
||||
if (fd < 0) {
|
||||
log_warning_errno(errno, "Failed to open %s/%s, ignoring: %m", full, de->d_name);
|
||||
continue;
|
||||
@ -1056,23 +1193,30 @@ static int boot_entries_find_unified(
|
||||
if (r == 0) /* inode already seen or otherwise not relevant */
|
||||
continue;
|
||||
|
||||
j = path_join(full, de->d_name);
|
||||
_cleanup_free_ char *j = path_join(full, de->d_name);
|
||||
if (!j)
|
||||
return log_oom();
|
||||
|
||||
if (find_uki_sections(fd, j, &osrelease, &cmdline) < 0)
|
||||
continue;
|
||||
for (unsigned p = 0; p < UNIFIED_PROFILES_MAX; p++) {
|
||||
_cleanup_free_ char *osrelease = NULL, *profile = NULL, *cmdline = NULL;
|
||||
|
||||
r = boot_entry_load_unified(root, j, osrelease, cmdline, config->entries + config->n_entries);
|
||||
if (r < 0)
|
||||
continue;
|
||||
r = pe_find_uki_sections(fd, j, p, &osrelease, &profile, &cmdline);
|
||||
if (r == 0) /* this profile does not exist, we are done */
|
||||
break;
|
||||
if (r < 0)
|
||||
continue;
|
||||
|
||||
/* look for .efi.extra.d */
|
||||
r = boot_entries_find_unified_local_addons(config, dirfd(d), de->d_name, full, config->entries + config->n_entries);
|
||||
if (r < 0)
|
||||
continue;
|
||||
if (!GREEDY_REALLOC0(config->entries, config->n_entries + 2))
|
||||
return log_oom();
|
||||
|
||||
config->n_entries++;
|
||||
if (boot_entry_load_unified(root, j, p, osrelease, profile, cmdline, config->entries + config->n_entries) < 0)
|
||||
continue;
|
||||
|
||||
config->n_entries++;
|
||||
|
||||
/* look for .efi.extra.d */
|
||||
(void) boot_entries_find_unified_local_addons(config, dirfd(d), de->d_name, full, config->entries + config->n_entries);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
@ -1648,8 +1792,14 @@ int show_boot_entry(
|
||||
|
||||
putchar('\n');
|
||||
|
||||
if (e->id)
|
||||
printf(" id: %s\n", e->id);
|
||||
if (e->id) {
|
||||
printf(" id: %s", e->id);
|
||||
|
||||
if (e->id_without_profile && !streq_ptr(e->id, e->id_without_profile))
|
||||
printf(" (without profile: %s)\n", e->id_without_profile);
|
||||
else
|
||||
putchar('\n');
|
||||
}
|
||||
if (e->path) {
|
||||
_cleanup_free_ char *text = NULL, *link = NULL;
|
||||
|
||||
@ -1673,7 +1823,7 @@ int show_boot_entry(
|
||||
if (e->tries_done != UINT_MAX)
|
||||
printf("; %u done\n", e->tries_done);
|
||||
else
|
||||
printf("\n");
|
||||
putchar('\n');
|
||||
}
|
||||
|
||||
if (e->sort_key)
|
||||
|
@ -38,6 +38,7 @@ typedef struct BootEntry {
|
||||
bool reported_by_loader;
|
||||
char *id; /* This is the file basename (including extension!) */
|
||||
char *id_old; /* Old-style ID, for deduplication purposes. */
|
||||
char *id_without_profile; /* id without profile suffixed */
|
||||
char *path; /* This is the full path to the drop-in file */
|
||||
char *root; /* The root path in which the drop-in was found, i.e. to which 'kernel', 'efi' and 'initrd' are relative */
|
||||
char *title;
|
||||
@ -55,6 +56,7 @@ typedef struct BootEntry {
|
||||
char **device_tree_overlay;
|
||||
unsigned tries_left;
|
||||
unsigned tries_done;
|
||||
unsigned profile;
|
||||
} BootEntry;
|
||||
|
||||
#define BOOT_ENTRY_INIT(t) \
|
||||
|
Reference in New Issue
Block a user