Skip to content

Instantly share code, notes, and snippets.

@easyaspi314
Last active March 18, 2019 18:58
Show Gist options
  • Save easyaspi314/7fd65e9c87ac57b8839301a358a3d7d6 to your computer and use it in GitHub Desktop.
Save easyaspi314/7fd65e9c87ac57b8839301a358a3d7d6 to your computer and use it in GitHub Desktop.
gets, but safer thanks to GNU extensions

gets but safer

This is like gets. It is a drop in for gets. It is smarter though.

gets_safe, which is aliased to gets for convenience, takes an optional length argument, but it also uses GCC and Clang's __builtin_object_size builtin to calculate the length.

The length will not always be calculated properly, but for static arrays and malloc'd pointers in the current block with a fixed size (only with optimizations), it will usually pick up on it.

This also has a safety feature: If it can properly calculate the length and the supplied length is larger than the one it calculated, it will be an error. Additionally, when both the optional length parameter are omitted and the length cannot be calculated at compile time, it is an error.

Don't call this on a function with side effects, the arguments are used multiple times.

1) char *gets_safe(char *const buf, size_t len = sizeof(buf) /* basically */);
1) char *gets(char *const buf, size_t len = sizeof(buf) /* basically */);
3) void flush_line(FILE *f);
4) void flush_stdin()
  1. Reads a line from standard input. buf: A valid pointer to writable memory. If it is a static array or a pointer which has a size known by __builtin_object_size, len is not required. It is undefined behavior for buf to be a null pointer. len: An optional length for buf. When __builtin_object_size cannot be calculated, len is required. It is undefined behavior for len to be larger than the size of buf. Returns: buf on success, NULL on error.
  2. A macro alias for 1)
  3. Reads the remainder of a line from f. This is to be used after a partial fgets or a scanf.
  4. Same as 3) but for stdin.
/* Copyright (C) 2019 easyaspi314. MIT license. */
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#ifndef __has_attribute
# define __has_attribute(x) 0
#endif
#if defined(__clang__) && __has_attribute(pass_object_size)
# define _PASS_OBJECT_SIZE __attribute__((__pass_object_size__(2)))
#else
# define _PASS_OBJECT_SIZE
#endif
#if !defined(__GNUC__)
# error "Please use a recent GCC-compatible compiler!"
#endif
#ifndef __OPTIMIZE__
# warning "Enable optimizations to make bounds checking work properly!"
#endif
static __inline__ void flush_line(FILE *__f) {
/* clear out the buffer */
int __c;
while ((__c = getc(__f)) != '\n' && __c != EOF) {}
}
#define flush_stdin() flush_line(stdin)
static char *__gets_safe(char *const __buf _PASS_OBJECT_SIZE, size_t __len, size_t __len_calculated)
{
char *__newline;
size_t __tmp;
/* Try to double check the object size */
if (__len_calculated == 0 && __len != 0 && (__tmp = __builtin_object_size(__buf, 2)) != 0) {
__len_calculated = __tmp;
}
/* the user didn't supply a length, but we were able to calculate it */
if (__len == 0 && __len_calculated != 0)
__len = __len_calculated;
/* the user supplied a longer length than what we could calculate. We
* ignore it in release mode, but we return NULL on debug mode. */
else if (__len > __len_calculated && __len_calculated != 0) {
#ifdef NDEBUG
__len = __len_calculated;
#else
fprintf(stderr, "gets_safe: Supplied length appears to be larger than the buffer size!\n");
return NULL;
#endif
/* Not even going to try on an unknown length */
} else if (__len == 0 && __len_calculated == 0) {
#ifndef NDEBUG
fprintf(stderr, "gets_safe: Unknown buffer size!\n");
#endif
return NULL;
}
#ifndef NDEBUG
if (__buf == NULL) {
fprintf(stderr, "gets_safe: null buffer!\n");
return NULL;
}
#endif
if (fgets(__buf, __len, stdin) == NULL) {
#ifndef NDEBUG
fprintf(stderr, "gets_safe: error reading line\n");
#endif
return NULL;
}
__newline = strchr(__buf, '\n'); /* find '\n' */
if (__newline != NULL) { /* we have a full line, replace the terminator */
*__newline = '\0';
} else { /* don't have a full line, clean up the rest */
flush_stdin();
}
return __buf;
}
/* Get the first argument in a variadic macro */
#define __gets_first_arg(x, ...) (x)
/* pass to __gets_safe the buffer, the user-supplied length or zero, and the compiler-calculated length. */
#define __gets_impl(x, ...) __gets_safe((x), __gets_first_arg(__VA_ARGS__), __builtin_object_size((x), 2))
/* We add two extra arguments to be placeholders when the user doesn't supply a length. */
#define gets_safe(...) __gets_impl(__VA_ARGS__, 0, 0)
#define gets(...) __gets_impl(__VA_ARGS__, 0, 0)
#ifdef GETS_SAFE_TEST
int main(void)
{
unsigned len;
char *buf;
char arr[4];
puts("=> Enter a buffer size, larger than zero:");
while (scanf("%u", &len) != 1 || len == 0) {
flush_stdin();
puts("=> Invalid input!");
}
buf = malloc(len);
if (buf == NULL) {
puts("=> Error! Out of memory!");
return 1;
}
flush_stdin();
printf("=> Enter a string. It should truncate to %u chars:\n", len - 1);
if (gets_safe(buf, len) != NULL)
puts(buf);
puts("=> Enter another string. It should truncate to 3 chars:");
if (gets_safe(arr) != NULL)
puts(arr);
puts("=> You shouldn't be able to enter a string here because the size isn't known.");
if (gets_safe(buf) != NULL)
puts("=> This shouldn't happen!");
puts("=> In release mode, you should only be able to type 3 chars here, despite giving a size of 100. In debug mode, you will get an error.");
if (gets_safe(arr, 100) != NULL)
puts(arr);
free(buf);
{
char *buf2 = malloc(10);
if (buf2 == NULL) {
puts("=> Error! Out of memory!");
return 1;
}
puts("=> Enter another string. This should truncate to 9 chars.");
if (gets_safe(buf2) != NULL)
puts(buf2);
free(buf2);
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment