I’m trying to make a “loadable” Bash builtin that can be enabled using the enable
command. This builtin will use lseek to allow changing the byte offset of a file descriptor.
The end goal here is being able to rewind a file descriptor by 1 byte and to do so extremely fast (say, less than 100 μs). For this particular application the time it would take to call an external binary to implement this is too long, but I think/hope that a loadable builtin will be sufficiently fast.
I am able to create the shared dynamically loaded library file and can load it with enable -f ./lseek lseek
, but when I try to use it I just get a segmentation fault (core dumped)
error.
The lseek.c
source code as well as the commands I’m using to compile it are shown in the script below.
NOTE: I have very little knowledge of C. The lseek.c
source code is originally from ChatGPT, and then I tried my best to modify it using the example loadables and this guide, and sorta just tweaked stuff until it compiled and loaded. The compile commands are based off of what make
runs to compile the other example loadables.
If anyone can see what the error is and tell me how to correct it I’d much appreciate it.
My env
- OS: Fedora 40
- Bash: 5.2.x
Build the loadable
git clone https://git.savannah.gnu.org/git/bash.git
cd bash
./configure
cd ./examples/loadables
cat<<'EOF' >lseek.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include "command.h"
#include "builtins.h"
int
builtin_lseek (list)
WORD_LIST *list; {
// Check for the right number of arguments
if (list == NULL || list->word->word == NULL || list->next->word->word == NULL || list->next->next->word->word != NULL) {
fprintf(stderr, "Usage: lseek <fd> <offset>n");
return 1;
}
// Extract arguments from WORD_LIST
char *fd_str = list->word->word;
char *offset_str = list->next->word->word;
int fd = atoi(fd_str); // Convert the first argument to integer (file descriptor)
off_t offset = atoll(offset_str); // Convert the second argument to long long (offset)
if (fd < 0) {
fprintf(stderr, "Invalid file descriptor: %dn", fd);
return 1;
}
off_t new_offset = lseek(fd, offset, SEEK_CUR); // Use SEEK_CUR to move relative to current position
if (new_offset == (off_t) -1) {
perror("lseek");
return 1;
}
printf("File descriptor %d moved to offset %lldn", fd, (long long)new_offset);
return EXIT_SUCCESS;
}
char *lseek_doc[] =
{
"Change file descriptor offset",
"",
"Changes the byte offset of the file descriptor relative to the current byte offset",
"--> 1st input is file descriptor number (non-negative integer)",
"--> 2nd input is relative byte offset (non-zero integer)",
"",
"Specifying a positive byte offset will advance the file descriptor",
"Specifying a negative byte offset will rewind the file descriptor",
(char *)NULL
};
struct builtin lseek_struct = {
"lseek", /* command name */
builtin_lseek, /* function implementing the builtin */
BUILTIN_ENABLED, /* initial flags for builtin */
lseek_doc, /* array of long documentation strings. */
"lseek <fd> <rel_offset>", /* usage synopsis; becomes short_doc */
0 /* reserved for internal use */
};
EOF
gcc -fPIC -DHAVE_CONFIG_H -DSHELL -g -O2 -Wno-parentheses -Wno-format-security -I. -I.. -I../.. -I../../lib -I../../builtins -I. -I../../include -c -o lseek.o lseek.c
gcc -shared -Wl,-soname,lseek -I /usr/include/bash -o lseek lseek.o
Test the loadable
enable -f ./lseek lseek
(
mapfile -t -n 11 -u $fd A
lseek $fd 1; read -r -u $fd B
printf '%sn' "${A[@]}"; echo; echo; echo $B
) {fd}<./lseek.c
Test result
Segmentation fault (core dumped)
3
This check is buggy:
if (list == NULL || list->word->word == NULL || list->next->word->word == NULL || list->next->next->word->word != NULL) {
fprintf(stderr, "Usage: lseek <fd> <offset>n");
return 1;
}
With lseek <fd> <offset>
, only list
(the fd) and list->next
(the offset) are available so list->next->next
would be NULL and list->next->next->word->word
causes the segfault.
You can just change it to:
if (list == NULL || list->next == NULL) {
fprintf(stderr, "Usage: lseek <fd> <offset>n");
return 1;
}
I tried and the lseek
loadable works fine with this change.
And it’s good practice to declare internal functions/vars as static:
static int builtin_lseek( ...
static char *lseek_doc[] = ...
3
I believe Ive figured out a working solution.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include "command.h"
#include "builtins.h"
#include "shell.h"
#include "common.h"
#include "bashgetopt.h"
#include "xmalloc.h"
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#ifndef errno
extern int errno;
#endif
// Function declaration for our builtin
static int lseek_main(int argc, char **argv);
int lseek_builtin(WORD_LIST *list);
extern char **make_builtin_argv();
// Metadata about the builtin
static char *lseek_doc[] = {
"",
"----------------------------------------------",
"USAGE: lseek <FD> <REL_OFFSET>",
"",
"Move the file descriptor <FD> by <REL_OFFSET>",
"bytes relative to its current byte offset.",
"",
"positive <REL_OFFSET> advances the <FD>",
"negative <REL_OFFSET> rewinds the <FD>",
"----------------------------------------------",
"",
NULL
};
// Struct to register the builtin with bash
struct builtin lseek_struct = {
"lseek", // Name of the builtin
lseek_builtin, // Function to call
BUILTIN_ENABLED, // Default status
lseek_doc, // Documentation strings
"lseek <FD> <REL_OFFSET>", // Usage string
0 // Number of long options
};
// main function
static int lseek_main(int argc, char **argv) {
// check for exactly 2 args passed to lseek
if (argc != 3) {
fprintf(stderr, "nIncorrect number of arguments.nUSAGE: lseek <FD> <REL_OFFSET>n");
return 1;
}
// get + validate file descriptor
int fd = atoi(argv[1]);
if (fd == 0 && strcmp(argv[1], "0") != 0) {
fprintf(stderr, "nERROR: Invalid file descriptor.n");
return 1;
}
// get + validate (relative) offset
errno = 0;
off_t offset = atoll(argv[2]);
if (errno == ERANGE) {
fprintf(stderr, "nERROR: Offset out of range.n");
return 1;
}
// call lseek to move fd byte offset
if (lseek(fd, offset, SEEK_CUR) == (off_t) -1) {
fprintf(stderr, "nERROR: %sn", strerror(errno));
return 1;
}
return 0;
}
// func to convert WORD_LIST to argc + argv
// (this one is called by the builtin)
int lseek_builtin(WORD_LIST *list) {
int c, r;
char **v;
v = make_builtin_argv(list, &c);
r = lseek_main(c, v);
xfree(v);
return r;
}
2