KP Singh <kpsingh@xxxxxxxxxx> writes: > Two modes of operation being added: > > Add two modes of operation: > > * For prog load, allow signing a program immediately before loading. This > is essential for command-line testing and administration. > > bpftool prog load -S -k <private_key> -i <identity_cert> fentry_test.bpf.o > > * For gen skeleton, embed a pre-generated signature into the C skeleton > file. This supports the use of signed programs in compiled applications. > > bpftool gen skeleton -S -k <private_key> -i <identity_cert> fentry_test.bpf.o > > Generation of the loader program and its metadata map is implemented in > libbpf (bpf_obj__gen_loader). bpftool generates a skeleton that loads > the program and automates the required steps: freezing the map, creating > an exclusive map, loading, and running. Users can use standard libbpf > APIs directly or integrate loader program generation into their own > toolchains. > > Signed-off-by: KP Singh <kpsingh@xxxxxxxxxx> > --- > .../bpf/bpftool/Documentation/bpftool-gen.rst | 16 +- > .../bpftool/Documentation/bpftool-prog.rst | 18 +- > tools/bpf/bpftool/Makefile | 6 +- > tools/bpf/bpftool/cgroup.c | 4 + > tools/bpf/bpftool/gen.c | 60 ++++- > tools/bpf/bpftool/main.c | 26 ++- > tools/bpf/bpftool/main.h | 11 + > tools/bpf/bpftool/prog.c | 27 ++- > tools/bpf/bpftool/sign.c | 212 ++++++++++++++++++ > 9 files changed, 367 insertions(+), 13 deletions(-) > create mode 100644 tools/bpf/bpftool/sign.c > > diff --git a/tools/bpf/bpftool/Documentation/bpftool-gen.rst b/tools/bpf/bpftool/Documentation/bpftool-gen.rst > index ca860fd97d8d..cef469d758ed 100644 > --- a/tools/bpf/bpftool/Documentation/bpftool-gen.rst > +++ b/tools/bpf/bpftool/Documentation/bpftool-gen.rst > @@ -16,7 +16,8 @@ SYNOPSIS > > **bpftool** [*OPTIONS*] **gen** *COMMAND* > > -*OPTIONS* := { |COMMON_OPTIONS| | { **-L** | **--use-loader** } } > +*OPTIONS* := { |COMMON_OPTIONS| [ { **-L** | **--use-loader** } ] > +[ { { **-S** | **--sign** } **-k** <private_key.pem> **-i** <certificate.x509> } ] }} > > *COMMAND* := { **object** | **skeleton** | **help** } > > @@ -186,6 +187,19 @@ OPTIONS > skeleton). A light skeleton contains a loader eBPF program. It does not use > the majority of the libbpf infrastructure, and does not need libelf. > > +-S, --sign > + For skeletons, generate a signed skeleton. This option must be used with > + **-k** and **-i**. Using this flag implicitly enables **--use-loader**. > + See the "Signed Skeletons" section in the description of the > + **gen skeleton** command for more details. > + > +-k <private_key.pem> > + Path to the private key file in PEM format, required for signing. > + > +-i <certificate.x509> > + Path to the X.509 certificate file in PEM or DER format, required for > + signing. > + > EXAMPLES > ======== > **$ cat example1.bpf.c** > diff --git a/tools/bpf/bpftool/Documentation/bpftool-prog.rst b/tools/bpf/bpftool/Documentation/bpftool-prog.rst > index f69fd92df8d8..55b812761df2 100644 > --- a/tools/bpf/bpftool/Documentation/bpftool-prog.rst > +++ b/tools/bpf/bpftool/Documentation/bpftool-prog.rst > @@ -16,9 +16,9 @@ SYNOPSIS > > **bpftool** [*OPTIONS*] **prog** *COMMAND* > > -*OPTIONS* := { |COMMON_OPTIONS| | > -{ **-f** | **--bpffs** } | { **-m** | **--mapcompat** } | { **-n** | **--nomount** } | > -{ **-L** | **--use-loader** } } > +*OPTIONS* := { |COMMON_OPTIONS| [ { **-f** | **--bpffs** } ] [ { **-m** | **--mapcompat** } ] > +[ { **-n** | **--nomount** } ] [ { **-L** | **--use-loader** } ] > +[ { { **-S** | **--sign** } **-k** <private_key.pem> **-i** <certificate.x509> } ] } > > *COMMANDS* := > { **show** | **list** | **dump xlated** | **dump jited** | **pin** | **load** | > @@ -248,6 +248,18 @@ OPTIONS > creating the maps, and loading the programs (see **bpftool prog tracelog** > as a way to dump those messages). > > +-S, --sign > + Enable signing of the BPF program before loading. This option must be > + used with **-k** and **-i**. Using this flag implicitly enables > + **--use-loader**. > + > +-k <private_key.pem> > + Path to the private key file in PEM format, required when signing. > + > +-i <certificate.x509> > + Path to the X.509 certificate file in PEM or DER format, required when > + signing. > + > EXAMPLES > ======== > **# bpftool prog show** > diff --git a/tools/bpf/bpftool/Makefile b/tools/bpf/bpftool/Makefile > index 9e9a5f006cd2..586d1b2595d1 100644 > --- a/tools/bpf/bpftool/Makefile > +++ b/tools/bpf/bpftool/Makefile > @@ -130,8 +130,8 @@ include $(FEATURES_DUMP) > endif > endif > > -LIBS = $(LIBBPF) -lelf -lz > -LIBS_BOOTSTRAP = $(LIBBPF_BOOTSTRAP) -lelf -lz > +LIBS = $(LIBBPF) -lelf -lz -lcrypto > +LIBS_BOOTSTRAP = $(LIBBPF_BOOTSTRAP) -lelf -lz -lcrypto > > ifeq ($(feature-libelf-zstd),1) > LIBS += -lzstd > @@ -194,7 +194,7 @@ endif > > BPFTOOL_BOOTSTRAP := $(BOOTSTRAP_OUTPUT)bpftool > > -BOOTSTRAP_OBJS = $(addprefix $(BOOTSTRAP_OUTPUT),main.o common.o json_writer.o gen.o btf.o) > +BOOTSTRAP_OBJS = $(addprefix $(BOOTSTRAP_OUTPUT),main.o common.o json_writer.o gen.o btf.o sign.o) > $(BOOTSTRAP_OBJS): $(LIBBPF_BOOTSTRAP) > > OBJS = $(patsubst %.c,$(OUTPUT)%.o,$(SRCS)) $(OUTPUT)disasm.o > diff --git a/tools/bpf/bpftool/cgroup.c b/tools/bpf/bpftool/cgroup.c > index 944ebe21a216..ec356deb27c9 100644 > --- a/tools/bpf/bpftool/cgroup.c > +++ b/tools/bpf/bpftool/cgroup.c > @@ -2,6 +2,10 @@ > // Copyright (C) 2017 Facebook > // Author: Roman Gushchin <guro@xxxxxx> > > +#undef GCC_VERSION > +#ifndef _GNU_SOURCE > +#define _GNU_SOURCE > +#endif > #define _XOPEN_SOURCE 500 > #include <errno.h> > #include <fcntl.h> > diff --git a/tools/bpf/bpftool/gen.c b/tools/bpf/bpftool/gen.c > index 67a60114368f..427468c9e9c2 100644 > --- a/tools/bpf/bpftool/gen.c > +++ b/tools/bpf/bpftool/gen.c > @@ -688,10 +688,17 @@ static void codegen_destroy(struct bpf_object *obj, const char *obj_name) > static int gen_trace(struct bpf_object *obj, const char *obj_name, const char *header_guard) > { > DECLARE_LIBBPF_OPTS(gen_loader_opts, opts); > + struct bpf_load_and_run_opts sopts = {}; > + char sig_buf[MAX_SIG_SIZE]; > + __u8 prog_sha[SHA256_DIGEST_LENGTH]; > struct bpf_map *map; > + > char ident[256]; > int err = 0; > > + if (sign_progs) > + opts.gen_hash = true; > + > err = bpf_object__gen_loader(obj, &opts); > if (err) > return err; > @@ -701,6 +708,7 @@ static int gen_trace(struct bpf_object *obj, const char *obj_name, const char *h > p_err("failed to load object file"); > goto out; > } > + > /* If there was no error during load then gen_loader_opts > * are populated with the loader program. > */ > @@ -780,8 +788,51 @@ static int gen_trace(struct bpf_object *obj, const char *obj_name, const char *h > print_hex(opts.insns, opts.insns_sz); > codegen("\ > \n\ > - \"; \n\ > - \n\ > + \";\n"); > + > + if (sign_progs) { > + sopts.insns = opts.insns; > + sopts.insns_sz = opts.insns_sz; > + sopts.excl_prog_hash = prog_sha; > + sopts.excl_prog_hash_sz = sizeof(prog_sha); > + sopts.signature = sig_buf; > + sopts.signature_sz = MAX_SIG_SIZE; > + sopts.keyring_id = KEY_SPEC_SESSION_KEYRING; > + This still has the session keyring hardcoded. > + err = bpftool_prog_sign(&sopts); > + if (err < 0) > + return err; > + > + codegen("\ > + \n\ > + static const char opts_sig[] __attribute__((__aligned__(8))) = \"\\\n\ > + "); > + print_hex((const void *)sig_buf, sopts.signature_sz); > + codegen("\ > + \n\ > + \";\n"); > + > + codegen("\ > + \n\ > + static const char opts_excl_hash[] __attribute__((__aligned__(8))) = \"\\\n\ > + "); > + print_hex((const void *)prog_sha, sizeof(prog_sha)); > + codegen("\ > + \n\ > + \";\n"); > + > + codegen("\ > + \n\ > + opts.signature = (void *)opts_sig; \n\ > + opts.signature_sz = sizeof(opts_sig) - 1; \n\ > + opts.excl_prog_hash = (void *)opts_excl_hash; \n\ > + opts.excl_prog_hash_sz = sizeof(opts_excl_hash) - 1; \n\ > + opts.keyring_id = KEY_SPEC_SESSION_KEYRING; \n\ > + "); And here. > + } > + > + codegen("\ > + \n\ > opts.ctx = (struct bpf_loader_ctx *)skel; \n\ > opts.data_sz = sizeof(opts_data) - 1; \n\ > opts.data = (void *)opts_data; \n\ > @@ -1240,7 +1291,7 @@ static int do_skeleton(int argc, char **argv) > err = -errno; > libbpf_strerror(err, err_buf, sizeof(err_buf)); > p_err("failed to open BPF object file: %s", err_buf); > - goto out; > + goto out_obj; > } > > bpf_object__for_each_map(map, obj) { > @@ -1552,6 +1603,7 @@ static int do_skeleton(int argc, char **argv) > err = 0; > out: > bpf_object__close(obj); > +out_obj: > if (obj_data) > munmap(obj_data, mmap_sz); > close(fd); > @@ -1930,7 +1982,7 @@ static int do_help(int argc, char **argv) > " %1$s %2$s help\n" > "\n" > " " HELP_SPEC_OPTIONS " |\n" > - " {-L|--use-loader} }\n" > + " {-L|--use-loader} | [ {-S|--sign } {-k} <private_key.pem> {-i} <certificate.x509> ]}\n" > "", > bin_name, "gen"); > > diff --git a/tools/bpf/bpftool/main.c b/tools/bpf/bpftool/main.c > index 0f1183b2ed0a..c78eb80b9c94 100644 > --- a/tools/bpf/bpftool/main.c > +++ b/tools/bpf/bpftool/main.c > @@ -33,6 +33,9 @@ bool relaxed_maps; > bool use_loader; > struct btf *base_btf; > struct hashmap *refs_table; > +bool sign_progs; > +const char *private_key_path; > +const char *cert_path; > > static void __noreturn clean_and_exit(int i) > { > @@ -448,6 +451,7 @@ int main(int argc, char **argv) > { "nomount", no_argument, NULL, 'n' }, > { "debug", no_argument, NULL, 'd' }, > { "use-loader", no_argument, NULL, 'L' }, > + { "sign", no_argument, NULL, 'S' }, > { "base-btf", required_argument, NULL, 'B' }, > { 0 } > }; > @@ -474,7 +478,7 @@ int main(int argc, char **argv) > bin_name = "bpftool"; > > opterr = 0; > - while ((opt = getopt_long(argc, argv, "VhpjfLmndB:l", > + while ((opt = getopt_long(argc, argv, "VhpjfLmndSi:k:B:l", > options, NULL)) >= 0) { > switch (opt) { > case 'V': > @@ -520,6 +524,16 @@ int main(int argc, char **argv) > case 'L': > use_loader = true; > break; > + case 'S': > + sign_progs = true; > + use_loader = true; > + break; > + case 'k': > + private_key_path = optarg; > + break; > + case 'i': > + cert_path = optarg; > + break; > default: > p_err("unrecognized option '%s'", argv[optind - 1]); > if (json_output) > @@ -534,6 +548,16 @@ int main(int argc, char **argv) > if (argc < 0) > usage(); > > + if (sign_progs && (private_key_path == NULL || cert_path == NULL)) { > + p_err("-i <identity_x509_cert> and -k <private> key must be supplied with -S for signing"); > + return -EINVAL; > + } > + > + if (!sign_progs && (private_key_path != NULL || cert_path != NULL)) { > + p_err("-i <identity_x509_cert> and -k <private> also need --sign to be used for sign programs"); > + return -EINVAL; > + } > + > if (version_requested) > ret = do_version(argc, argv); > else > diff --git a/tools/bpf/bpftool/main.h b/tools/bpf/bpftool/main.h > index a2bb0714b3d6..f7f5b39b66c8 100644 > --- a/tools/bpf/bpftool/main.h > +++ b/tools/bpf/bpftool/main.h > @@ -6,9 +6,14 @@ > > /* BFD and kernel.h both define GCC_VERSION, differently */ > #undef GCC_VERSION > +#ifndef _GNU_SOURCE > +#define _GNU_SOURCE > +#endif > #include <stdbool.h> > #include <stdio.h> > +#include <errno.h> > #include <stdlib.h> > +#include <bpf/skel_internal.h> > #include <linux/bpf.h> > #include <linux/compiler.h> > #include <linux/kernel.h> > @@ -52,6 +57,7 @@ static inline void *u64_to_ptr(__u64 ptr) > }) > > #define ERR_MAX_LEN 1024 > +#define MAX_SIG_SIZE 4096 > > #define BPF_TAG_FMT "%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx" > > @@ -85,6 +91,9 @@ extern bool relaxed_maps; > extern bool use_loader; > extern struct btf *base_btf; > extern struct hashmap *refs_table; > +extern bool sign_progs; > +extern const char *private_key_path; > +extern const char *cert_path; > > void __printf(1, 2) p_err(const char *fmt, ...); > void __printf(1, 2) p_info(const char *fmt, ...); > @@ -275,4 +284,6 @@ int pathname_concat(char *buf, int buf_sz, const char *path, > /* print netfilter bpf_link info */ > void netfilter_dump_plain(const struct bpf_link_info *info); > void netfilter_dump_json(const struct bpf_link_info *info, json_writer_t *wtr); > +int bpftool_prog_sign(struct bpf_load_and_run_opts *opts); > +__u32 register_session_key(const char *key_der_path); > #endif > diff --git a/tools/bpf/bpftool/prog.c b/tools/bpf/bpftool/prog.c > index 9722d841abc0..82b8da084504 100644 > --- a/tools/bpf/bpftool/prog.c > +++ b/tools/bpf/bpftool/prog.c > @@ -23,6 +23,7 @@ > #include <linux/err.h> > #include <linux/perf_event.h> > #include <linux/sizes.h> > +#include <linux/keyctl.h> > > #include <bpf/bpf.h> > #include <bpf/btf.h> > @@ -1930,6 +1931,8 @@ static int try_loader(struct gen_loader_opts *gen) > { > struct bpf_load_and_run_opts opts = {}; > struct bpf_loader_ctx *ctx; > + char sig_buf[MAX_SIG_SIZE]; > + __u8 prog_sha[SHA256_DIGEST_LENGTH]; > int ctx_sz = sizeof(*ctx) + 64 * max(sizeof(struct bpf_map_desc), > sizeof(struct bpf_prog_desc)); > int log_buf_sz = (1u << 24) - 1; > @@ -1953,6 +1956,24 @@ static int try_loader(struct gen_loader_opts *gen) > opts.insns = gen->insns; > opts.insns_sz = gen->insns_sz; > fds_before = count_open_fds(); > + > + if (sign_progs) { > + opts.excl_prog_hash = prog_sha; > + opts.excl_prog_hash_sz = sizeof(prog_sha); > + opts.signature = sig_buf; > + opts.signature_sz = MAX_SIG_SIZE; > + opts.keyring_id = KEY_SPEC_SESSION_KEYRING; > + And here as well. > + err = bpftool_prog_sign(&opts); > + if (err < 0) > + return err; > + > + err = register_session_key(cert_path); > + if (err < 0) { > + p_err("failed to add session key"); > + goto out; > + } > + } > err = bpf_load_and_run(&opts); > fd_delta = count_open_fds() - fds_before; > if (err < 0 || verifier_logs) { > @@ -1961,6 +1982,7 @@ static int try_loader(struct gen_loader_opts *gen) > fprintf(stderr, "loader prog leaked %d FDs\n", > fd_delta); > } > +out: > free(log_buf); > return err; > } > @@ -1988,6 +2010,9 @@ static int do_loader(int argc, char **argv) > goto err_close_obj; > } > > + if (sign_progs) > + gen.gen_hash = true; > + > err = bpf_object__gen_loader(obj, &gen); > if (err) > goto err_close_obj; > @@ -2562,7 +2587,7 @@ static int do_help(int argc, char **argv) > " METRIC := { cycles | instructions | l1d_loads | llc_misses | itlb_misses | dtlb_misses }\n" > " " HELP_SPEC_OPTIONS " |\n" > " {-f|--bpffs} | {-m|--mapcompat} | {-n|--nomount} |\n" > - " {-L|--use-loader} }\n" > + " {-L|--use-loader} | [ {-S|--sign } {-k} <private_key.pem> {-i} <certificate.x509> ] \n" > "", > bin_name, argv[-2]); > > diff --git a/tools/bpf/bpftool/sign.c b/tools/bpf/bpftool/sign.c > new file mode 100644 > index 000000000000..b29d825bb1d4 > --- /dev/null > +++ b/tools/bpf/bpftool/sign.c > @@ -0,0 +1,212 @@ > +// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) > +/* > + * Copyright (C) 2025 Google LLC. > + */ > + > +#ifndef _GNU_SOURCE > +#define _GNU_SOURCE > +#endif > +#include <stdio.h> > +#include <stdlib.h> > +#include <stdint.h> > +#include <stdbool.h> > +#include <string.h> > +#include <string.h> > +#include <getopt.h> > +#include <err.h> > +#include <openssl/opensslv.h> > +#include <openssl/bio.h> > +#include <openssl/evp.h> > +#include <openssl/pem.h> > +#include <openssl/err.h> > +#include <openssl/cms.h> > +#include <linux/keyctl.h> > +#include <errno.h> > + > +#include <bpf/skel_internal.h> > + > +#include "main.h" > + > +#define OPEN_SSL_ERR_BUF_LEN 256 > + > +static void display_openssl_errors(int l) > +{ > + char buf[OPEN_SSL_ERR_BUF_LEN]; > + const char *file; > + const char *data; > + unsigned long e; > + int flags; > + int line; > + > + while ((e = ERR_get_error_all(&file, &line, NULL, &data, &flags))) { > + ERR_error_string_n(e, buf, sizeof(buf)); > + if (data && (flags & ERR_TXT_STRING)) { > + p_err("OpenSSL %s: %s:%d: %s", buf, file, line, data); > + } else { > + p_err("OpenSSL %s: %s:%d", buf, file, line); > + } > + } > +} > + > +#define DISPLAY_OSSL_ERR(cond) \ > + do { \ > + bool __cond = (cond); \ > + if (__cond && ERR_peek_error()) \ > + display_openssl_errors(__LINE__);\ > + } while (0) > + > +static EVP_PKEY *read_private_key(const char *pkey_path) > +{ > + EVP_PKEY *private_key = NULL; > + BIO *b; > + > + b = BIO_new_file(pkey_path, "rb"); > + private_key = PEM_read_bio_PrivateKey(b, NULL, NULL, NULL); > + BIO_free(b); > + DISPLAY_OSSL_ERR(!private_key); > + return private_key; > +} > + > +static X509 *read_x509(const char *x509_name) > +{ > + unsigned char buf[2]; > + X509 *x509 = NULL; > + BIO *b; > + int n; > + > + b = BIO_new_file(x509_name, "rb"); > + if (!b) > + goto cleanup; > + > + /* Look at the first two bytes of the file to determine the encoding */ > + n = BIO_read(b, buf, 2); > + if (n != 2) > + goto cleanup; > + > + if (BIO_reset(b) != 0) > + goto cleanup; > + > + if (buf[0] == 0x30 && buf[1] >= 0x81 && buf[1] <= 0x84) > + /* Assume raw DER encoded X.509 */ > + x509 = d2i_X509_bio(b, NULL); > + else > + /* Assume PEM encoded X.509 */ > + x509 = PEM_read_bio_X509(b, NULL, NULL, NULL); > + > +cleanup: > + BIO_free(b); > + DISPLAY_OSSL_ERR(!x509); > + return x509; > +} > + > +__u32 register_session_key(const char *key_der_path) > +{ > + unsigned char *der_buf = NULL; > + X509 *x509 = NULL; > + int key_id = -1; > + int der_len; > + > + if (!key_der_path) > + return key_id; > + x509 = read_x509(key_der_path); > + if (!x509) > + goto cleanup; > + der_len = i2d_X509(x509, &der_buf); > + if (der_len < 0) > + goto cleanup; > + key_id = syscall(__NR_add_key, "asymmetric", key_der_path, der_buf, > + (size_t)der_len, KEY_SPEC_SESSION_KEYRING); > +cleanup: > + X509_free(x509); > + OPENSSL_free(der_buf); > + DISPLAY_OSSL_ERR(key_id == -1); > + return key_id; > +} > + > +int bpftool_prog_sign(struct bpf_load_and_run_opts *opts) > +{ > + BIO *bd_in = NULL, *bd_out = NULL; > + EVP_PKEY *private_key = NULL; > + CMS_ContentInfo *cms = NULL; > + long actual_sig_len = 0; > + X509 *x509 = NULL; > + int err = 0; > + > + bd_in = BIO_new_mem_buf(opts->insns, opts->insns_sz); > + if (!bd_in) { > + err = -ENOMEM; > + goto cleanup; > + } > + > + private_key = read_private_key(private_key_path); > + if (!private_key) { > + err = -EINVAL; > + goto cleanup; > + } > + > + x509 = read_x509(cert_path); > + if (!x509) { > + err = -EINVAL; > + goto cleanup; > + } > + > + cms = CMS_sign(NULL, NULL, NULL, NULL, > + CMS_NOCERTS | CMS_PARTIAL | CMS_BINARY | CMS_DETACHED | > + CMS_STREAM); > + if (!cms) { > + err = -EINVAL; > + goto cleanup; > + } > + > + if (!CMS_add1_signer(cms, x509, private_key, EVP_sha256(), > + CMS_NOCERTS | CMS_BINARY | CMS_NOSMIMECAP | > + CMS_USE_KEYID | CMS_NOATTR)) { > + err = -EINVAL; > + goto cleanup; > + } > + > + if (CMS_final(cms, bd_in, NULL, CMS_NOCERTS | CMS_BINARY) != 1) { > + err = -EIO; > + goto cleanup; > + } > + > + EVP_Digest(opts->insns, opts->insns_sz, opts->excl_prog_hash, > + &opts->excl_prog_hash_sz, EVP_sha256(), NULL); > + > + bd_out = BIO_new(BIO_s_mem()); > + if (!bd_out) { > + err = -ENOMEM; > + goto cleanup; > + } > + > + if (!i2d_CMS_bio_stream(bd_out, cms, NULL, 0)) { > + err = -EIO; > + goto cleanup; > + } > + > + actual_sig_len = BIO_get_mem_data(bd_out, NULL); > + if (actual_sig_len <= 0) { > + err = -EIO; > + goto cleanup; > + } > + > + if ((size_t)actual_sig_len > opts->signature_sz) { > + err = -ENOSPC; > + goto cleanup; > + } > + > + if (BIO_read(bd_out, opts->signature, actual_sig_len) != actual_sig_len) { > + err = -EIO; > + goto cleanup; > + } > + > + opts->signature_sz = actual_sig_len; > +cleanup: > + BIO_free(bd_out); > + CMS_ContentInfo_free(cms); > + X509_free(x509); > + EVP_PKEY_free(private_key); > + BIO_free(bd_in); > + DISPLAY_OSSL_ERR(err < 0); > + return err; > +} > -- > 2.43.0