Line data Source code
1 : #define _GNU_SOURCE
2 : #define FD_UNALIGNED_ACCESS_STYLE 0
3 :
4 : #include "fdctl.h"
5 :
6 : #include <fcntl.h>
7 : #include <sys/mman.h>
8 :
9 : action_t ACTIONS[ ACTIONS_CNT ] = {
10 : { .name = "run", .args = NULL, .fn = run_cmd_fn, .perm = run_cmd_perm, .description = "Start up a Firedancer validator" },
11 : { .name = "run1", .args = run1_cmd_args, .fn = run1_cmd_fn, .perm = NULL, .description = "Start up a single Firedancer tile" },
12 : { .name = "run-agave", .args = NULL, .fn = run_agave_cmd_fn, .perm = NULL, .description = "Start up the Agave side of a Firedancer validator" },
13 : { .name = "configure", .args = configure_cmd_args, .fn = configure_cmd_fn, .perm = configure_cmd_perm, .description = "Configure the local host so it can run Firedancer correctly" },
14 : { .name = "monitor", .args = monitor_cmd_args, .fn = monitor_cmd_fn, .perm = monitor_cmd_perm, .description = "Monitor a locally running Firedancer instance with a terminal GUI" },
15 : { .name = "keys", .args = keys_cmd_args, .fn = keys_cmd_fn, .perm = NULL, .description = "Generate new keypairs for use with the validator or print a public key" },
16 : { .name = "ready", .args = NULL, .fn = ready_cmd_fn, .perm = NULL, .description = "Wait for all tiles to be running" },
17 : { .name = "mem", .args = NULL, .fn = mem_cmd_fn, .perm = NULL, .description = "Print workspace memory and tile topology information" },
18 : { .name = "spy", .args = NULL, .fn = spy_cmd_fn, .perm = NULL, .description = "Spy on and print out gossip traffic" },
19 : { .name = "help", .args = NULL, .fn = help_cmd_fn, .perm = NULL, .description = "Print this help message" },
20 : { .name = "version", .args = NULL, .fn = version_cmd_fn, .perm = NULL, .description = "Show the current software version" },
21 : };
22 :
23 : struct action_alias {
24 : const char * name;
25 : const char * alias;
26 : };
27 :
28 : struct action_alias ALIASES[] = {
29 : { .name = "info", .alias = "mem" },
30 : { .name = "topo", .alias = "mem" },
31 : };
32 :
33 : extern int * fd_log_private_shared_lock;
34 :
35 : static void
36 : copy_config_from_fd( int config_fd,
37 0 : config_t * config ) {
38 0 : uchar * bytes = mmap( NULL, sizeof( config_t ), PROT_READ, MAP_PRIVATE, config_fd, 0 );
39 0 : if( FD_UNLIKELY( bytes == MAP_FAILED ) ) FD_LOG_ERR(( "mmap() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
40 0 : fd_memcpy( config, bytes, sizeof( config_t ) );
41 0 : if( FD_UNLIKELY( munmap( bytes, sizeof( config_t ) ) ) ) FD_LOG_ERR(( "munmap() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
42 0 : if( FD_UNLIKELY( close( config_fd ) ) ) FD_LOG_ERR(( "close() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
43 0 : }
44 :
45 : static int *
46 0 : map_log_memfd( int log_memfd ) {
47 0 : void * shmem = mmap( NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, log_memfd, (off_t)0 );
48 0 : if( FD_UNLIKELY( shmem==MAP_FAILED ) ) {
49 0 : FD_LOG_ERR(( "mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,memfd,(off_t)0) (%i-%s); ", errno, fd_io_strerror( errno ) ));
50 0 : } else {
51 0 : if( FD_UNLIKELY( mlock( shmem, 4096 ) ) ) {
52 0 : FD_LOG_ERR(( "mlock(%p,4096) (%i-%s); unable to lock log file shared lock in memory\n", shmem, errno, fd_io_strerror( errno ) ));
53 0 : }
54 0 : }
55 0 : return shmem;
56 0 : }
57 :
58 : /* Try to allocate an anonymous page of memory in a file descriptor
59 : (memfd) for fd_log_private_shared_lock such that the log can strictly
60 : sequence messages written by clones of the caller made after the
61 : caller has finished booting the log. Must be a file descriptor so
62 : we can pass it through `execve` calls. */
63 : static int
64 0 : init_log_memfd( void ) {
65 0 : int memfd = memfd_create( "fd_log_lock_page", 0U );
66 0 : if( FD_UNLIKELY( -1==memfd) ) FD_LOG_ERR(( "memfd_create(\"fd_log_lock_page\",0) failed (%i-%s)", errno, fd_io_strerror( errno ) ));
67 0 : if( FD_UNLIKELY( -1==ftruncate( memfd, 4096 ) ) ) FD_LOG_ERR(( "ftruncate(memfd,4096) failed (%i-%s)", errno, fd_io_strerror( errno ) ));
68 0 : return memfd;
69 0 : }
70 :
71 : static int
72 0 : should_colorize( void ) {
73 0 : char const * cstr = fd_env_strip_cmdline_cstr( NULL, NULL, NULL, "COLORTERM", NULL );
74 0 : if( cstr && !strcmp( cstr, "truecolor" ) ) return 1;
75 :
76 0 : cstr = fd_env_strip_cmdline_cstr( NULL, NULL, NULL, "TERM", NULL );
77 0 : if( cstr && !strcmp( cstr, "xterm-256color" ) ) return 1;
78 0 : return 0;
79 0 : }
80 :
81 : static void
82 0 : initialize_numa_assignments( fd_topo_t * topo ) {
83 : /* Assign workspaces to NUMA nodes. The heuristic here is pretty
84 : simple for now: workspaces go on the NUMA node of the first
85 : tile which maps the largest object in the workspace. */
86 :
87 0 : for( ulong i=0UL; i<topo->wksp_cnt; i++ ) {
88 0 : ulong max_footprint = 0UL;
89 0 : ulong max_obj = ULONG_MAX;
90 :
91 0 : for( ulong j=0UL; j<topo->obj_cnt; j++ ) {
92 0 : fd_topo_obj_t * obj = &topo->objs[ j ];
93 0 : if( obj->wksp_id!=i ) continue;
94 :
95 0 : if( FD_UNLIKELY( !max_footprint || obj->footprint>max_footprint ) ) {
96 0 : max_footprint = obj->footprint;
97 0 : max_obj = j;
98 0 : }
99 0 : }
100 :
101 0 : if( FD_UNLIKELY( max_obj==ULONG_MAX ) ) FD_LOG_ERR(( "no object found for workspace %s", topo->workspaces[ i ].name ));
102 :
103 0 : int found_strict = 0;
104 0 : int found_lazy = 1;
105 0 : for( ulong j=0UL; j<topo->tile_cnt; j++ ) {
106 0 : fd_topo_tile_t * tile = &topo->tiles[ j ];
107 0 : if( FD_UNLIKELY( tile->tile_obj_id==max_obj && tile->cpu_idx!=ULONG_MAX ) ) {
108 0 : topo->workspaces[ i ].numa_idx = fd_shmem_numa_idx( tile->cpu_idx );
109 0 : FD_TEST( topo->workspaces[ i ].numa_idx!=ULONG_MAX );
110 0 : found_strict = 1;
111 0 : found_lazy = 1;
112 0 : break;
113 0 : } else if( FD_UNLIKELY( tile->tile_obj_id==max_obj && tile->cpu_idx==ULONG_MAX ) ) {
114 0 : topo->workspaces[ i ].numa_idx = 0;
115 0 : found_lazy = 1;
116 0 : break;
117 0 : }
118 0 : }
119 :
120 0 : if( FD_LIKELY( !found_strict ) ) {
121 0 : for( ulong j=0UL; j<topo->tile_cnt; j++ ) {
122 0 : fd_topo_tile_t * tile = &topo->tiles[ j ];
123 0 : for( ulong k=0UL; k<tile->uses_obj_cnt; k++ ) {
124 0 : if( FD_LIKELY( tile->uses_obj_id[ k ]==max_obj && tile->cpu_idx!=ULONG_MAX ) ) {
125 0 : topo->workspaces[ i ].numa_idx = fd_shmem_numa_idx( tile->cpu_idx );
126 0 : FD_TEST( topo->workspaces[ i ].numa_idx!=ULONG_MAX );
127 0 : found_lazy = 1;
128 0 : break;
129 0 : } else if( FD_UNLIKELY( tile->uses_obj_id[ k ]==max_obj ) && tile->cpu_idx==ULONG_MAX ) {
130 0 : topo->workspaces[ i ].numa_idx = 0;
131 0 : found_lazy = 1;
132 : /* Don't break, keep looking -- a tile with a CPU assignment
133 : might also use object in which case we want to use that
134 : NUMA node. */
135 0 : }
136 0 : }
137 :
138 0 : if( FD_UNLIKELY( found_lazy ) ) break;
139 0 : }
140 0 : }
141 :
142 0 : if( FD_UNLIKELY( !found_lazy ) ) FD_LOG_ERR(( "no tile uses object %s for workspace %s", topo->objs[ max_obj ].name, topo->workspaces[ i ].name ));
143 0 : }
144 0 : }
145 :
146 : void
147 : fdctl_boot( int * pargc,
148 : char *** pargv,
149 : config_t * config,
150 0 : char const * log_path ) {
151 0 : fd_log_enable_unclean_exit(); /* Don't call atexit handlers on FD_LOG_ERR */
152 0 : fd_log_level_core_set( 5 ); /* Don't dump core for FD_LOG_ERR during boot */
153 0 : fd_log_colorize_set( should_colorize() ); /* Colorize during boot until we can determine from config */
154 0 : fd_log_level_stderr_set( 2 ); /* Only NOTICE and above will be logged during boot until fd_log is initialized */
155 :
156 0 : int config_fd = fd_env_strip_cmdline_int( pargc, pargv, "--config-fd", NULL, -1 );
157 :
158 0 : fd_memset( config, 0, sizeof( config_t ) );
159 0 : char * thread = "";
160 0 : if( FD_UNLIKELY( config_fd >= 0 ) ) {
161 0 : copy_config_from_fd( config_fd, config );
162 : /* tick_per_ns needs to be synchronized across processes so that
163 : they can coordinate on metrics measurement. */
164 0 : fd_tempo_set_tick_per_ns( config->tick_per_ns_mu, config->tick_per_ns_sigma );
165 0 : } else {
166 0 : fdctl_cfg_from_env( pargc, pargv, config );
167 0 : config->tick_per_ns_mu = fd_tempo_tick_per_ns( &config->tick_per_ns_sigma );
168 0 : config->log.lock_fd = init_log_memfd();
169 0 : config->log.log_fd = -1;
170 0 : thread = "main";
171 0 : if( FD_UNLIKELY( log_path ) )
172 0 : strncpy( config->log.path, log_path, sizeof( config->log.path ) - 1 );
173 0 : }
174 :
175 0 : char * shmem_args[ 3 ];
176 : /* pass in --shmem-path value from the config */
177 0 : shmem_args[ 0 ] = "--shmem-path";
178 0 : shmem_args[ 1 ] = config->hugetlbfs.mount_path;
179 0 : shmem_args[ 2 ] = NULL;
180 0 : char ** argv = shmem_args;
181 0 : int argc = 2;
182 :
183 0 : int * log_lock = map_log_memfd( config->log.lock_fd );
184 0 : ulong pid = fd_sandbox_getpid(); /* Need to read /proc since we might be in a PID namespace now */;
185 :
186 0 : log_path = config->log.path;
187 0 : if( FD_LIKELY( config->log.path[ 0 ]=='\0' ) ) log_path = NULL;
188 :
189 : /* Switch to the sandbox uid/gid for log file creation, so it's always
190 : owned by that user. */
191 :
192 0 : gid_t gid = getgid();
193 0 : uid_t uid = getuid();
194 0 : if( FD_LIKELY( !gid && setegid( config->gid ) ) ) FD_LOG_ERR(( "setegid() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
195 0 : if( FD_LIKELY( !uid && seteuid( config->uid ) ) ) FD_LOG_ERR(( "seteuid() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
196 :
197 0 : fd_log_private_boot_custom( log_lock,
198 0 : 0UL,
199 0 : config->name,
200 0 : 0UL, /* Thread ID will be initialized later */
201 0 : thread, /* Thread will be initialized later */
202 0 : 0UL,
203 0 : config->hostname,
204 0 : fd_log_private_cpu_id_default(),
205 0 : NULL,
206 0 : pid,
207 0 : NULL,
208 0 : pid,
209 0 : config->uid,
210 0 : config->user,
211 0 : 1,
212 0 : config->log.colorize1,
213 0 : config->log.level_logfile1,
214 0 : config->log.level_stderr1,
215 0 : config->log.level_flush1,
216 0 : 5,
217 0 : config->log.log_fd,
218 0 : log_path );
219 :
220 0 : if( FD_UNLIKELY( seteuid( uid ) ) ) FD_LOG_ERR(( "seteuid() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
221 0 : if( FD_UNLIKELY( setegid( gid ) ) ) FD_LOG_ERR(( "setegid() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
222 :
223 0 : config->log.log_fd = fd_log_private_logfile_fd();
224 0 : fd_shmem_private_boot( &argc, &argv );
225 0 : fd_tile_private_boot( 0, NULL );
226 :
227 : /* Kind of a hack but initializing NUMA config depends on shmem, which
228 : depends on logging, which depends on loading the config file, but
229 : we would like to do this as part of loading config ... the topo
230 : creation needs to be moved out of configuration and happen after
231 : boot. */
232 :
233 0 : if( FD_LIKELY( -1==config_fd ) ) {
234 0 : initialize_numa_assignments( &config->topo );
235 0 : }
236 0 : }
237 :
238 : static config_t config;
239 :
240 : int
241 : main1( int argc,
242 0 : char ** _argv ) {
243 0 : char ** argv = _argv;
244 0 : argc--; argv++;
245 :
246 : /* Short circuit evaluating help and version commands so that we don't
247 : need to load and evaluate the entire config file to run them.
248 : This is useful for some operators in CI environments where, for
249 : example, they want to show the version or validate the produced
250 : binary without yet setting up the full TOML. */
251 :
252 0 : if( FD_UNLIKELY( argc==1 && (!strcmp( argv[ 0 ], "help" ) || !strcmp( argv[ 0 ], "--help" )) ) ) {
253 0 : help_cmd_fn( NULL, NULL );
254 0 : return 0;
255 0 : } else if( FD_UNLIKELY( argc==1 && (!strcmp( argv[ 0 ], "version" ) || !strcmp( argv[ 0 ], "--version" )) ) ) {
256 0 : version_cmd_fn( NULL, NULL );
257 0 : return 0;
258 0 : }
259 :
260 0 : fdctl_boot( &argc, &argv, &config, NULL );
261 :
262 0 : if( FD_UNLIKELY( !argc ) ) {
263 0 : help_cmd_fn( NULL, &config );
264 0 : FD_LOG_ERR(( "no subcommand specified" ));
265 0 : }
266 :
267 0 : const char * command = argv[ 0 ];
268 0 : for ( ulong i=0; i <sizeof(ALIASES)/sizeof(ALIASES[ 0 ]); i++ ) {
269 0 : if( FD_UNLIKELY( !strcmp( argv[ 0 ], ALIASES[ i ].name ) ) ) {
270 0 : command = ALIASES[ i ].alias;
271 0 : break;
272 0 : }
273 0 : }
274 :
275 0 : action_t * action = NULL;
276 0 : for( ulong i=0; i<ACTIONS_CNT; i++ ) {
277 0 : if( FD_UNLIKELY( !strcmp( command, ACTIONS[ i ].name ) ) ) {
278 0 : action = &ACTIONS[ i ];
279 0 : break;
280 0 : }
281 0 : }
282 0 : if( FD_UNLIKELY( !action ) ) {
283 0 : help_cmd_fn( NULL, &config );
284 0 : FD_LOG_ERR(( "unknown subcommand `%s`", argv[ 0 ] ));
285 0 : }
286 :
287 0 : argc--; argv++;
288 :
289 0 : args_t args = {0};
290 0 : if( FD_LIKELY( action->args ) ) action->args( &argc, &argv, &args );
291 0 : if( FD_UNLIKELY( argc ) ) FD_LOG_ERR(( "unknown argument `%s`", argv[ 0 ] ));
292 :
293 : /* check if we are appropriate permissioned to run the desired command */
294 0 : if( FD_LIKELY( action->perm ) ) {
295 0 : fd_caps_ctx_t caps[1] = {0};
296 0 : action->perm( &args, caps, &config );
297 0 : if( FD_UNLIKELY( caps->err_cnt ) ) {
298 0 : for( ulong i=0; i<caps->err_cnt; i++ ) FD_LOG_WARNING(( "%s", caps->err[ i ] ));
299 0 : if( FD_LIKELY( !strcmp( action->name, "run" ) ) ) {
300 0 : FD_LOG_ERR(( "insufficient permissions to execute command `%s`. It is recommended "
301 0 : "to start Firedancer as the root user, but you can also start it "
302 0 : "with the missing capabilities listed above. The program only needs "
303 0 : "to start with elevated permissions to do privileged operations at "
304 0 : "boot, and will immediately drop permissions and switch to the user "
305 0 : "specified in your configuration file once they are complete. Firedancer "
306 0 : "will not execute outside of the boot process as root, and will refuse "
307 0 : "to start if it cannot drop privileges. Firedancer needs to be started "
308 0 : "privileged to configure high performance networking with XDP.", action->name ));
309 0 : } else if( FD_LIKELY( !strcmp( action->name, "configure" ) ) ) {
310 0 : FD_LOG_ERR(( "insufficient permissions to execute command `%s`. It is recommended "
311 0 : "to configure Firedancer as the root user. Firedancer configuration requires "
312 0 : "root because it does privileged operating system actions like mounting huge page filesystems. "
313 0 : "Configuration is a local action that does not access the network, and the process "
314 0 : "exits immediately once configuration completes. The user that Firedancer runs "
315 0 : "as is specified in your configuration file, and although configuration runs as root "
316 0 : "it will permission the relevant resources for the user in your configuration file, "
317 0 : "which can be an anonymous maximally restrictive account with no privileges.", action->name ));
318 0 : } else {
319 0 : FD_LOG_ERR(( "insufficient permissions to execute command `%s`", action->name ));
320 0 : }
321 0 : }
322 0 : }
323 :
324 : /* run the command */
325 0 : action->fn( &args, &config );
326 :
327 0 : return 0;
328 0 : }
|