diff options
| author | Mark Brown <broonie@kernel.org> | 2026-09-30 13:52:57 +0100 |
|---|---|---|
| committer | Mark Brown <broonie@kernel.org> | 2026-09-30 13:52:57 +0100 |
| commit | 5ea4b2dc75234871d652b398076868ef68f307bc (patch) | |
| tree | 549b1093514445094d515a28a217d64624f89d7e | |
| parent | 40ef4277c51f6fdf33003a352eb952f8e8e4202c (diff) | |
| parent | 4991c8b72b564cd16cb48124f880617fd49f1013 (diff) | |
| download | linux-next-5ea4b2dc75234871d652b398076868ef68f307bc.tar.gz linux-next-5ea4b2dc75234871d652b398076868ef68f307bc.zip | |
Merge branch 'sysctl-next' of https://git.kernel.org/pub/scm/linux/kernel/git/sysctl/sysctl.git
| -rw-r--r-- | drivers/parport/procfs.c | 6 | ||||
| -rw-r--r-- | fs/dcache.c | 2 | ||||
| -rw-r--r-- | fs/file_table.c | 2 | ||||
| -rw-r--r-- | fs/proc/proc_sysctl.c | 2 | ||||
| -rw-r--r-- | kernel/sysctl.c | 419 | ||||
| -rw-r--r-- | kernel/time/jiffies.c | 2 | ||||
| -rw-r--r-- | lib/test_sysctl.c | 33 | ||||
| -rwxr-xr-x | tools/testing/selftests/sysctl/sysctl.sh | 1434 |
8 files changed, 842 insertions, 1058 deletions
diff --git a/drivers/parport/procfs.c b/drivers/parport/procfs.c index 3880460e67f2..1bfafc2a5724 100644 --- a/drivers/parport/procfs.c +++ b/drivers/parport/procfs.c @@ -26,7 +26,7 @@ #include <linux/uaccess.h> -#if defined(CONFIG_SYSCTL) && defined(CONFIG_PROC_FS) +#ifdef CONFIG_SYSCTL #define PARPORT_MIN_TIMESLICE_VALUE 1ul #define PARPORT_MAX_TIMESLICE_VALUE ((unsigned long) HZ) @@ -560,7 +560,7 @@ static void __exit parport_default_proc_unregister(void) parport_bus_exit(); } -#else /* no sysctl or no procfs*/ +#else /* CONFIG_SYSCTL */ int parport_proc_register(struct parport *pp) { @@ -591,7 +591,7 @@ static void __exit parport_default_proc_unregister (void) { parport_bus_exit(); } -#endif +#endif /* CONFIG_SYSCTL */ subsys_initcall(parport_default_proc_register) module_exit(parport_default_proc_unregister) diff --git a/fs/dcache.c b/fs/dcache.c index 7a9346c4f2e4..71918fe19d49 100644 --- a/fs/dcache.c +++ b/fs/dcache.c @@ -145,7 +145,7 @@ static DEFINE_PER_CPU(long, nr_dentry_unused); static DEFINE_PER_CPU(long, nr_dentry_negative); static int dentry_negative_policy; -#if defined(CONFIG_SYSCTL) && defined(CONFIG_PROC_FS) +#ifdef CONFIG_SYSCTL /* Statistics gathering. */ static struct dentry_stat_t dentry_stat = { .age_limit = 45, diff --git a/fs/file_table.c b/fs/file_table.c index c68b8c0a4097..08ca3631b726 100644 --- a/fs/file_table.c +++ b/fs/file_table.c @@ -122,7 +122,7 @@ unsigned long get_max_files(void) } EXPORT_SYMBOL_GPL(get_max_files); -#if defined(CONFIG_SYSCTL) && defined(CONFIG_PROC_FS) +#ifdef CONFIG_SYSCTL /* * Handle nr_files sysctl diff --git a/fs/proc/proc_sysctl.c b/fs/proc/proc_sysctl.c index d1cfd2941359..6c0859c66563 100644 --- a/fs/proc/proc_sysctl.c +++ b/fs/proc/proc_sysctl.c @@ -206,7 +206,7 @@ static void init_header(struct ctl_table_header *head, head->node = node; INIT_HLIST_HEAD(&head->inodes); if (node) { - const struct ctl_table *entry; + const struct ctl_table *entry __maybe_unused; list_for_each_table_entry(entry, head) { node->header = head; diff --git a/kernel/sysctl.c b/kernel/sysctl.c index f7b75985d542..f5145b32f6be 100644 --- a/kernel/sysctl.c +++ b/kernel/sysctl.c @@ -29,14 +29,12 @@ EXPORT_SYMBOL(sysctl_vals); const unsigned long sysctl_long_vals[] = { 0, 1, LONG_MAX }; EXPORT_SYMBOL_GPL(sysctl_long_vals); -#if defined(CONFIG_SYSCTL) +#ifdef CONFIG_SYSCTL /* Constants used for minimum and maximum */ static const int ngroups_max = NGROUPS_MAX; static const int cap_last_cap = CAP_LAST_CAP; -#ifdef CONFIG_SYSCTL - /** * enum sysctl_writes_mode - supported sysctl write modes * @@ -64,14 +62,6 @@ enum sysctl_writes_mode { }; static enum sysctl_writes_mode sysctl_writes_strict = SYSCTL_WRITES_STRICT; -#endif /* CONFIG_SYSCTL */ -#endif /* CONFIG_SYSCTL */ - -/* - * /proc/sys support - */ - -#ifdef CONFIG_SYSCTL static int _proc_do_string(char *data, int maxlen, int dir, char *buffer, size_t *lenp, loff_t *ppos) @@ -483,7 +473,7 @@ int proc_int_k2u_conv_kop(ulong *u_ptr, const int *k_ptr, bool *negp, if (val < 0) { *negp = true; - *u_ptr = k_ptr_op ? -k_ptr_op((ulong)val) : -(ulong)val; + *u_ptr = k_ptr_op ? k_ptr_op(-(ulong)val) : -(ulong)val; } else { *negp = false; *u_ptr = k_ptr_op ? k_ptr_op((ulong)val) : (ulong) val; @@ -595,7 +585,7 @@ static int do_proc_int_conv_minmax(bool *negp, unsigned long *u_ptr, int *k_ptr, static const char proc_wspace_sep[] = { ' ', '\t', '\n' }; /* - * Element type processed by do_proc_vec(). The tag selects the element size + * Element type processed by proc_vec(). The tag selects the element size * and signedness, and it selects which member of union proc_vec_conv is live. */ enum proc_vec_type { @@ -605,7 +595,7 @@ enum proc_vec_type { }; /* - * Converter passed to do_proc_vec(). Only the member matching the + * Converter passed to proc_vec(). Only the member matching the * enum proc_vec_type tag is ever read, so every dispatch stays fully typed and * no void * converter pointer is needed. */ @@ -637,101 +627,177 @@ static int proc_vec_conv(enum proc_vec_type type, union proc_vec_conv conv, return -EINVAL; } -/* - * Read/write a vector of @type elements. The element size and signedness are - * derived from @type, so a single runtime function replaces the per-type - * variants. table->data is walked as raw bytes (@i) advanced by @size; the - * converter performs the actual typed load/store. - */ -static int do_proc_vec(const struct ctl_table *table, int dir, - void *buffer, size_t *lenp, loff_t *ppos, - enum proc_vec_type type, union proc_vec_conv conv) +static int commit_conv_vec(const enum proc_vec_type data_type, void *dst, + const void *src, size_t nr) { - int vleft, first = 1, err = 0; - size_t left, size; - bool is_unsigned; - char *i, *p; + size_t i; - switch (type) { + switch (data_type) { case PROC_VEC_INT: - size = sizeof(int); - is_unsigned = false; - break; - case PROC_VEC_UINT: - size = sizeof(uint); - is_unsigned = true; - break; + for (i = 0; i < nr; i++) + WRITE_ONCE(((int *)dst)[i], ((const int *)src)[i]); + return 0; + case PROC_VEC_ULONG: - size = sizeof(ulong); - is_unsigned = true; - break; + for (i = 0; i < nr; i++) + WRITE_ONCE(((ulong *)dst)[i], ((const ulong *)src)[i]); + return 0; default: return -EINVAL; } +} - if (!table->data || !table->maxlen || !*lenp || - (*ppos && SYSCTL_KERN_TO_USER(dir))) { - *lenp = 0; - return 0; - } - - i = table->data; - vleft = table->maxlen / size; - left = *lenp; - - /* uint arrays are not supported, *Do not* add support for them. */ - if (type == PROC_VEC_UINT && vleft != 1) - return -EINVAL; - - if (SYSCTL_USER_TO_KERN(dir)) { - if (proc_first_pos_non_zero_ignore(ppos, table)) - goto out; - - if (left > PAGE_SIZE - 1) - left = PAGE_SIZE - 1; - p = buffer; +/** + * apply_conv_on_vec - Apply converter function on data vector + * + * @conv: The converter to be applied + * @table: The sysctl table + * @data_type: Type used in converter appliation (INT, UINT or ULONG) + * @data_size: Number of data elements. + * @conv_dir: %TRUE if this is a write to the sysctl file + * @buf_nbyte: Number of bytes for buf + * @buf: The user buffer + * @buf_left_final: Number of outstanding (non converted) bytes. + * + * Element signedness is derived from @data_type. table->data is walked + * as raw bytes (@data) advanced by @data_size; the converter performs + * the actual typed load/store. Sets buf_left_final to the number of + * bytes that where left outstanding after conversion. Can be > 0. + * + * Returns: %0 on success. Non-zero on error. + */ +static int apply_conv_on_vec(const union proc_vec_conv conv, + const struct ctl_table *table, + const enum proc_vec_type data_type, + const size_t data_size, const int conv_dir, + const size_t buf_nbyte, void *buf, + size_t *buf_left_final) +{ + int vec_left, err = 0; + size_t buf_left, nr_conv = 0; + char *data, *data_stage = NULL, *p; + bool is_unsigned = data_type == PROC_VEC_UINT || data_type == PROC_VEC_ULONG; + + buf_left = buf_nbyte; + + data = table->data; + if (SYSCTL_USER_TO_KERN(conv_dir)) { + if (buf_left > PAGE_SIZE - 1) + buf_left = PAGE_SIZE - 1; + p = buf; + + if (table->maxlen > data_size) { + data_stage = kmemdup(table->data, table->maxlen, GFP_KERNEL); + if (!data_stage) { + err = -ENOMEM; + goto out; + } + data = data_stage; + } } - for (; left && vleft--; i += size, first = 0) { + vec_left = table->maxlen / data_size; + for (; buf_left && vec_left--; data += data_size, nr_conv++) { unsigned long lval; bool neg = false; - if (SYSCTL_USER_TO_KERN(dir)) { - proc_skip_spaces(&p, &left); + if (SYSCTL_USER_TO_KERN(conv_dir)) { + proc_skip_spaces(&p, &buf_left); - if (!left) + if (!buf_left) break; - err = proc_get_long(&p, &left, &lval, &neg, + err = proc_get_long(&p, &buf_left, &lval, &neg, proc_wspace_sep, sizeof(proc_wspace_sep), NULL); if (!err && neg && is_unsigned) err = -EINVAL; if (err) break; - if (proc_vec_conv(type, conv, &neg, &lval, i, dir, table)) { + if (proc_vec_conv(data_type, conv, &neg, &lval, data, conv_dir, table)) { err = -EINVAL; break; } } else { - if (proc_vec_conv(type, conv, &neg, &lval, i, dir, table)) { + if (proc_vec_conv(data_type, conv, &neg, &lval, data, conv_dir, table)) { err = -EINVAL; break; } - if (!first) - proc_put_char(&buffer, &left, '\t'); - proc_put_long(&buffer, &left, lval, neg); + if (nr_conv) + proc_put_char(&buf, &buf_left, '\t'); + proc_put_long(&buf, &buf_left, lval, neg); } } - if (SYSCTL_KERN_TO_USER(dir) && !first && left && !err) - proc_put_char(&buffer, &left, '\n'); - if (SYSCTL_USER_TO_KERN(dir) && !err && left) - proc_skip_spaces(&p, &left); - if (SYSCTL_USER_TO_KERN(dir) && first) - return err ? : -EINVAL; - *lenp -= left; + if (SYSCTL_USER_TO_KERN(conv_dir)) { + if (!err && buf_left) + proc_skip_spaces(&p, &buf_left); + if (!nr_conv) { + err = err ? : -EINVAL; + goto out; + } + if (!err && data_stage) + err = commit_conv_vec(data_type, table->data, data_stage, nr_conv); + } else { + if (nr_conv && buf_left && !err) + proc_put_char(&buf, &buf_left, '\n'); + } + + *buf_left_final = buf_left; + +out: + kfree(data_stage); + return err; +} + +/* Read/write a vector of @type elements. */ +static int proc_vec(const struct ctl_table *table, int dir, void *buffer, + size_t *lenp, loff_t *ppos, enum proc_vec_type type, + union proc_vec_conv conv) +{ + int err = 0; + size_t data_size, left_nbyte = SIZE_MAX; + + switch (type) { + case PROC_VEC_INT: + data_size = sizeof(int); + break; + case PROC_VEC_UINT: + data_size = sizeof(uint); + break; + case PROC_VEC_ULONG: + data_size = sizeof(ulong); + break; + default: + return -EINVAL; + } + + if (!table->data || !table->maxlen || !*lenp || + (*ppos && SYSCTL_KERN_TO_USER(dir))) { + *lenp = 0; + return 0; + } + + if (SYSCTL_USER_TO_KERN(dir)) { + if (proc_first_pos_non_zero_ignore(ppos, table)) + goto out; + } + + err = apply_conv_on_vec(conv, table, type, data_size, dir, *lenp, buffer, + &left_nbyte); + + /* + * An unchanged left_nbyte signals a write with no parsed element; which + * is an error. Using SIZE_MAX to detect this error is possible because: + * 1. lenp is bounded by KMALLOC_MAX_SIZE in proc_sys_call_handler + * 2. lenp could never be SIZE_MAX as it is a "ridiculous" (exabyte) allocation. + */ + if (left_nbyte == SIZE_MAX) + return err; + + *lenp -= left_nbyte; out: *ppos += *lenp; + return err; } @@ -756,12 +822,15 @@ int proc_douintvec_conv(const struct ctl_table *table, int dir, void *buffer, int (*conv)(bool *negp, ulong *u_ptr, uint *k_ptr, int dir, const struct ctl_table *table)) { + /* uint arrays are not supported, *Do not* add support for them. */ + if (table->maxlen && (table->maxlen / sizeof(uint)) != 1) + return -EINVAL; if (!conv) conv = do_proc_uint_conv; - return do_proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_UINT, - (union proc_vec_conv){ .uint_conv = conv }); + return proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_UINT, + (union proc_vec_conv){ .uint_conv = conv }); } /** @@ -820,8 +889,8 @@ int proc_dobool(const struct ctl_table *table, int dir, void *buffer, int proc_dointvec(const struct ctl_table *table, int dir, void *buffer, size_t *lenp, loff_t *ppos) { - return do_proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_INT, - (union proc_vec_conv){ .int_conv = do_proc_int_conv }); + return proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_INT, + (union proc_vec_conv){ .int_conv = do_proc_int_conv }); } /** @@ -840,8 +909,7 @@ int proc_dointvec(const struct ctl_table *table, int dir, void *buffer, int proc_douintvec(const struct ctl_table *table, int dir, void *buffer, size_t *lenp, loff_t *ppos) { - return do_proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_UINT, - (union proc_vec_conv){ .uint_conv = do_proc_uint_conv }); + return proc_douintvec_conv(table, dir, buffer, lenp, ppos, do_proc_uint_conv); } /** @@ -864,8 +932,8 @@ int proc_douintvec(const struct ctl_table *table, int dir, void *buffer, int proc_dointvec_minmax(const struct ctl_table *table, int dir, void *buffer, size_t *lenp, loff_t *ppos) { - return do_proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_INT, - (union proc_vec_conv){ .int_conv = do_proc_int_conv_minmax }); + return proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_INT, + (union proc_vec_conv){ .int_conv = do_proc_int_conv_minmax }); } /** @@ -891,8 +959,8 @@ int proc_dointvec_minmax(const struct ctl_table *table, int dir, int proc_douintvec_minmax(const struct ctl_table *table, int dir, void *buffer, size_t *lenp, loff_t *ppos) { - return do_proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_UINT, - (union proc_vec_conv){ .uint_conv = do_proc_uint_conv_minmax }); + return proc_douintvec_conv(table, dir, buffer, lenp, ppos, + do_proc_uint_conv_minmax); } /** @@ -935,8 +1003,7 @@ int proc_dou8vec_minmax(const struct ctl_table *table, int dir, tmp.extra2 = (unsigned int *) &max; val = READ_ONCE(*data); - res = do_proc_vec(&tmp, dir, buffer, lenp, ppos, PROC_VEC_UINT, - (union proc_vec_conv){ .uint_conv = do_proc_uint_conv_minmax }); + res = proc_douintvec_minmax(&tmp, dir, buffer, lenp, ppos); if (res) return res; if (SYSCTL_USER_TO_KERN(dir)) @@ -1066,8 +1133,8 @@ int proc_doulongvec_conv(const struct ctl_table *table, int dir, int (*conv)(bool *negp, ulong *u_ptr, ulong *k_ptr, int dir, const struct ctl_table *table)) { - return do_proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_ULONG, - (union proc_vec_conv){ .ulong_conv = conv }); + return proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_ULONG, + (union proc_vec_conv){ .ulong_conv = conv }); } /** @@ -1087,10 +1154,10 @@ int proc_doulongvec_conv(const struct ctl_table *table, int dir, * Returns: %0 on success. */ int proc_doulongvec_minmax(const struct ctl_table *table, int dir, - void *buffer, size_t *lenp, loff_t *ppos) + void *buffer, size_t *lenp, loff_t *ppos) { - return do_proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_ULONG, - (union proc_vec_conv){ .ulong_conv = do_proc_ulong_conv }); + return proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_ULONG, + (union proc_vec_conv){ .ulong_conv = do_proc_ulong_conv }); } /** @@ -1114,8 +1181,8 @@ int proc_dointvec_conv(const struct ctl_table *table, int dir, void *buffer, { if (!conv) conv = do_proc_int_conv; - return do_proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_INT, - (union proc_vec_conv){ .int_conv = conv }); + return proc_vec(table, dir, buffer, lenp, ppos, PROC_VEC_INT, + (union proc_vec_conv){ .int_conv = conv }); } /** @@ -1265,6 +1332,87 @@ int proc_do_large_bitmap(const struct ctl_table *table, int dir, return err; } +int proc_do_static_key(const struct ctl_table *table, int dir, + void *buffer, size_t *lenp, loff_t *ppos) +{ + struct static_key *key = (struct static_key *)table->data; + static DEFINE_MUTEX(static_key_mutex); + int val, ret; + struct ctl_table tmp = { + .data = &val, + .maxlen = sizeof(val), + .mode = table->mode, + .extra1 = SYSCTL_ZERO, + .extra2 = SYSCTL_ONE, + }; + + if (SYSCTL_USER_TO_KERN(dir) && !capable(CAP_SYS_ADMIN)) + return -EPERM; + + mutex_lock(&static_key_mutex); + val = static_key_enabled(key); + ret = proc_dointvec_minmax(&tmp, dir, buffer, lenp, ppos); + if (SYSCTL_USER_TO_KERN(dir) && !ret) { + if (val) + static_key_enable(key); + else + static_key_disable(key); + } + mutex_unlock(&static_key_mutex); + return ret; +} + +static const struct ctl_table sysctl_subsys_table[] = { + { + .procname = "sysctl_writes_strict", + .data = &sysctl_writes_strict, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = proc_dointvec_minmax, + .extra1 = SYSCTL_NEG_ONE, + .extra2 = SYSCTL_ONE, + }, + { + .procname = "ngroups_max", + .data = (void *)&ngroups_max, + .maxlen = sizeof (int), + .mode = 0444, + .proc_handler = proc_dointvec, + }, + { + .procname = "cap_last_cap", + .data = (void *)&cap_last_cap, + .maxlen = sizeof(int), + .mode = 0444, + .proc_handler = proc_dointvec, + }, +#ifdef CONFIG_SYSCTL_ARCH_UNALIGN_ALLOW + { + .procname = "unaligned-trap", + .data = &unaligned_enabled, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = proc_dointvec, + }, +#endif +#ifdef CONFIG_SYSCTL_ARCH_UNALIGN_NO_WARN + { + .procname = "ignore-unaligned-usertrap", + .data = &no_unaligned_warning, + .maxlen = sizeof (int), + .mode = 0644, + .proc_handler = proc_dointvec, + }, +#endif +}; + +int __init sysctl_init_bases(void) +{ + register_sysctl_init("kernel", sysctl_subsys_table); + + return 0; +} + #else /* CONFIG_SYSCTL */ int proc_dostring(const struct ctl_table *table, int dir, @@ -1364,91 +1512,12 @@ int proc_do_large_bitmap(const struct ctl_table *table, int dir, return -ENOSYS; } -#endif /* CONFIG_SYSCTL */ - -#if defined(CONFIG_SYSCTL) int proc_do_static_key(const struct ctl_table *table, int dir, void *buffer, size_t *lenp, loff_t *ppos) { - struct static_key *key = (struct static_key *)table->data; - static DEFINE_MUTEX(static_key_mutex); - int val, ret; - struct ctl_table tmp = { - .data = &val, - .maxlen = sizeof(val), - .mode = table->mode, - .extra1 = SYSCTL_ZERO, - .extra2 = SYSCTL_ONE, - }; - - if (SYSCTL_USER_TO_KERN(dir) && !capable(CAP_SYS_ADMIN)) - return -EPERM; - - mutex_lock(&static_key_mutex); - val = static_key_enabled(key); - ret = proc_dointvec_minmax(&tmp, dir, buffer, lenp, ppos); - if (SYSCTL_USER_TO_KERN(dir) && !ret) { - if (val) - static_key_enable(key); - else - static_key_disable(key); - } - mutex_unlock(&static_key_mutex); - return ret; + return -ENOSYS; } -static const struct ctl_table sysctl_subsys_table[] = { -#ifdef CONFIG_SYSCTL - { - .procname = "sysctl_writes_strict", - .data = &sysctl_writes_strict, - .maxlen = sizeof(int), - .mode = 0644, - .proc_handler = proc_dointvec_minmax, - .extra1 = SYSCTL_NEG_ONE, - .extra2 = SYSCTL_ONE, - }, -#endif - { - .procname = "ngroups_max", - .data = (void *)&ngroups_max, - .maxlen = sizeof (int), - .mode = 0444, - .proc_handler = proc_dointvec, - }, - { - .procname = "cap_last_cap", - .data = (void *)&cap_last_cap, - .maxlen = sizeof(int), - .mode = 0444, - .proc_handler = proc_dointvec, - }, -#ifdef CONFIG_SYSCTL_ARCH_UNALIGN_ALLOW - { - .procname = "unaligned-trap", - .data = &unaligned_enabled, - .maxlen = sizeof(int), - .mode = 0644, - .proc_handler = proc_dointvec, - }, -#endif -#ifdef CONFIG_SYSCTL_ARCH_UNALIGN_NO_WARN - { - .procname = "ignore-unaligned-usertrap", - .data = &no_unaligned_warning, - .maxlen = sizeof (int), - .mode = 0644, - .proc_handler = proc_dointvec, - }, -#endif -}; - -int __init sysctl_init_bases(void) -{ - register_sysctl_init("kernel", sysctl_subsys_table); - - return 0; -} #endif /* CONFIG_SYSCTL */ /* * No sense putting this after each symbol definition, twice, diff --git a/kernel/time/jiffies.c b/kernel/time/jiffies.c index 1ee9e0a1dd6e..af1c69bf0c23 100644 --- a/kernel/time/jiffies.c +++ b/kernel/time/jiffies.c @@ -102,6 +102,8 @@ void __init register_refined_jiffies(long cycles_per_second) #ifdef CONFIG_SYSCTL static ulong mult_hz(const ulong val) { + if (val >= ULONG_MAX / HZ) + return ULONG_MAX; return val * HZ; } diff --git a/lib/test_sysctl.c b/lib/test_sysctl.c index 909cfcf76dbf..65ce42346170 100644 --- a/lib/test_sysctl.c +++ b/lib/test_sysctl.c @@ -15,6 +15,7 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include <linux/init.h> +#include <linux/jiffies.h> #include <linux/list.h> #include <linux/module.h> #include <linux/printk.h> @@ -48,6 +49,10 @@ struct test_sysctl_data { int boot_int; + int int_jiffies; + int int_ms_jiffies; + int int_userhz_jiffies; + unsigned int uint_0001; char string_0001[65]; @@ -67,6 +72,11 @@ static struct test_sysctl_data test_data = { .boot_int = 0, + /* One second in each converter's unit; all three store HZ jiffies. */ + .int_jiffies = HZ, + .int_ms_jiffies = HZ, + .int_userhz_jiffies = HZ, + .uint_0001 = 314, .string_0001 = "(none)", @@ -114,6 +124,27 @@ static const struct ctl_table test_table[] = { .extra2 = SYSCTL_ONE, }, { + .procname = "int_jiffies", + .data = &test_data.int_jiffies, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = proc_dointvec_jiffies, + }, + { + .procname = "int_ms_jiffies", + .data = &test_data.int_ms_jiffies, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = proc_dointvec_ms_jiffies, + }, + { + .procname = "int_userhz_jiffies", + .data = &test_data.int_userhz_jiffies, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = proc_dointvec_userhz_jiffies, + }, + { .procname = "uint_0001", .data = &test_data.uint_0001, .maxlen = sizeof(unsigned int), @@ -321,11 +352,11 @@ module_init(test_sysctl_init); static void __exit test_sysctl_exit(void) { - kfree(test_data.bitmap_0001); for (int i = 0; i < TEST_H_SIZE; i++) { if (ctl_headers[i]) unregister_sysctl_table(ctl_headers[i]); } + kfree(test_data.bitmap_0001); } module_exit(test_sysctl_exit); diff --git a/tools/testing/selftests/sysctl/sysctl.sh b/tools/testing/selftests/sysctl/sysctl.sh index b2d8bd9026a7..ad1ee0fccd7c 100755 --- a/tools/testing/selftests/sysctl/sysctl.sh +++ b/tools/testing/selftests/sysctl/sysctl.sh @@ -2,345 +2,239 @@ # SPDX-License-Identifier: GPL-2.0-or-later OR copyleft-next-0.3.1 # Copyright (C) 2017 Luis R. Rodriguez <mcgrof@kernel.org> -# This performs a series tests against the proc sysctl interface. - -# Kselftest framework requirement - SKIP code is 4. -ksft_skip=4 - -TEST_NAME="sysctl" -TEST_DRIVER="test_${TEST_NAME}" -TEST_DIR=$(dirname $0) -TEST_FILE=$(mktemp) - -# This represents -# -# TEST_ID:TEST_COUNT:ENABLED:TARGET:SKIP_NO_TARGET -# -# TEST_ID: is the test id number -# TEST_COUNT: number of times we should run the test -# ENABLED: 1 if enabled, 0 otherwise -# TARGET: test target file required on the test_sysctl module -# SKIP_NO_TARGET: 1 skip if TARGET not there -# 0 run even though TARGET not there -# -# Once these are enabled please leave them as-is. Write your own test, -# we have tons of space. -ALL_TESTS="0001:1:1:int_0001:1" -ALL_TESTS="$ALL_TESTS 0002:1:1:string_0001:1" -ALL_TESTS="$ALL_TESTS 0003:1:1:int_0002:1" -ALL_TESTS="$ALL_TESTS 0004:1:1:uint_0001:1" -ALL_TESTS="$ALL_TESTS 0005:3:1:int_0003:1" -ALL_TESTS="$ALL_TESTS 0006:50:1:bitmap_0001:1" -ALL_TESTS="$ALL_TESTS 0007:1:1:boot_int:1" -ALL_TESTS="$ALL_TESTS 0008:1:1:match_int:1" -ALL_TESTS="$ALL_TESTS 0009:1:1:unregister_error:0" -ALL_TESTS="$ALL_TESTS 0010:1:1:mnt/mnt_error:0" -ALL_TESTS="$ALL_TESTS 0011:1:1:empty_add:0" -ALL_TESTS="$ALL_TESTS 0012:1:1:u8_valid:0" - -function allow_user_defaults() +# Reduce noise by disabling "unreachable" check. +# shellcheck disable=SC2317 + +# This performs a series of tests against the proc sysctl interface. + +DIR="$(dirname "$(readlink -f "$0")")" +source "${DIR}"/../kselftest/ktap_helpers.sh + +TEST_DRIVER="test_sysctl" + +assert_write_rejected() # <file> <value> { - if [ -z $DIR ]; then - DIR="/sys/module/test_sysctl/" - fi - if [ -z $DEFAULT_NUM_TESTS ]; then - DEFAULT_NUM_TESTS=50 - fi - if [ -z $SYSCTL ]; then - SYSCTL="/proc/sys/debug/test_sysctl" - fi - if [ -z $PROD_SYSCTL ]; then - PROD_SYSCTL="/proc/sys" - fi - if [ -z $WRITES_STRICT ]; then - WRITES_STRICT="${PROD_SYSCTL}/kernel/sysctl_writes_strict" + local file=$1 val=$2 + if printf '%s' "$val" > "$file" 2>/dev/null; then + ktap_print_msg "$file: write of '$val' succeeded, expected rejection" + return 1 fi + return 0 } -function check_production_sysctl_writes_strict() +assert_content() # <file> <want-file> { - echo -n "Checking production write strict setting ... " - if [ ! -e ${WRITES_STRICT} ]; then - echo "FAIL, but skip in case of old kernel" >&2 - else - old_strict=$(cat ${WRITES_STRICT}) - if [ "$old_strict" = "1" ]; then - echo "OK" - else - echo "FAIL, strict value is 0 but force to 1 to continue" >&2 - echo "1" > ${WRITES_STRICT} - fi - fi - - if [ -z $PAGE_SIZE ]; then - PAGE_SIZE=$(getconf PAGESIZE) - fi - if [ -z $MAX_DIGITS ]; then - MAX_DIGITS=$(($PAGE_SIZE/8)) - fi - if [ -z $INT_MAX ]; then - INT_MAX=$(getconf INT_MAX) - fi - if [ -z $UINT_MAX ]; then - UINT_MAX=$(getconf UINT_MAX) + local file=$1 want_file=$2 got + got=$(cat "$file") + if [ "$got" == "$want_file" ]; then + return 0 fi + ktap_print_msg "$file: got '$got', want '$want_file'" + return 1 } -test_reqs() +# compare ignoring whitespaces +assert_content_loose() # <file> <want-file> { - uid=$(id -u) - if [ $uid -ne 0 ]; then - echo $msg must be run as root >&2 - exit $ksft_skip + local file=$1 want_file=$2 + if echo "$want_file" | diff -q -w -u - "$file" > /dev/null; then + return 0 fi + ktap_print_msg "$file: content does not match '$want_file' (whitespace-insensitive)" + return 1 +} - if ! which perl 2> /dev/null > /dev/null; then - echo "$0: You need perl installed" - exit $ksft_skip - fi - if ! which getconf 2> /dev/null > /dev/null; then - echo "$0: You need getconf installed" - exit $ksft_skip - fi - if ! which diff 2> /dev/null > /dev/null; then - echo "$0: You need diff installed" - exit $ksft_skip +# For checks where writes must NOT take effect +assert_content_rejected() # <file> <bad> +{ + local file=$1 bad=$2 + if echo "$bad" | diff -q -w -u - "$file" > /dev/null; then + ktap_print_msg "$file: unexpectedly now holds rejected value '$bad'" + return 1 fi + return 0 } -function load_req_mod() +# proc files get read a page at a time, which can confuse diff resulting in +# incorrect results. Use a temp file to diff. +assert_diff_proc_file() # <file> <want-file> { - if [ ! -d $SYSCTL ]; then - if ! modprobe -q -n $TEST_DRIVER; then - echo "$0: module $TEST_DRIVER not found [SKIP]" - echo "You must set CONFIG_TEST_SYSCTL=m in your kernel" >&2 - exit $ksft_skip - fi - modprobe $TEST_DRIVER - if [ $? -ne 0 ]; then - echo "$0: modprobe $TEST_DRIVER failed." - exit - fi + local file=$1 want_file=$2 tmp + tmp=$(mktemp) + cat "$file" > "$tmp" + + if diff -w -q "$tmp" "$want_file" > /dev/null; then + rm -f "$tmp" + return 0 fi + ktap_print_msg "$file: content does not match expected file $want_file" + rm -f "$tmp" + return 1 } -reset_vals() +# Verify that an erroneous update fails and does not change the baseline. +assert_no_partial_update() # <file> <baseline> <bad-value> { - VAL="" - TRIGGER=$(basename ${TARGET}) - case "$TRIGGER" in - int_0001) - VAL="60" - ;; - int_0002) - VAL="1" - ;; - uint_0001) - VAL="314" - ;; - string_0001) - VAL="(none)" - ;; - bitmap_0001) - VAL="" - ;; - *) - ;; - esac - echo -n $VAL > $TARGET + local file=$1 baseline=$2 bad=$3 + + printf '%s' "$baseline" > "$file" + assert_write_rejected "$file" "$bad" || return 1 + assert_content_loose "$file" "$baseline" } -set_orig() +assert_grep() # <file> <pattern> - <pattern> must appear in <file> { - if [ ! -z $TARGET ] && [ ! -z $ORIG ]; then - if [ -f ${TARGET} ]; then - echo "${ORIG}" > "${TARGET}" - fi + local file=$1 pattern=$2 + if grep -q "$pattern" "$file"; then + return 0 fi + ktap_print_msg "$file: pattern '$pattern' not found" + return 1 } -set_test() +assert_grep_absent() # <file> <pattern> - <pattern> must NOT appear { - echo "${TEST_STR}" > "${TARGET}" + local file=$1 pattern=$2 + if ! grep -q "$pattern" "$file"; then + return 0 + fi + ktap_print_msg "$file: pattern '$pattern' unexpectedly found" + return 1 } -verify() +assert_dmesg_count() # <pattern> <want-count> { - local seen - seen=$(cat "$1") - if [ "${seen}" != "${TEST_STR}" ]; then - return 1 + local pattern=$1 want=$2 got + got=$(dmesg | grep -c "$pattern") + if [ "$got" -eq "$want" ]; then + return 0 fi - return 0 + ktap_print_msg "dmesg: pattern '$pattern' seen $got times, want $want" + return 1 } -# proc files get read a page at a time, which can confuse diff, -# and get you incorrect results on proc files with long data. To use -# diff against them you must first extract the output to a file, and -# then compare against that file. -verify_diff_proc_file() +# write <str> through dd in <bs>-sized chunks +write_in_chunks() # <file> <str> <bs> { - TMP_DUMP_FILE=$(mktemp) - cat $1 > $TMP_DUMP_FILE + local file=$1 str=$2 bs=$3 + printf '%s' "$str" | dd of="$file" bs="$bs" 2>/dev/null +} - if ! diff -w -q $TMP_DUMP_FILE $2; then - return 1 +# write <str> through dd in <bs>-sized chunks. seeking <seek> blocks of size +# <bs> into the output. skipping <skip> blocks of size <bs> from the input. +# Unsyncrhonized when skip is not passed. +write_at_offset() # <file> <str> <bs> <seek> [skip] +{ + local file=$1 str=$2 bs=$3 seek=$4 skip=${5:-} + if [ -n "$skip" ]; then + printf '%s' "$str" | dd of="$file" bs="$bs" seek="$seek" skip="$skip" 2>/dev/null else - return 0 + printf '%s' "$str" | dd of="$file" bs="$bs" seek="$seek" 2>/dev/null fi } -verify_diff_w() +# pad (prefix) <str> with <pad> 'A' characters, then write it all to <file> +# with dd block size <bs> (defaults to <pad>). +write_overlong() # <file> <pad> <str> [bs] { - echo "$TEST_STR" | diff -q -w -u - $1 > /dev/null - return $? + local file=$1 pad=$2 str=$3 bs=$4 p + : "${bs:=$pad}" + + # save a string with $pad spaces in $p + printf -v p '%*s' "$pad" '' + # replace " " with "A" + printf '%s%s' "${p// /A}" "$str" | dd of="$file" bs="$bs" 2>/dev/null } -test_rc() +# pad (prefix) <str> with <pad> ASCII spaces, then write with default block size. +write_padded_digits() # <file> <pad> <val> { - if [[ $rc != 0 ]]; then - echo "Failed test, return value: $rc" >&2 - exit $rc - fi + local file=$1 pad=$2 val=$3 p + printf -v p '%*s' "$pad" '' + printf '%s%s' "$p" "$val" | dd of="$file" 2>/dev/null } -test_finish() +test_int_array_writes() { - set_orig - rm -f "${TEST_FILE}" + local t="${SYSCTL}/int_0003" + local limit - if [ ! -z ${old_strict} ]; then - echo ${old_strict} > ${WRITES_STRICT} - fi - exit $rc + [[ -f "$t" ]] || { RET=$KSFT_SKIP; return; } + + printf '%s' "4 3 2 1" > "$t" + assert_content_loose "$t" "4 3 2 1" || RET=$KSFT_FAIL + + # Skipping trailing elements leaves them intact + printf '%s' "100 101" > "$t" + assert_content_loose "$t" "100 101 2 1" || RET=$KSFT_FAIL + + # Even for an int array, a single write is still capped at MAX_DIGITS + # (PAGE_SIZE/8) bytes; check right at, and just past, that boundary. + # Carries on the state from the writes above. + limit=$((MAX_DIGITS - 1)) + write_padded_digits "$t" "$limit" "9" + assert_content_loose "$t" "9 101 2 1" || RET=$KSFT_FAIL + + limit=$((MAX_DIGITS)) + write_padded_digits "$t" "$limit" "7" + assert_content_rejected "$t" "7 101 2 1" || RET=$KSFT_FAIL } -run_numerictests() +test_int_array_no_partial_update() { - echo "== Testing sysctl behavior against ${TARGET} ==" + local t="${SYSCTL}/int_0003" - rc=0 + [[ -f "$t" ]] || { RET=$KSFT_SKIP; return; } - echo -n "Writing test file ... " - echo "${TEST_STR}" > "${TEST_FILE}" - if ! verify "${TEST_FILE}"; then - echo "FAIL" >&2 - exit 1 - else - echo "OK" - fi + # bad 3rd element (not a number) + assert_no_partial_update "$t" "1 2 3 4" "10 20 abc 40" || RET=$KSFT_FAIL - echo -n "Checking sysctl is not set to test value ... " - if verify "${TARGET}"; then - echo "FAIL" >&2 - exit 1 - else - echo "OK" - fi + # bad 3rd element (overflows int) + assert_no_partial_update "$t" "1 2 3 4" "10 20 $((INT_MAX + 1)) 40" || RET=$KSFT_FAIL - echo -n "Writing sysctl from shell ... " - set_test - if ! verify "${TARGET}"; then - echo "FAIL" >&2 - exit 1 - else - echo "OK" - fi + # bad 1st element + assert_no_partial_update "$t" "1 2 3 4" "abc 20 30 40" || RET=$KSFT_FAIL +} - echo -n "Resetting sysctl to original value ... " - set_orig - if verify "${TARGET}"; then - echo "FAIL" >&2 - exit 1 - else - echo "OK" - fi +# Exercise the write patterns +assert_write_patterns() # <file> <baseline> <val> +{ + local file=$1 baseline=$2 val=$3 status=0 - # Now that we've validated the sanity of "set_test" and "set_orig", - # we can use those functions to set starting states before running - # specific behavioral tests. + printf '%s' "$val" > "$file" + assert_content "$file" "$val" || status=1 + printf '%s' "$baseline" > "$file" + assert_content "$file" "$baseline" || status=1 - echo -n "Writing entire sysctl in single write ... " - set_orig - dd if="${TEST_FILE}" of="${TARGET}" bs=4096 2>/dev/null - if ! verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi + printf '%s' "$baseline" > "$file" + write_in_chunks "$file" "$val" 4096 + assert_content "$file" "$val" || status=1 - echo -n "Writing middle of sysctl after synchronized seek ... " - set_test - dd if="${TEST_FILE}" of="${TARGET}" bs=1 seek=1 skip=1 2>/dev/null - if ! verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi + printf '%s' "$val" > "$file" + write_at_offset "$file" "$val" 1 1 1 + assert_content "$file" "$val" || status=1 - echo -n "Writing beyond end of sysctl ... " - set_orig - dd if="${TEST_FILE}" of="${TARGET}" bs=20 seek=2 2>/dev/null - if verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi + printf '%s' "$baseline" > "$file" + write_at_offset "$file" "$val" 20 2 + assert_content_rejected "$file" "$val" || status=1 - echo -n "Writing sysctl with multiple long writes ... " - set_orig - (perl -e 'print "A" x 50;'; echo "${TEST_STR}") | \ - dd of="${TARGET}" bs=50 2>/dev/null - if verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc -} + printf '%s' "$baseline" > "$file" + write_overlong "$file" 50 "$val" + assert_content_rejected "$file" "$val" || status=1 -check_failure() -{ - echo -n "Testing that $1 fails as expected ... " - reset_vals - TEST_STR="$1" - orig="$(cat $TARGET)" - echo -n "$TEST_STR" > $TARGET 2> /dev/null - - # write should fail and $TARGET should retain its original value - if [ $? = 0 ] || [ "$(cat $TARGET)" != "$orig" ]; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc + return "$status" } -run_wideint_tests() +# None of these fit in 32 bits, so writing them to an int/uint sysctl must fail +assert_wideint_writes_rejected() # <file> <baseline> { - # sysctl conversion functions receive a boolean sign and ulong - # magnitude; here we list the magnitudes we want to test (each of - # which will be tested in both positive and negative forms). Since - # none of these values fit in 32 bits, writing them to an int- or - # uint-typed sysctl should fail. + local file=$1 baseline=$2 sign mag status=0 local magnitudes=( - # common boundary-condition values (zero, +1, -1, INT_MIN, - # and INT_MAX respectively) if truncated to lower 32 bits - # (potential for being falsely deemed in range) 0x0000000100000000 0x0000000100000001 0x00000001ffffffff 0x0000000180000000 0x000000017fffffff - - # these look like negatives, but without a leading '-' are - # actually large positives (should be rejected as above - # despite being zero/+1/-1/INT_MIN/INT_MAX in the lower 32) 0xffffffff00000000 0xffffffff00000001 0xffffffffffffffff @@ -350,750 +244,538 @@ run_wideint_tests() for sign in '' '-'; do for mag in "${magnitudes[@]}"; do - check_failure "${sign}${mag}" + assert_no_partial_update "$file" "$baseline" "${sign}${mag}" || status=1 done done + return "$status" } -# Your test must accept digits 3 and 4 to use this -run_limit_digit() +# A single write is capped at MAX_DIGITS (PAGE_SIZE/8) bytes; check that +# leading whitespace up to that limit is ignored, and that PAGE_SIZE of +# leading whitespace pushes the value past the limit and fails. +assert_digit_limit() # <file> <baseline> { - echo -n "Checking ignoring spaces up to PAGE_SIZE works on write ... " - reset_vals + local file=$1 baseline=$2 status=0 - LIMIT=$((MAX_DIGITS -1)) - TEST_STR="3" - (perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \ - dd of="${TARGET}" 2>/dev/null + printf '%s' "$baseline" > "$file" + write_padded_digits "$file" "$((MAX_DIGITS - 1))" "3" + assert_content "$file" "3" || status=1 - if ! verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc - - echo -n "Checking passing PAGE_SIZE of spaces fails on write ... " - reset_vals - - LIMIT=$((MAX_DIGITS)) - TEST_STR="4" - (perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \ - dd of="${TARGET}" 2>/dev/null + printf '%s' "$baseline" > "$file" + write_padded_digits "$file" "$MAX_DIGITS" "4" + assert_content_rejected "$file" "4" || status=1 - if verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc + return "$status" } -# You are using an int -run_limit_digit_int() +# proc_dointvec()-specific range checks: INT_MAX must be accepted, INT_MAX+1 +# must be rejected, and negative values must be accepted. +assert_int_range() # <file> <baseline> { - echo -n "Testing INT_MAX works ... " - reset_vals - TEST_STR="$INT_MAX" - echo -n $TEST_STR > $TARGET - - if ! verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc + local file=$1 baseline=$2 status=0 - echo -n "Testing INT_MAX + 1 will fail as expected ... " - reset_vals - let TEST_STR=$INT_MAX+1 - echo -n $TEST_STR > $TARGET 2> /dev/null + printf '%s' "$baseline" > "$file" + printf '%s' "$INT_MAX" > "$file" + assert_content "$file" "$INT_MAX" || status=1 - if verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc - - echo -n "Testing negative values will work as expected ... " - reset_vals - TEST_STR="-3" - echo -n $TEST_STR > $TARGET 2> /dev/null - if ! verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc -} + assert_no_partial_update "$file" "$baseline" "$((INT_MAX + 1))" || status=1 -# You used an int array -run_limit_digit_int_array() -{ - echo -n "Testing array works as expected ... " - TEST_STR="4 3 2 1" - echo -n $TEST_STR > $TARGET + printf '%s' "$baseline" > "$file" + printf '%s' "-3" > "$file" 2>/dev/null + assert_content "$file" "-3" || status=1 - if ! verify_diff_w "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc - - echo -n "Testing skipping trailing array elements works ... " - # Do not reset_vals, carry on the values from the last test. - # If we only echo in two digits the last two are left intact - TEST_STR="100 101" - echo -n $TEST_STR > $TARGET - # After we echo in, to help diff we need to set on TEST_STR what - # we expect the result to be. - TEST_STR="100 101 2 1" - - if ! verify_diff_w "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc - - echo -n "Testing PAGE_SIZE limit on array works ... " - # Do not reset_vals, carry on the values from the last test. - # Even if you use an int array, you are still restricted to - # MAX_DIGITS, this is a known limitation. Test limit works. - LIMIT=$((MAX_DIGITS -1)) - TEST_STR="9" - (perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \ - dd of="${TARGET}" 2>/dev/null - - TEST_STR="9 101 2 1" - if ! verify_diff_w "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc - - echo -n "Testing exceeding PAGE_SIZE limit fails as expected ... " - # Do not reset_vals, carry on the values from the last test. - # Now go over limit. - LIMIT=$((MAX_DIGITS)) - TEST_STR="7" - (perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \ - dd of="${TARGET}" 2>/dev/null - - TEST_STR="7 101 2 1" - if verify_diff_w "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc + return "$status" } -# You are using an unsigned int -run_limit_digit_uint() +# proc_douintvec()-specific range checks: UINT_MAX must be accepted, +# UINT_MAX+1 must be rejected, and negative values must be rejected too. +assert_uint_range() # <file> <baseline> { - echo -n "Testing UINT_MAX works ... " - reset_vals - TEST_STR="$UINT_MAX" - echo -n $TEST_STR > $TARGET - - if ! verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc + local file=$1 baseline=$2 status=0 - echo -n "Testing UINT_MAX + 1 will fail as expected ... " - reset_vals - TEST_STR=$(($UINT_MAX+1)) - echo -n $TEST_STR > $TARGET 2> /dev/null + printf '%s' "$baseline" > "$file" + printf '%s' "$UINT_MAX" > "$file" + assert_content "$file" "$UINT_MAX" || status=1 - if verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc + assert_no_partial_update "$file" "$baseline" "$((UINT_MAX + 1))" || status=1 + assert_no_partial_update "$file" "$baseline" "-3" || status=1 - echo -n "Testing negative values will not work as expected ... " - reset_vals - TEST_STR="-3" - echo -n $TEST_STR > $TARGET 2> /dev/null + return "$status" +} - if verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi - test_rc +test_int_minmax() +{ + local t="${SYSCTL}/int_0001" + local baseline="60" val="61" + + [[ -f "$t" ]] || { RET=$KSFT_SKIP; return; } + + assert_write_patterns "$t" "$baseline" "$val" || RET=$KSFT_FAIL + assert_wideint_writes_rejected "$t" "$baseline" || RET=$KSFT_FAIL + assert_digit_limit "$t" "$baseline" || RET=$KSFT_FAIL } -run_stringtests() +test_int_plain() { - echo -n "Writing entire sysctl in short writes ... " - set_orig - dd if="${TEST_FILE}" of="${TARGET}" bs=1 2>/dev/null - if ! verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi + local t="${SYSCTL}/int_0002" + local baseline="1" val="2" - echo -n "Writing middle of sysctl after unsynchronized seek ... " - set_test - dd if="${TEST_FILE}" of="${TARGET}" bs=1 seek=1 2>/dev/null - if verify "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi + [[ -f "$t" ]] || { RET=$KSFT_SKIP; return; } - echo -n "Checking sysctl maxlen is at least $MAXLEN ... " - set_orig - perl -e 'print "A" x ('"${MAXLEN}"'-2), "B";' | \ - dd of="${TARGET}" bs="${MAXLEN}" 2>/dev/null - if ! grep -q B "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi + assert_write_patterns "$t" "$baseline" "$val" || RET=$KSFT_FAIL + assert_wideint_writes_rejected "$t" "$baseline" || RET=$KSFT_FAIL + assert_digit_limit "$t" "$baseline" || RET=$KSFT_FAIL + assert_int_range "$t" "$baseline" || RET=$KSFT_FAIL +} - echo -n "Checking sysctl keeps original string on overflow append ... " - set_orig - perl -e 'print "A" x ('"${MAXLEN}"'-1), "B";' | \ - dd of="${TARGET}" bs=$(( MAXLEN - 1 )) 2>/dev/null - if grep -q B "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi +# The jiffies converters report the sign separately from the magnitude, so a +# negative value must survive the round trip through the converter. Each +# value below is chosen to convert exactly at every CONFIG_HZ. +test_int_jiffies_negative() +{ + local t="${SYSCTL}/int_jiffies" + local m="${SYSCTL}/int_ms_jiffies" + local u="${SYSCTL}/int_userhz_jiffies" - echo -n "Checking sysctl stays NULL terminated on write ... " - set_orig - perl -e 'print "A" x ('"${MAXLEN}"'-1), "B";' | \ - dd of="${TARGET}" bs="${MAXLEN}" 2>/dev/null - if grep -q B "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi + [[ -f "$t" && -f "$m" && -f "$u" ]] || { RET=$KSFT_SKIP; return; } - echo -n "Checking sysctl stays NULL terminated on overwrite ... " - set_orig - perl -e 'print "A" x ('"${MAXLEN}"'-1), "BB";' | \ - dd of="${TARGET}" bs=$(( $MAXLEN + 1 )) 2>/dev/null - if grep -q B "${TARGET}"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - fi + printf '%s' "-1" > "$t" + assert_content "$t" "-1" || RET=$KSFT_FAIL + printf '%s' "-1000" > "$m" + assert_content "$m" "-1000" || RET=$KSFT_FAIL + printf '%s' "-100" > "$u" + assert_content "$u" "-100" || RET=$KSFT_FAIL - test_rc + # The positive path was never broken; keep it covered. + printf '%s' "1" > "$t" + assert_content "$t" "1" || RET=$KSFT_FAIL } -target_exists() +test_uint_plain() { - TARGET="${SYSCTL}/$1" - TEST_ID="$2" + local t="${SYSCTL}/uint_0001" + local baseline="314" val="315" - if [ ! -f ${TARGET} ] ; then - return 0 - fi - return 1 -} + [[ -f "$t" ]] || { RET=$KSFT_SKIP; return; } -run_bitmaptest() { - # Total length of bitmaps string to use, a bit under - # the maximum input size of the test node - LENGTH=$((RANDOM % 65000)) + assert_write_patterns "$t" "$baseline" "$val" || RET=$KSFT_FAIL + assert_wideint_writes_rejected "$t" "$baseline" || RET=$KSFT_FAIL + assert_digit_limit "$t" "$baseline" || RET=$KSFT_FAIL + assert_uint_range "$t" "$baseline" || RET=$KSFT_FAIL +} - # First bit to set - BIT=$((RANDOM % 1024)) +# proc_dostring()-specific write patterns +assert_string_write_patterns() # <file> <baseline> <val> <maxlen> +{ + local file=$1 baseline=$2 val=$3 maxlen=$4 status=0 - # String containing our list of bits to set - TEST_STR=$BIT + printf '%s' "$baseline" > "$file" + write_in_chunks "$file" "$val" 1 + assert_content "$file" "$val" || status=1 - # build up the string - while [ "${#TEST_STR}" -le "$LENGTH" ]; do - # Make sure next entry is discontiguous, - # skip ahead at least 2 - BIT=$((BIT + $((2 + RANDOM % 10)))) + printf '%s' "$val" > "$file" + write_at_offset "$file" "$val" 1 1 + assert_content_rejected "$file" "$val" || status=1 - # Add new bit to the list - TEST_STR="${TEST_STR},${BIT}" + # sysctl maxlen is at least $maxlen + printf '%s' "$baseline" > "$file" + write_overlong "$file" "$((maxlen - 2))" "B" "$maxlen" + assert_grep "$file" "B" || status=1 - # Randomly make it a range - if [ "$((RANDOM % 2))" -eq "1" ]; then - RANGE_END=$((BIT + $((1 + RANDOM % 10)))) - TEST_STR="${TEST_STR}-${RANGE_END}" - BIT=$RANGE_END - fi - done + # keep original string on overflow append + printf '%s' "$baseline" > "$file" + write_overlong "$file" "$((maxlen - 1))" "B" "$((maxlen - 1))" + assert_grep_absent "$file" "B" || status=1 - echo -n "Checking bitmap handler ... " - TEST_FILE=$(mktemp) - echo -n "$TEST_STR" > $TEST_FILE + # sysctl stays NULL terminated on write + printf '%s' "$baseline" > "$file" + write_overlong "$file" "$((maxlen - 1))" "B" "$maxlen" + assert_grep_absent "$file" "B" || status=1 - cat $TEST_FILE > $TARGET 2> /dev/null - if [ $? -ne 0 ]; then - echo "FAIL" >&2 - rc=1 - test_rc - fi + # sysctl stays NULL terminated on overwrite + printf '%s' "$baseline" > "$file" + write_overlong "$file" "$((maxlen - 1))" "BB" "$((maxlen + 1))" + assert_grep_absent "$file" "B" || status=1 - if ! verify_diff_proc_file "$TARGET" "$TEST_FILE"; then - echo "FAIL" >&2 - rc=1 - else - echo "OK" - rc=0 - fi - test_rc + return "$status" } -sysctl_test_0001() +test_string_dostring() { - TARGET="${SYSCTL}/$(get_test_target 0001)" - reset_vals - ORIG=$(cat "${TARGET}") - TEST_STR=$(( $ORIG + 1 )) - - run_numerictests - run_wideint_tests - run_limit_digit -} + local t="${SYSCTL}/string_0001" + local baseline="(none)" val="Testing sysctl" maxlen=65 -sysctl_test_0002() -{ - TARGET="${SYSCTL}/$(get_test_target 0002)" - reset_vals - ORIG=$(cat "${TARGET}") - TEST_STR="Testing sysctl" - # Only string sysctls support seeking/appending. - MAXLEN=65 - - run_numerictests - run_stringtests -} + [[ -f "$t" ]] || { RET=$KSFT_SKIP; return; } -sysctl_test_0003() -{ - TARGET="${SYSCTL}/$(get_test_target 0003)" - reset_vals - ORIG=$(cat "${TARGET}") - TEST_STR=$(( $ORIG + 1 )) - - run_numerictests - run_wideint_tests - run_limit_digit - run_limit_digit_int + assert_write_patterns "$t" "$baseline" "$val" || RET=$KSFT_FAIL + assert_string_write_patterns "$t" "$baseline" "$val" "$maxlen" || RET=$KSFT_FAIL } -sysctl_test_0004() +# Emit a random sysctl bit map +random_bitmap_spec() { - TARGET="${SYSCTL}/$(get_test_target 0004)" - reset_vals - ORIG=$(cat "${TARGET}") - TEST_STR=$(( $ORIG + 1 )) - - run_numerictests - run_wideint_tests - run_limit_digit - run_limit_digit_uint -} + local length=$((RANDOM % 65000)) + local bit=$((RANDOM % 1024)) + local spec=$bit + local range_end -sysctl_test_0005() -{ - TARGET="${SYSCTL}/$(get_test_target 0005)" - reset_vals - ORIG=$(cat "${TARGET}") + while [ "${#spec}" -le "$length" ]; do + # Keep entries discontiguous, skip ahead by at least 2. + bit=$((bit + 2 + RANDOM % 10)) + spec="${spec},${bit}" - run_limit_digit_int_array -} + if [ "$((RANDOM % 2))" -eq 1 ]; then + range_end=$((bit + 1 + RANDOM % 10)) + spec="${spec}-${range_end}" + bit=$range_end + fi + done -sysctl_test_0006() -{ - TARGET="${SYSCTL}/$(get_test_target 0006)" - reset_vals - ORIG="" - run_bitmaptest + echo -n "$spec" } -sysctl_test_0007() +# Do *not* prefix with "test_". All autogenerate bitmap tests call this helper +bitmap_range_check_once() # <iter> { - TARGET="${SYSCTL}/$(get_test_target 0007)" - echo -n "Testing if $TARGET is set to 1 ... " - - if [ ! -f $TARGET ]; then - echo -e "SKIPPING\n$TARGET is not present" - return $ksft_skip - fi - - if [ -d $DIR ]; then - echo -e "SKIPPING\nTest only possible if sysctl_test is built-in, not module:" - cat $TEST_DIR/config >&2 - return $ksft_skip - fi + local t="${SYSCTL}/bitmap_0001" + local spec_file seed - ORIG=$(cat "${TARGET}") + [[ -f "$t" ]] || { RET=$KSFT_SKIP; return; } - if [ x$ORIG = "x1" ]; then - echo "OK" - return 0 - fi + seed=$((BITMAP_SEED + $1)) + RANDOM=$seed - if [ ! -f /proc/cmdline ]; then - echo -e "SKIPPING\nThere is no /proc/cmdline to check for parameter" - return $ksft_skip - fi + spec_file=$(mktemp) + random_bitmap_spec > "$spec_file" - FOUND=$(grep -c "sysctl[./]debug[./]test_sysctl[./]boot_int=1" /proc/cmdline) - if [ $FOUND = "1" ]; then - echo -e "FAIL\nKernel param found but $TARGET is not 1." >&2 - rc=1 - test_rc + if ! cat "$spec_file" > "$t" 2>/dev/null; then + ktap_print_msg "$t: write of random bitmap spec rejected (seed $seed)" + RET=$KSFT_FAIL + elif ! assert_diff_proc_file "$t" "$spec_file"; then + ktap_print_msg "$t: readback mismatch (seed $seed)" + RET=$KSFT_FAIL fi - echo -e "SKIPPING\nExpected kernel parameter missing." - echo "Kernel must be booted with parameter: sysctl.debug.test_sysctl.boot_int=1" - return $ksft_skip + rm -f "$spec_file" } -sysctl_test_0008() +test_unregister_removes_dir() { - TARGET="${SYSCTL}/$(get_test_target 0008)" - echo -n "Testing if $TARGET is matched in kernel ... " + local t="${SYSCTL}/unregister_error" - if [ ! -f $TARGET ]; then - echo -e "SKIPPING\n$TARGET is not present" - return $ksft_skip + if [[ -d "$t" ]]; then + ktap_print_msg "$t: directory still exists, expected it to be unregistered" + RET=$KSFT_FAIL fi +} - ORIG_VALUE=$(cat "${TARGET}") +test_mount_point_error() +{ + local t="${SYSCTL}/mnt/mnt_error" - if [ $ORIG_VALUE -ne 1 ]; then - echo "FAIL" >&2 - rc=1 - test_rc + if [[ -d "$t" ]]; then + ktap_print_msg "$t: directory unexpectedly created" + RET=$KSFT_FAIL fi - - echo "OK" - return 0 } -sysctl_test_0009() +test_empty_dir_registration() { - TARGET="${SYSCTL}/$(get_test_target 0009)" - echo -n "Testing if $TARGET unregistered correctly ... " - if [ -d $TARGET ]; then - echo "FAIL" >&2 - rc=1 - test_rc - fi + local t="${SYSCTL}/empty_add" - echo "OK" - return 0 + if [[ ! -d "$t" ]]; then + ktap_print_msg "$t: directory was not created" + RET=$KSFT_FAIL + return + fi + if [[ ! -d "$t/empty" ]]; then + ktap_print_msg "$t/empty: directory was not created" + RET=$KSFT_FAIL + fi } -sysctl_test_0010() +test_u8_range_check() { - TARGET="${SYSCTL}/$(get_test_target 0010)" - echo -n "Testing that $TARGET was not created ... " - if [ -d $TARGET ]; then - echo "FAIL" >&2 - rc=1 - test_rc + local t="${SYSCTL}/u8_valid" + + if [[ ! -f "$t" ]]; then + ktap_print_msg "$t: file was not created" + RET=$KSFT_FAIL + return fi - echo "OK" - return 0 + assert_dmesg_count "u8_over range value" 1 || RET=$KSFT_FAIL + assert_dmesg_count "u8_under range value" 1 || RET=$KSFT_FAIL } -sysctl_test_0011() +test_boot_param_int() { - TARGET="${SYSCTL}/$(get_test_target 0011)" - echo -n "Testing empty dir handling in ${TARGET} ... " - if [ ! -d ${TARGET} ]; then - echo -e "FAIL\nCould not create ${TARGET}" >&2 - rc=1 - test_rc - fi + local t="${SYSCTL}/boot_int" + local orig found - TARGET2="${TARGET}/empty" - if [ ! -d ${TARGET2} ]; then - echo -e "FAIL\nCould not create ${TARGET2}" >&2 - rc=1 - test_rc - fi + [[ -f "$t" ]] || { RET=$KSFT_SKIP; return; } - echo "OK" - return 0 -} + # Boot cmdline params only apply when test_sysctl is built in, not + # loaded as a module. + if [[ -d "$MODULE_DIR" ]]; then + ktap_print_msg "$t: test only possible if test_sysctl is built-in, not a module" + RET=$KSFT_SKIP + return + fi -sysctl_test_0012() -{ - TARGET="${SYSCTL}/$(get_test_target 0012)" - echo -n "Testing u8 range check in sysctl table check in ${TARGET} ... " - if [ ! -f ${TARGET} ]; then - echo -e "FAIL\nCould not create ${TARGET}" >&2 - rc=1 - test_rc + orig=$(cat "$t") + if [[ "$orig" == "1" ]]; then + return fi - local u8over_msg=$(dmesg | grep "u8_over range value" | wc -l) - if [ ! ${u8over_msg} -eq 1 ]; then - echo -e "FAIL\nu8 overflow not detected" >&2 - rc=1 - test_rc + if [[ ! -f /proc/cmdline ]]; then + ktap_print_msg "$t: no /proc/cmdline to check for the boot parameter" + RET=$KSFT_SKIP + return fi - local u8under_msg=$(dmesg | grep "u8_under range value" | wc -l) - if [ ! ${u8under_msg} -eq 1 ]; then - echo -e "FAIL\nu8 underflow not detected" >&2 - rc=1 - test_rc + found=$(grep -c "sysctl[./]debug[./]test_sysctl[./]boot_int=1" /proc/cmdline) + if [[ "$found" == "1" ]]; then + ktap_print_msg "$t: kernel param found on cmdline but value is not 1" + RET=$KSFT_FAIL + return fi - echo "OK" - return 0 + ktap_print_msg "$t: kernel not booted with sysctl.debug.test_sysctl.boot_int=1" + RET=$KSFT_SKIP } -list_tests() +test_sysctl_macro_match() { - echo "Test ID list:" - echo - echo "TEST_ID x NUM_TEST" - echo "TEST_ID: Test ID" - echo "NUM_TESTS: Recommended number of times to run the test" - echo - echo "0001 x $(get_test_count 0001) - tests proc_dointvec_minmax()" - echo "0002 x $(get_test_count 0002) - tests proc_dostring()" - echo "0003 x $(get_test_count 0003) - tests proc_dointvec()" - echo "0004 x $(get_test_count 0004) - tests proc_douintvec()" - echo "0005 x $(get_test_count 0005) - tests proc_douintvec() array" - echo "0006 x $(get_test_count 0006) - tests proc_do_large_bitmap()" - echo "0007 x $(get_test_count 0007) - tests setting sysctl from kernel boot param" - echo "0008 x $(get_test_count 0008) - tests sysctl macro values match" - echo "0009 x $(get_test_count 0009) - tests sysct unregister" - echo "0010 x $(get_test_count 0010) - tests sysct mount point" - echo "0011 x $(get_test_count 0011) - tests empty directories" - echo "0012 x $(get_test_count 0012) - tests range check for u8 proc_handler" -} + local t="${SYSCTL}/match_int" -usage() -{ - NUM_TESTS=$(grep -o ' ' <<<"$ALL_TESTS" | grep -c .) - let NUM_TESTS=$NUM_TESTS+1 - MAX_TEST=$(printf "%04d\n" $NUM_TESTS) - echo "Usage: $0 [ -t <4-number-digit> ] | [ -w <4-number-digit> ] |" - echo " [ -s <4-number-digit> ] | [ -c <4-number-digit> <test- count>" - echo " [ all ] [ -h | --help ] [ -l ]" - echo "" - echo "Valid tests: 0001-$MAX_TEST" - echo "" - echo " all Runs all tests (default)" - echo " -t Run test ID the recommended number of times" - echo " -w Watch test ID run until it runs into an error" - echo " -c Run test ID once" - echo " -s Run test ID x test-count number of times" - echo " -l List all test ID list" - echo " -h|--help Help" - echo - echo "If an error every occurs execution will immediately terminate." - echo "If you are adding a new test try using -w <test-ID> first to" - echo "make sure the test passes a series of tests." - echo - echo Example uses: - echo - echo "$TEST_NAME.sh -- executes all tests" - echo "$TEST_NAME.sh -t 0002 -- Executes test ID 0002 the recommended number of times" - echo "$TEST_NAME.sh -w 0002 -- Watch test ID 0002 run until an error occurs" - echo "$TEST_NAME.sh -s 0002 -- Run test ID 0002 once" - echo "$TEST_NAME.sh -c 0002 3 -- Run test ID 0002 three times" - echo - list_tests - exit 1 -} + [[ -f "$t" ]] || { RET=$KSFT_SKIP; return; } -function test_num() -{ - re='^[0-9]+$' - if ! [[ $1 =~ $re ]]; then - usage - fi -} -function remove_leading_zeros() -{ - echo $1 | sed 's/^0*//' + assert_content "$t" "1" || RET=$KSFT_FAIL } -function get_test_count() +run_one() { - test_num $1 - awk_field=$(remove_leading_zeros $1) - TEST_DATA=$(echo $ALL_TESTS | awk '{print $'$awk_field'}') - echo ${TEST_DATA} | awk -F":" '{print $2}' -} + local name=$1 -function get_test_enabled() -{ - test_num $1 - awk_field=$(remove_leading_zeros $1) - TEST_DATA=$(echo $ALL_TESTS | awk '{print $'$awk_field'}') - echo ${TEST_DATA} | awk -F":" '{print $3}' + # Subshell: a test cannot leak vars/cwd/traps into the next one, and an + # accidental `exit` inside a test kills only its own subshell. + ( RET=$KSFT_PASS; "$name"; exit $RET ) + + case $? in + "$KSFT_PASS") ktap_test_pass "$name" ;; + "$KSFT_SKIP") ktap_test_skip "$name" ;; + *) ktap_test_fail "$name" ;; + esac } -function get_test_target() +usage() { - test_num $1 - awk_field=$(remove_leading_zeros $1) - TEST_DATA=$(echo $ALL_TESTS | awk '{print $'$awk_field'}') - echo ${TEST_DATA} | awk -F":" '{print $4}' + cat <<-EOF + Usage: $0 [-r N] [-l] [-t T] [-s N] [-h|--help] + + Runs every auto-discovered test_* function. Setting SYSCTL_TESTS will + override the default and passing -t will override both env var and the + default. + + -r N repeat selected tests N times (default 1) + -l list discovered tests + -t T space separated tests list. For example "test_1 test_2" + -s N base seed for the randomized bitmap iterations. + -h, --help help + + SYSCTL_TESTS='test_1 test_2' $0 run only the named tests + EOF } -function get_test_skip_no_target() +list_tests() { - test_num $1 - awk_field=$(remove_leading_zeros $1) - TEST_DATA=$(echo $ALL_TESTS | awk '{print $'$awk_field'}') - echo ${TEST_DATA} | awk -F":" '{print $5}' + echo "Discovered tests:" + local t + for t in $ALL_TESTS; do + echo " $t" + done } -function skip_test() +check_reqs() { - TEST_ID=$1 - TEST_TARGET=$2 - if target_exists $TEST_TARGET $TEST_ID; then - TEST_SKIP=$(get_test_skip_no_target $TEST_ID) - if [[ $TEST_SKIP -eq "1" ]]; then - echo "Target $TEST_TARGET for test $TEST_ID does not exist ... SKIPPING" - return 0 - fi + local uid + uid=$(id -u) + + if [ "$uid" -ne 0 ]; then + ktap_skip_all "must be run as root" + exit "$KSFT_SKIP" + fi + + if ! which getconf > /dev/null 2>&1; then + ktap_skip_all "$0: You need getconf installed" + exit "$KSFT_SKIP" + fi + + if ! which diff > /dev/null 2>&1; then + ktap_skip_all "$0: You need diff installed" + exit "$KSFT_SKIP" fi - return 1 } -function run_all_tests() +check_args() # <repeat> <selected> <seed> { - for i in $ALL_TESTS ; do - TEST_ID=${i%:*:*:*:*} - ENABLED=$(get_test_enabled $TEST_ID) - TEST_COUNT=$(get_test_count $TEST_ID) - TEST_TARGET=$(get_test_target $TEST_ID) - - if [[ $ENABLED -eq "1" ]]; then - test_case $TEST_ID $TEST_COUNT $TEST_TARGET + local repeat_arg="$1" + local selected_arg="$2" + local seed_arg="$3" + local t + + if [ -z "$selected_arg" ]; then + ktap_exit_fail_msg "no test selected" + fi + + if ! [[ "$repeat_arg" =~ ^[0-9]+$ ]]; then + ktap_exit_fail_msg "repeat argument '$repeat_arg' is not a number" + fi + + if ! [[ "$seed_arg" =~ ^[0-9]+$ ]]; then + ktap_exit_fail_msg "seed argument '$seed_arg' is not a number" + fi + + for t in $selected_arg; do + if ! declare -F "$t" > /dev/null; then + ktap_exit_fail_msg "unknown selected test '$t'" fi done } -function watch_log() +allow_user_defaults() { - if [ $# -ne 3 ]; then - clear + if [ -z "${MODULE_DIR}" ]; then + MODULE_DIR="/sys/module/test_sysctl/" + fi + if [ -z "${SYSCTL}" ]; then + SYSCTL="/proc/sys/debug/test_sysctl" + fi + if [ -z "${PROD_SYSCTL}" ]; then + PROD_SYSCTL="/proc/sys" + fi + if [ -z "${WRITES_STRICT}" ]; then + WRITES_STRICT="${PROD_SYSCTL}/kernel/sysctl_writes_strict" + fi + if [ -z "${BITMAP_ITERATIONS}" ]; then + BITMAP_ITERATIONS=50 + fi + # Set seed for bitmap random tests + if [ -z "${BITMAP_SEED}" ]; then + BITMAP_SEED=$RANDOM fi - date - echo "Running test: $2 - run #$1" } -function watch_case() +check_production_sysctl_writes_strict() { - i=0 - while [ 1 ]; do - - if [ $# -eq 1 ]; then - test_num $1 - watch_log $i ${TEST_NAME}_test_$1 - ${TEST_NAME}_test_$1 - else - watch_log $i all - run_all_tests + if [ ! -e "${WRITES_STRICT}" ]; then + ktap_print_msg "${WRITES_STRICT} missing, skipping strict write check (old kernel?)" + else + old_strict=$(cat "${WRITES_STRICT}") + if [ "$old_strict" != "1" ]; then + ktap_print_msg "forcing ${WRITES_STRICT} to 1 (was ${old_strict})" + echo "1" > "${WRITES_STRICT}" fi - let i=$i+1 - done + fi + + if [ -z "${PAGE_SIZE}" ]; then + PAGE_SIZE=$(getconf PAGESIZE) + fi + if [ -z "${MAX_DIGITS}" ]; then + MAX_DIGITS=$((PAGE_SIZE / 8)) + fi + if [ -z "${INT_MAX}" ]; then + INT_MAX=$(getconf INT_MAX) + fi + if [ -z "${UINT_MAX}" ]; then + UINT_MAX=$(getconf UINT_MAX) + fi } -function test_case() +load_req_mod() { - TEST_ID=$1 - NUM_TESTS=$2 - TARGET=$3 - - if skip_test $TEST_ID $TARGET; then - return + if [ ! -d "${SYSCTL}" ]; then + if ! modprobe -q -n "${TEST_DRIVER}"; then + ktap_skip_all "module ${TEST_DRIVER} not found. \ + You must set CONFIG_TEST_SYSCTL=m in your kernel" + exit "$KSFT_SKIP" + fi + if ! modprobe "${TEST_DRIVER}"; then + ktap_exit_fail_msg "modprobe ${TEST_DRIVER} failed" + fi fi - - i=0 - while [ $i -lt $NUM_TESTS ]; do - test_num $TEST_ID - watch_log $i ${TEST_NAME}_test_${TEST_ID} noclear - RUN_TEST=${TEST_NAME}_test_${TEST_ID} - $RUN_TEST - let i=$i+1 - done } -function parse_args() +# Cleanup only! Do not call exit here or it will silently override KTAP exit status +restore_sysctl_writes_strict() { - if [ $# -eq 0 ]; then - run_all_tests - else - if [[ "$1" = "all" ]]; then - run_all_tests - elif [[ "$1" = "-w" ]]; then - shift - watch_case $@ - elif [[ "$1" = "-t" ]]; then - shift - test_num $1 - test_case $1 $(get_test_count $1) $(get_test_target $1) - elif [[ "$1" = "-c" ]]; then - shift - test_num $1 - test_num $2 - test_case $1 $2 $(get_test_target $1) - elif [[ "$1" = "-s" ]]; then - shift - test_case $1 1 $(get_test_target $1) - elif [[ "$1" = "-l" ]]; then - list_tests - elif [[ "$1" = "-h" || "$1" = "--help" ]]; then - usage - else - usage - fi + if [ -n "${old_strict}" ]; then + echo "${old_strict}" > "${WRITES_STRICT}" fi } -test_reqs allow_user_defaults + +# Each bitmap iteration is one function to generate one KTAP report per iteration +for i in $(seq 1 "$BITMAP_ITERATIONS"); do + eval "test_bitmap_range_check_$(printf '%02d' "$i")() \ + { \ + bitmap_range_check_once $i; \ + }" +done + +# Discover test_* functions above this line +ALL_TESTS=$(declare -F | awk '{print $3}' | grep '^test_' | sort) + +REPEAT=1 +while [ $# -gt 0 ]; do + case "$1" in + -r ) + shift + REPEAT=$1 + ;; + -l ) + list_tests + exit 0 + ;; + -h | --help ) + usage + exit 0 + ;; + -t ) + shift + SYSCTL_TESTS="$1" + ;; + -s ) + shift + BITMAP_SEED="$1" + ;; + * ) + usage + exit 1 + ;; + esac + shift +done + +SELECTED=${SYSCTL_TESTS-$ALL_TESTS} + +ktap_print_header +trap restore_sysctl_writes_strict EXIT +check_args "$REPEAT" "$SELECTED" "$BITMAP_SEED" +check_reqs check_production_sysctl_writes_strict load_req_mod -trap "test_finish" EXIT +ktap_print_msg "bitmap base seed: $BITMAP_SEED (replay with -s $BITMAP_SEED)" -parse_args $@ +ktap_set_plan "$(( $(wc -w <<< "$SELECTED") * REPEAT ))" + +for ((i = 0; i < REPEAT; i++)); do + for t in $SELECTED; do + run_one "$t" + done +done -exit 0 +ktap_finished |
