Line data Source code
1 : #define _GNU_SOURCE
2 : #define FD_UNALIGNED_ACCESS_STYLE 0
3 : #include "../../util/bits/fd_bits.h"
4 :
5 : #include "fdctl.h"
6 :
7 : #include <fcntl.h>
8 : #include <sys/mman.h>
9 :
10 : action_t ACTIONS[ ACTIONS_CNT ] = {
11 : { .name = "run", .args = NULL, .fn = run_cmd_fn, .perm = run_cmd_perm, .description = "Start up a Firedancer validator" },
12 : { .name = "run1", .args = run1_cmd_args, .fn = run1_cmd_fn, .perm = NULL, .description = "Start up a single Firedancer tile" },
13 : { .name = "run-agave", .args = NULL, .fn = run_agave_cmd_fn, .perm = NULL, .description = "Start up the Agave side of a Firedancer validator" },
14 : { .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" },
15 : { .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" },
16 : { .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" },
17 : { .name = "ready", .args = NULL, .fn = ready_cmd_fn, .perm = NULL, .description = "Wait for all tiles to be running" },
18 : { .name = "mem", .args = NULL, .fn = mem_cmd_fn, .perm = NULL, .description = "Print workspace memory and tile topology information" },
19 : { .name = "spy", .args = NULL, .fn = spy_cmd_fn, .perm = NULL, .description = "Spy on and print out gossip traffic" },
20 : { .name = "help", .args = NULL, .fn = help_cmd_fn, .perm = NULL, .description = "Print this help message" },
21 : { .name = "version", .args = NULL, .fn = version_cmd_fn, .perm = NULL, .description = "Show the current software version" },
22 : };
23 :
24 : struct action_alias {
25 : const char * name;
26 : const char * alias;
27 : };
28 :
29 : struct action_alias ALIASES[] = {
30 : { .name = "info", .alias = "mem" },
31 : { .name = "topo", .alias = "mem" },
32 : };
33 :
34 : extern int * fd_log_private_shared_lock;
35 :
36 : static void
37 : copy_config_from_fd( int config_fd,
38 0 : config_t * config ) {
39 0 : uchar * bytes = mmap( NULL, sizeof( config_t ), PROT_READ, MAP_PRIVATE, config_fd, 0 );
40 0 : if( FD_UNLIKELY( bytes == MAP_FAILED ) ) FD_LOG_ERR(( "mmap() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
41 0 : fd_memcpy( config, bytes, sizeof( config_t ) );
42 0 : if( FD_UNLIKELY( munmap( bytes, sizeof( config_t ) ) ) ) FD_LOG_ERR(( "munmap() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
43 0 : if( FD_UNLIKELY( close( config_fd ) ) ) FD_LOG_ERR(( "close() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
44 0 : }
45 :
46 : static int *
47 0 : map_log_memfd( int log_memfd ) {
48 0 : void * shmem = mmap( NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, log_memfd, (off_t)0 );
49 0 : if( FD_UNLIKELY( shmem==MAP_FAILED ) ) {
50 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 ) ));
51 0 : } else {
52 0 : if( FD_UNLIKELY( mlock( shmem, 4096 ) ) ) {
53 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 ) ));
54 0 : }
55 0 : }
56 0 : return shmem;
57 0 : }
58 :
59 : /* Try to allocate an anonymous page of memory in a file descriptor
60 : (memfd) for fd_log_private_shared_lock such that the log can strictly
61 : sequence messages written by clones of the caller made after the
62 : caller has finished booting the log. Must be a file descriptor so
63 : we can pass it through `execve` calls. */
64 : static int
65 0 : init_log_memfd( void ) {
66 0 : int memfd = memfd_create( "fd_log_lock_page", 0U );
67 0 : if( FD_UNLIKELY( -1==memfd) ) FD_LOG_ERR(( "memfd_create(\"fd_log_lock_page\",0) failed (%i-%s)", errno, fd_io_strerror( errno ) ));
68 0 : if( FD_UNLIKELY( -1==ftruncate( memfd, 4096 ) ) ) FD_LOG_ERR(( "ftruncate(memfd,4096) failed (%i-%s)", errno, fd_io_strerror( errno ) ));
69 0 : return memfd;
70 0 : }
71 :
72 : static int
73 0 : should_colorize( void ) {
74 0 : char const * cstr = fd_env_strip_cmdline_cstr( NULL, NULL, NULL, "COLORTERM", NULL );
75 0 : if( cstr && !strcmp( cstr, "truecolor" ) ) return 1;
76 :
77 0 : cstr = fd_env_strip_cmdline_cstr( NULL, NULL, NULL, "TERM", NULL );
78 0 : if( cstr && !strcmp( cstr, "xterm-256color" ) ) return 1;
79 0 : return 0;
80 0 : }
81 :
82 : static void
83 0 : initialize_numa_assignments( fd_topo_t * topo ) {
84 : /* Assign workspaces to NUMA nodes. The heuristic here is pretty
85 : simple for now: workspaces go on the NUMA node of the first
86 : tile which maps the largest object in the workspace. */
87 :
88 0 : for( ulong i=0UL; i<topo->wksp_cnt; i++ ) {
89 0 : ulong max_footprint = 0UL;
90 0 : ulong max_obj = ULONG_MAX;
91 :
92 0 : for( ulong j=0UL; j<topo->obj_cnt; j++ ) {
93 0 : fd_topo_obj_t * obj = &topo->objs[ j ];
94 0 : if( obj->wksp_id!=i ) continue;
95 :
96 0 : if( FD_UNLIKELY( !max_footprint || obj->footprint>max_footprint ) ) {
97 0 : max_footprint = obj->footprint;
98 0 : max_obj = j;
99 0 : }
100 0 : }
101 :
102 0 : if( FD_UNLIKELY( max_obj==ULONG_MAX ) ) FD_LOG_ERR(( "no object found for workspace %s", topo->workspaces[ i ].name ));
103 :
104 0 : int found_strict = 0;
105 0 : int found_lazy = 1;
106 0 : for( ulong j=0UL; j<topo->tile_cnt; j++ ) {
107 0 : fd_topo_tile_t * tile = &topo->tiles[ j ];
108 0 : if( FD_UNLIKELY( tile->tile_obj_id==max_obj && tile->cpu_idx!=ULONG_MAX ) ) {
109 0 : topo->workspaces[ i ].numa_idx = fd_shmem_numa_idx( tile->cpu_idx );
110 0 : FD_TEST( topo->workspaces[ i ].numa_idx!=ULONG_MAX );
111 0 : found_strict = 1;
112 0 : found_lazy = 1;
113 0 : break;
114 0 : } else if( FD_UNLIKELY( tile->tile_obj_id==max_obj && tile->cpu_idx==ULONG_MAX ) ) {
115 0 : topo->workspaces[ i ].numa_idx = 0;
116 0 : found_lazy = 1;
117 0 : break;
118 0 : }
119 0 : }
120 :
121 0 : if( FD_LIKELY( !found_strict ) ) {
122 0 : for( ulong j=0UL; j<topo->tile_cnt; j++ ) {
123 0 : fd_topo_tile_t * tile = &topo->tiles[ j ];
124 0 : for( ulong k=0UL; k<tile->uses_obj_cnt; k++ ) {
125 0 : if( FD_LIKELY( tile->uses_obj_id[ k ]==max_obj && tile->cpu_idx!=ULONG_MAX ) ) {
126 0 : topo->workspaces[ i ].numa_idx = fd_shmem_numa_idx( tile->cpu_idx );
127 0 : FD_TEST( topo->workspaces[ i ].numa_idx!=ULONG_MAX );
128 0 : found_lazy = 1;
129 0 : break;
130 0 : } else if( FD_UNLIKELY( tile->uses_obj_id[ k ]==max_obj ) && tile->cpu_idx==ULONG_MAX ) {
131 0 : topo->workspaces[ i ].numa_idx = 0;
132 0 : found_lazy = 1;
133 : /* Don't break, keep looking -- a tile with a CPU assignment
134 : might also use object in which case we want to use that
135 : NUMA node. */
136 0 : }
137 0 : }
138 :
139 0 : if( FD_UNLIKELY( found_lazy ) ) break;
140 0 : }
141 0 : }
142 :
143 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 ));
144 0 : }
145 0 : }
146 :
147 : void
148 : fdctl_boot( int * pargc,
149 : char *** pargv,
150 : config_t * config,
151 0 : char const * log_path ) {
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 : }
|