Module Name: src Committed By: rillig Date: Fri Mar 1 19:40:45 UTC 2024
Added Files: src/usr.bin/xlint/lint1: cksnprintb.c Log Message: lint: test format strings from snprintb calls The functions snprintb and snprintb_m are specific to NetBSD, and their format strings are tricky to get correct. Provide some assistance in catching the most common mistakes. To generate a diff of this commit: cvs rdiff -u -r0 -r1.1 src/usr.bin/xlint/lint1/cksnprintb.c Please note that diffs are not public domain; they are subject to the copyright notices on the relevant files.
Added files: Index: src/usr.bin/xlint/lint1/cksnprintb.c diff -u /dev/null src/usr.bin/xlint/lint1/cksnprintb.c:1.1 --- /dev/null Fri Mar 1 19:40:45 2024 +++ src/usr.bin/xlint/lint1/cksnprintb.c Fri Mar 1 19:40:45 2024 @@ -0,0 +1,290 @@ +/* $NetBSD: cksnprintb.c,v 1.1 2024/03/01 19:40:45 rillig Exp $ */ + +/*- + * Copyright (c) 2024 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Roland Illig <ril...@netbsd.org>. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#if HAVE_NBTOOL_CONFIG_H +#include "nbtool_config.h" +#endif + +#include <sys/cdefs.h> +#if defined(__RCSID) +__RCSID("$NetBSD: cksnprintb.c,v 1.1 2024/03/01 19:40:45 rillig Exp $"); +#endif + +#include <stdbool.h> +#include <string.h> + +#include "lint1.h" + +static bool +match_string_literal(const tnode_t *tn, const buffer **str) +{ + while (tn->tn_op == CVT) + tn = tn_ck_left(tn); + return tn->tn_op == ADDR + && tn->tn_left->tn_op == STRING + && (*str = tn->tn_left->tn_string, (*str)->data != NULL); +} + +static bool +match_snprintb_call(const function_call *call, + const buffer **out_fmt, const tnode_t **out_val) +{ + const char *func; + const tnode_t *val; + const buffer *str; + + if (call->func->tn_op == ADDR + && call->func->tn_left->tn_op == NAME + && (func = call->func->tn_left->tn_sym->s_name, true) + && ((strcmp(func, "snprintb") == 0 && call->args_len == 4) + || (strcmp(func, "snprintb_m") == 0 && call->args_len == 5)) + && match_string_literal(call->args[2], &str) + && (val = call->args[3], true)) { + *out_fmt = str; + *out_val = val; + return true; + } + return false; +} + +static int +len(quoted_iterator it) +{ + return (int)(it.i - it.start); +} + +static int +range(quoted_iterator start, quoted_iterator end) +{ + return (int)(end.i - start.start); +} + +static const char * +start(quoted_iterator it, const buffer *buf) +{ + return buf->data + it.start; +} + +static uintmax_t +val(quoted_iterator it) +{ + return it.value; +} + +static void +check_hex_escape(const buffer *buf, quoted_iterator it) +{ + if (it.hex_digits > 1) { + bool upper = false; + bool lower = false; + for (size_t i = it.start + 2; i < it.i; i++) { + if (isupper((unsigned char)buf->data[i])) + upper = true; + if (islower((unsigned char)buf->data[i])) + lower = true; + } + if (upper && lower) + /* hex escape '%.*s' mixes uppercase and lower... */ + warning(357, len(it), start(it, buf)); + } + if (it.hex_digits > 2) + /* hex escape '%.*s' has more than 2 digits */ + warning(358, len(it), start(it, buf)); +} + +static bool +check_directive(const buffer *fmt, quoted_iterator *it, bool new_style, + uint64_t *prev_field_width) +{ + + if (!quoted_next(fmt, it)) + return false; + quoted_iterator dir = *it; + + bool has_bit = !new_style + || dir.value == 'b' || dir.value == 'f' || dir.value == 'F'; + if (has_bit && new_style && !quoted_next(fmt, it)) { + /* missing bit position after '%.*s' */ + warning(364, len(dir), start(dir, fmt)); + return false; + } + /* LINTED 86 "automatic 'bit' hides external declaration" */ + quoted_iterator bit = *it; + + bool has_width = new_style + && (dir.value == 'f' || dir.value == 'F'); + if (has_width && !quoted_next(fmt, it)) { + /* missing field width after '%.*s' */ + warning(365, range(dir, bit), start(dir, fmt)); + return false; + } + quoted_iterator width = *it; + + bool has_cmp = new_style + && (dir.value == '=' || dir.value == ':'); + if (has_cmp && !quoted_next(fmt, it)) { + /* missing comparison value after directive '%.*s' */ + warning(368, range(dir, bit), start(dir, fmt)); + return false; + } + quoted_iterator cmp = *it; + + bool has_default = new_style && dir.value == '*'; + if (has_default && !quoted_next(fmt, it)) { + /* missing '\0' at the end of '%.*s' */ + warning(366, range(dir, *it), start(dir, fmt)); + return false; + } + + if (!has_bit && !has_cmp && !has_default) { + /* unknown directive '%.*s' */ + warning(374, len(dir), start(dir, fmt)); + return false; + } + + if (!quoted_next(fmt, it)) { + if (new_style && dir.value != '*') + /* missing '\0' at the end of '%.*s' */ + warning(366, range(dir, *it), start(dir, fmt)); + else + /* empty description in '%.*s' */ + warning(367, range(dir, *it), start(dir, fmt)); + return false; + } + quoted_iterator descr = *it; + + quoted_iterator prev = *it; + for (;;) { + if (new_style && it->value == 0) + break; + if (!new_style && it->value == 0) + /* old-style format contains '\0' */ + warning(362); + if (!new_style && it->value <= 32) { + *it = prev; + break; + } + if (it->escaped && !isprint((unsigned char)it->value)) { + /* non-printing character '%.*s' in description ... */ + warning(363, + len(*it), start(*it, fmt), + range(descr, *it), start(descr, fmt)); + } + prev = *it; + if (!quoted_next(fmt, it)) { + if (new_style) { + /* missing '\0' at the end of '%.*s' */ + warning(366, range(dir, prev), + start(dir, fmt)); + } + break; + } + } + + if (has_bit) + check_hex_escape(fmt, bit); + if (has_width) + check_hex_escape(fmt, width); + if (has_bit && bit.octal_digits == 0 && bit.hex_digits == 0) { + /* bit position '%.*s' in '%.*s' should be escaped as ... */ + warning(369, len(bit), start(bit, fmt), + range(dir, *it), start(dir, fmt)); + } + if (has_width && width.octal_digits == 0 && width.hex_digits == 0) { + /* field width '%.*s' in '%.*s' should be escaped as ... */ + warning(370, len(width), start(width, fmt), + range(dir, *it), start(dir, fmt)); + } + if (has_bit && (new_style ? bit.value > 63 : bit.value - 1 > 31)) { + /* bit position '%.*s' (%ju) in '%.*s' out of range %u..%u */ + warning(371, + len(bit), start(bit, fmt), val(bit), + range(dir, *it), start(dir, fmt), + new_style ? 0 : 1, new_style ? 63 : 32); + } + if (has_width && width.value > (new_style ? 64 : 32)) { + /* field width '%.*s' (%ju) in '%.*s' out of range 0..%u */ + warning(372, + len(width), start(width, fmt), val(width), + range(dir, *it), start(dir, fmt), + new_style ? 64 : 32); + } + if (has_width && bit.value + width.value > 64) { + /* bit field end %ju in '%.*s' out of range 0..64 */ + warning(373, val(bit) + val(width), + range(dir, *it), start(dir, fmt)); + } + if (has_cmp && *prev_field_width < 64 + && cmp.value & ~(uint64_t)0 << *prev_field_width) { + /* comparison value '%.*s' (%ju) exceeds field width %ju */ + warning(375, len(cmp), start(cmp, fmt), val(cmp), + *prev_field_width); + } + if (descr.i == prev.i && dir.value != 'F') { + /* empty description in '%.*s' */ + warning(367, range(dir, *it), start(dir, fmt)); + } + + if (has_width) + *prev_field_width = width.value; + return true; +} + +void +check_snprintb(const tnode_t *expr) +{ + const buffer *fmt; + const tnode_t *value; + if (!match_snprintb_call(expr->tn_call, &fmt, &value)) + return; + + quoted_iterator it = { .start = 0 }; + if (!quoted_next(fmt, &it)) { + /* missing new-style '\177' or old-style number base */ + warning(359); + return; + } + bool new_style = it.value == '\177'; + if (new_style && !quoted_next(fmt, &it)) { + /* missing new-style number base after '\177' */ + warning(360); + return; + } + if (it.value != 8 && it.value != 10 && it.value != 16) { + /* number base '%.*s' is %ju, should be 8, 10 or 16 */ + warning(361, len(it), start(it, fmt), val(it)); + return; + } + + uint64_t prev_field_width = 64; + while (check_directive(fmt, &it, new_style, &prev_field_width)) + continue; +}