3434static bool print_log = true, print_stdout = true;
3535bool debug_flags [DEBUG_MAX ] = { false };
3636
37- // Per-file log state: fd, path, writer-preferenced lock, reopen flag
37+ // Per-file log state: fd, path (owned copy) , writer-preferenced lock, reopen flag
3838struct log_fd {
3939 int fd ;
40- const char * path ;
40+ char * path ;
4141 pthread_mutex_t lock ;
4242 volatile sig_atomic_t reopen_needed ;
4343};
@@ -70,8 +70,9 @@ static void log_atfork_child(void)
7070}
7171
7272// Return 1 if this fd is associated with any logfile to avoid
73- // dnsmasq closing it during initialization
74- int __attribute__((pure )) is_log_fd (const int fd )
73+ // dnsmasq closing it during initialization.
74+ // Not marked pure: the descriptors are reassigned on reopen from another thread.
75+ int is_log_fd (const int fd )
7576{
7677 return fd == ftl_log .fd || fd == webserver_log .fd || fd == dnsmasq_log .fd ;
7778}
@@ -137,6 +138,17 @@ void log_ctrl(bool plog, bool pstdout)
137138 print_stdout = pstdout ;
138139}
139140
141+ // Set a log_fd path from a config string. The path is duplicated so
142+ // that a config replacement (free_config + memcpy) cannot leave a
143+ // dangling pointer in the reopen path.
144+ static void set_log_path (struct log_fd * log , const char * path )
145+ {
146+ if (log -> path != NULL && path != NULL && strcmp (log -> path , path ) == 0 )
147+ return ; // unchanged
148+ free (log -> path );
149+ log -> path = path != NULL ? strdup (path ) : NULL ;
150+ }
151+
140152// Open cached log fds from config paths.
141153// open_log_fds(true): open FTL.log only (called early, before full config)
142154// open_log_fds(false): open webserver.log + pihole.log (called after config)
@@ -147,7 +159,7 @@ void open_log_fds(bool ftl)
147159 // FTL.log - path is known from getLogFilePath()
148160 if (config .files .log .ftl .v .s != NULL )
149161 {
150- ftl_log . path = config .files .log .ftl .v .s ;
162+ set_log_path ( & ftl_log , config .files .log .ftl .v .s ) ;
151163 ftl_log .fd = open (ftl_log .path , O_WRONLY |O_CREAT |O_APPEND |O_CLOEXEC , S_IRUSR |S_IWUSR |S_IRGRP );
152164 if (ftl_log .fd == -1 )
153165 {
@@ -162,7 +174,7 @@ void open_log_fds(bool ftl)
162174 // webserver.log + pihole.log - paths are known after readFTLconf()
163175 if (config .files .log .webserver .v .s != NULL )
164176 {
165- webserver_log . path = config .files .log .webserver .v .s ;
177+ set_log_path ( & webserver_log , config .files .log .webserver .v .s ) ;
166178 webserver_log .fd = open (webserver_log .path , O_WRONLY |O_CREAT |O_APPEND |O_CLOEXEC , S_IRUSR |S_IWUSR |S_IRGRP );
167179 if (webserver_log .fd == -1 )
168180 {
@@ -174,7 +186,7 @@ void open_log_fds(bool ftl)
174186 // pihole.log (dnsmasq) - FTL owns this file from now on
175187 if (config .files .log .dnsmasq .v .s != NULL )
176188 {
177- dnsmasq_log . path = config .files .log .dnsmasq .v .s ;
189+ set_log_path ( & dnsmasq_log , config .files .log .dnsmasq .v .s ) ;
178190 dnsmasq_log .fd = open (dnsmasq_log .path , O_WRONLY |O_CREAT |O_APPEND |O_CLOEXEC , S_IRUSR |S_IWUSR |S_IRGRP );
179191 if (dnsmasq_log .fd == -1 )
180192 {
@@ -190,7 +202,10 @@ void open_log_fds(bool ftl)
190202 }
191203
192204 // Register atfork handlers once, before any threads or dnsmasq forks
193- // exist, so a TCP-query fork can never inherit a locked log mutex
205+ // exist, so a TCP-query fork can never inherit a locked log mutex.
206+ // Invariant: fork() is never called from inside a log write, so the
207+ // atfork prepare/parent/child handlers only need to cover the case
208+ // where a thread holds a log mutex at the moment of the fork.
194209 static bool atfork_registered = false;
195210 if (!atfork_registered )
196211 {
@@ -400,36 +415,45 @@ const char *debugstr(const enum debug_flag flag)
400415// "Jan 1 12:00:00 dnsmasq-dhcp[12345]: <message>\n"
401416// where the func suffix (e.g. "-dhcp", "-tftp") comes from the priority
402417// bits extracted in my_syslog().
403- void FTL_write_dnsmasq_log (const char * message , const char * func )
418+ bool FTL_write_dnsmasq_log (const char * message , const char * func )
404419{
405- // Locale-independent timestamp: ctime () renders the month/day in the
420+ // Locale-independent timestamp: ctime_r () renders the month/day in the
406421 // C locale regardless of setlocale(LC_ALL, ""), so the buffer cannot
407422 // overflow with non-English month names (strftime("%b") would emit
408- // e.g. six bytes for ru_RU). This is dnsmasq's own idiom and keeps
409- // the on-disk format byte-identical to what we wrote before.
423+ // e.g. six bytes for ru_RU). ctime_r() is reentrant, unlike ctime()
424+ // which returns a pointer to a static buffer shared with localtime()
425+ // and asctime() - critical since FTL_write_dnsmasq_log() runs on the
426+ // DNS thread while the webserver, database and NTP threads format their
427+ // own timestamps. This is dnsmasq's own idiom and keeps the on-disk
428+ // format byte-identical to what we wrote before.
410429 time_t now = time (NULL );
430+ char ctime_buf [26 ];
431+ char * ctime_str = ctime_r (& now , ctime_buf );
432+ if (ctime_str == NULL )
433+ ctime_str = "Jan 1 00:00:00 " ;
411434 char ts_buf [16 ];
412- snprintf (ts_buf , sizeof (ts_buf ), "%.15s" , ctime ( & now ) + 4 );
435+ snprintf (ts_buf , sizeof (ts_buf ), "%.15s" , ctime_str + 4 );
413436
414437 char line [2048 ];
415438 int off = snprintf (line , sizeof (line ), "%s dnsmasq%s[%d]: " , ts_buf , func ? func : "" , getpid ());
416439
417440 // Clamp before using off as an offset - snprintf returns the would-be
418- // length on truncation and sizeof(line) - off would underflow otherwise
419- if (off >= (int )sizeof (line ))
441+ // length on truncation and sizeof(line) - off would underflow otherwise;
442+ // it may also return negative on an encoding error
443+ if (off < 0 || off >= (int )sizeof (line ))
420444 off = sizeof (line ) - 1 ;
421445
422446 const char * msg = message ? message : "" ;
423447 off += snprintf (line + off , sizeof (line ) - off , "%s" , msg );
424448
425449 // Clamp to buffer end - snprintf returns would-be length on truncation
426- if (off >= (int )sizeof (line ))
450+ if (off < 0 || off >= (int )sizeof (line ))
427451 off = sizeof (line ) - 1 ;
428452
429453 if (off > 0 && line [off - 1 ] != '\n' )
430454 line [off ++ ] = '\n' ;
431455
432- write_log_line (& dnsmasq_log , line , off );
456+ return write_log_line (& dnsmasq_log , line , off );
433457}
434458
435459void __attribute__ ((format (printf , 3 , 4 ))) _FTL_log (const int priority , const enum debug_flag flag , const char * format , ...)
@@ -477,16 +501,17 @@ void __attribute__ ((format (printf, 3, 4))) _FTL_log(const int priority, const
477501 int off = snprintf (line , sizeof (line ), "%s [%s] %s: " , timestring , idstr , prio );
478502
479503 // Clamp before using off as an offset - snprintf returns the would-be
480- // length on truncation and sizeof(line) - off would underflow otherwise
481- if (off >= (int )sizeof (line ))
504+ // length on truncation and sizeof(line) - off would underflow otherwise;
505+ // it may also return negative on an encoding error
506+ if (off < 0 || off >= (int )sizeof (line ))
482507 off = sizeof (line ) - 1 ;
483508
484509 va_start (args , format );
485510 off += vsnprintf (line + off , sizeof (line ) - off , format , args );
486511 va_end (args );
487512
488513 // Clamp to buffer end - snprintf returns would-be length on truncation
489- if (off >= (int )sizeof (line ))
514+ if (off < 0 || off >= (int )sizeof (line ))
490515 off = sizeof (line ) - 1 ;
491516
492517 line [off ++ ] = '\n' ;
@@ -547,16 +572,17 @@ void __attribute__ ((format (printf, 3, 4))) _log_web(const int priority, const
547572 int off = snprintf (line , sizeof (line ), "%s [%s] %s: " , timestring , idstr , prio );
548573
549574 // Clamp before using off as an offset - snprintf returns the would-be
550- // length on truncation and sizeof(line) - off would underflow otherwise
551- if (off >= (int )sizeof (line ))
575+ // length on truncation and sizeof(line) - off would underflow otherwise;
576+ // it may also return negative on an encoding error
577+ if (off < 0 || off >= (int )sizeof (line ))
552578 off = sizeof (line ) - 1 ;
553579
554580 va_start (args , format );
555581 off += vsnprintf (line + off , sizeof (line ) - off , format , args );
556582 va_end (args );
557583
558584 // Clamp to buffer end - snprintf returns would-be length on truncation
559- if (off >= (int )sizeof (line ))
585+ if (off < 0 || off >= (int )sizeof (line ))
560586 off = sizeof (line ) - 1 ;
561587
562588 line [off ++ ] = '\n' ;
@@ -975,10 +1001,10 @@ bool flush_dnsmasq_log(void)
9751001 lock_shm ();
9761002
9771003 // Open file in write mode to truncate it
978- FILE * logfile = fopen (config . files . log . dnsmasq . v . s , "w" );
1004+ FILE * logfile = fopen (dnsmasq_log . path , "w" );
9791005 if (!logfile )
9801006 {
981- log_err ("Could not open log file %s for truncation: %s\n" , config . files . log . dnsmasq . v . s , strerror (errno ));
1007+ log_err ("Could not open log file %s for truncation: %s\n" , dnsmasq_log . path , strerror (errno ));
9821008 unlock_shm ();
9831009 return false;
9841010 }
@@ -989,6 +1015,8 @@ bool flush_dnsmasq_log(void)
9891015 if (dnsmasq_log .fd != -1 )
9901016 close (dnsmasq_log .fd );
9911017 dnsmasq_log .fd = open (dnsmasq_log .path , O_WRONLY |O_CREAT |O_APPEND |O_CLOEXEC , S_IRUSR |S_IWUSR |S_IRGRP );
1018+ if (dnsmasq_log .fd == -1 )
1019+ log_warn ("pihole.log reopen failed after flush (%s)" , strerror (errno ));
9921020 pthread_mutex_unlock (& dnsmasq_log .lock );
9931021
9941022 // Flush dnsmasq FIFO logs
0 commit comments