Coverage Report

Created: 2025-08-29 06:24

/src/mosquitto/libcommon/file_common.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
Copyright (c) 2009-2021 Roger Light <roger@atchoo.org>
3
4
All rights reserved. This program and the accompanying materials
5
are made available under the terms of the Eclipse Public License 2.0
6
and Eclipse Distribution License v1.0 which accompany this distribution.
7
8
The Eclipse Public License is available at
9
   https://www.eclipse.org/legal/epl-2.0/
10
and the Eclipse Distribution License is available at
11
  http://www.eclipse.org/org/documents/edl-v10.php.
12
13
SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
14
15
Contributors:
16
   Roger Light - initial implementation and documentation.
17
*/
18
19
/* This contains general purpose utility functions that are not specific to
20
 * Mosquitto/MQTT features. */
21
22
#include "config.h"
23
24
#include <ctype.h>
25
#include <errno.h>
26
#include <limits.h>
27
#include <stdbool.h>
28
#include <stdio.h>
29
#include <stdlib.h>
30
#include <string.h>
31
32
#ifdef WIN32
33
#  include <winsock2.h>
34
#  include <aclapi.h>
35
#  include <io.h>
36
#  include <lmcons.h>
37
#  include <fcntl.h>
38
#  define PATH_MAX MAX_PATH
39
#else
40
#  include <sys/stat.h>
41
#  include <pwd.h>
42
#  include <grp.h>
43
#  include <unistd.h>
44
#  include <fcntl.h>
45
#endif
46
47
#include "mosquitto.h"
48
49
FILE *mosquitto_fopen(const char *path, const char *mode, bool restrict_read)
50
11.4k
{
51
#ifdef WIN32
52
  char buf[4096];
53
  int rc;
54
  int flags = 0;
55
56
  rc = ExpandEnvironmentStringsA(path, buf, 4096);
57
  if(rc == 0 || rc > 4096){
58
    return NULL;
59
  }else{
60
    if(restrict_read){
61
      HANDLE hfile;
62
      SECURITY_ATTRIBUTES sec;
63
      EXPLICIT_ACCESS_A ea;
64
      PACL pacl = NULL;
65
      char username[UNLEN + 1];
66
      DWORD ulen = UNLEN;
67
      SECURITY_DESCRIPTOR sd;
68
      DWORD dwCreationDisposition;
69
      DWORD dwShareMode;
70
      int fd;
71
      FILE *fptr;
72
73
      switch(mode[0]){
74
        case 'a':
75
          dwCreationDisposition = OPEN_ALWAYS;
76
          dwShareMode = GENERIC_WRITE;
77
          flags = _O_APPEND;
78
          break;
79
        case 'r':
80
          dwCreationDisposition = OPEN_EXISTING;
81
          dwShareMode = GENERIC_READ;
82
          flags = _O_RDONLY;
83
          break;
84
        case 'w':
85
          dwCreationDisposition = CREATE_ALWAYS;
86
          dwShareMode = GENERIC_WRITE;
87
          break;
88
        default:
89
          return NULL;
90
      }
91
      if(mode[1] == '+'){
92
        dwShareMode = GENERIC_READ | GENERIC_WRITE;
93
      }
94
95
      GetUserNameA(username, &ulen);
96
      if(!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)){
97
        return NULL;
98
      }
99
      BuildExplicitAccessWithNameA(&ea, username, GENERIC_ALL, SET_ACCESS, NO_INHERITANCE);
100
      if(SetEntriesInAclA(1, &ea, NULL, &pacl) != ERROR_SUCCESS){
101
        return NULL;
102
      }
103
      if(!SetSecurityDescriptorDacl(&sd, TRUE, pacl, FALSE)){
104
        LocalFree(pacl);
105
        return NULL;
106
      }
107
108
      memset(&sec, 0, sizeof(sec));
109
      sec.nLength = sizeof(SECURITY_ATTRIBUTES);
110
      sec.bInheritHandle = FALSE;
111
      sec.lpSecurityDescriptor = &sd;
112
113
      hfile = CreateFileA(buf, dwShareMode, FILE_SHARE_READ,
114
        &sec,
115
        dwCreationDisposition,
116
        FILE_ATTRIBUTE_NORMAL,
117
        NULL);
118
119
      LocalFree(pacl);
120
121
      fd = _open_osfhandle((intptr_t)hfile, flags);
122
      if(fd < 0){
123
        return NULL;
124
      }
125
126
      fptr = _fdopen(fd, mode);
127
      if(!fptr){
128
        _close(fd);
129
        return NULL;
130
      }
131
      if(mode[0] == 'a'){
132
        fseek(fptr, 0, SEEK_END);
133
      }
134
      return fptr;
135
136
    }else {
137
      return fopen(buf, mode);
138
    }
139
  }
140
#else
141
11.4k
  FILE *fptr;
142
11.4k
  struct stat statbuf;
143
144
11.4k
  if(restrict_read){
145
3.24k
    mode_t old_mask;
146
147
3.24k
    old_mask = umask(0077);
148
149
3.24k
    int open_flags = O_NOFOLLOW;
150
9.95k
    for(size_t i = 0; i<strlen(mode); i++){
151
6.70k
      if(mode[i] == 'r'){
152
3.24k
        open_flags |= O_RDONLY;
153
3.46k
      }else if(mode[i] == 'w'){
154
1
        open_flags |= O_WRONLY;
155
1
        open_flags |= (O_TRUNC | O_CREAT | O_EXCL);
156
157
3.46k
      }else if(mode[i] == 'a'){
158
0
        open_flags |= O_WRONLY;
159
0
        open_flags |= (O_APPEND | O_CREAT);
160
3.46k
      }else if(mode[i] == 't'){
161
3.24k
      }else if(mode[i] == 'b'){
162
223
      }else if(mode[i] == '+'){
163
223
        open_flags |= O_RDWR;
164
223
      }
165
6.70k
    }
166
3.24k
    int fd = open(path, open_flags, 0600);
167
3.24k
    if(fd < 0) return NULL;
168
3.24k
    fptr = fdopen(fd, mode);
169
170
3.24k
    umask(old_mask);
171
8.16k
  }else{
172
8.16k
    fptr = fopen(path, mode);
173
8.16k
  }
174
11.4k
  if(!fptr) return NULL;
175
176
11.4k
  if(fstat(fileno(fptr), &statbuf) < 0){
177
0
    fclose(fptr);
178
0
    return NULL;
179
0
  }
180
181
11.4k
  if(restrict_read){
182
3.24k
    if(statbuf.st_mode & S_IRWXO){
183
0
      fprintf(stderr,
184
0
          "Warning: File %s has world readable permissions. Future versions will refuse to load this file.\n"
185
0
          "To fix this, use `chmod 0700 %s`.\n",
186
0
          path, path);
187
#if 0
188
      return NULL;
189
#endif
190
0
    }
191
3.24k
    if(statbuf.st_uid != getuid()){
192
0
      char buf[4096];
193
0
      struct passwd pw, *result;
194
195
0
      getpwuid_r(getuid(), &pw, buf, sizeof(buf), &result);
196
0
      if(result){
197
0
        fprintf(stderr,
198
0
            "Warning: File %s owner is not %s. Future versions will refuse to load this file."
199
0
            "To fix this, use `chown %s %s`.\n",
200
0
            path, result->pw_name, result->pw_name, path);
201
0
      }
202
#if 0
203
      // Future version
204
      return NULL;
205
#endif
206
0
    }
207
3.24k
    if(statbuf.st_gid != getgid()){
208
0
      char buf[4096];
209
0
      struct group grp, *result;
210
211
0
      if(getgrgid_r(getgid(), &grp, buf, sizeof(buf), &result) == 0){
212
0
        fprintf(stderr,
213
0
            "Warning: File %s group is not %s. Future versions will refuse to load this file.\n",
214
0
            path, result->gr_name);
215
0
      }
216
#if 0
217
      // Future version
218
      return NULL
219
#endif
220
0
    }
221
3.24k
  }
222
223
11.4k
  if(!S_ISREG(statbuf.st_mode)){
224
0
    fprintf(stderr, "Error: %s is not a file.", path);
225
0
    fclose(fptr);
226
0
    return NULL;
227
0
  }
228
11.4k
  return fptr;
229
11.4k
#endif
230
11.4k
}
231
232
233
char *mosquitto_trimblanks(char *str)
234
347k
{
235
347k
  char *endptr;
236
237
347k
  if(str == NULL) return NULL;
238
239
347k
  while(isspace(str[0])){
240
67.7k
    str++;
241
67.7k
  }
242
347k
  endptr = &str[strlen(str)-1];
243
376k
  while(endptr > str && isspace(endptr[0])){
244
28.0k
    endptr[0] = '\0';
245
28.0k
    endptr--;
246
28.0k
  }
247
347k
  return str;
248
347k
}
249
250
251
char *mosquitto_fgets(char **buf, int *buflen, FILE *stream)
252
983k
{
253
983k
  char *rc;
254
983k
  char endchar;
255
983k
  int offset = 0;
256
983k
  char *newbuf;
257
983k
  size_t len;
258
259
983k
  if(stream == NULL || buf == NULL || buflen == NULL || *buflen < 1){
260
0
    return NULL;
261
0
  }
262
263
1.03M
  do{
264
1.03M
    rc = fgets(&((*buf)[offset]), (*buflen)-offset, stream);
265
1.03M
    if(feof(stream) || rc == NULL){
266
11.9k
      return rc;
267
11.9k
    }
268
269
1.02M
    len = strlen(*buf);
270
1.02M
    if(len == 0){
271
1.59k
      return rc;
272
1.59k
    }
273
1.02M
    endchar = (*buf)[len-1];
274
1.02M
    if(endchar == '\n'){
275
964k
      return rc;
276
964k
    }
277
56.8k
    if((int)(len+1) < *buflen){
278
      /* Embedded nulls, invalid string */
279
5.82k
      return NULL;
280
5.82k
    }
281
282
    /* No EOL char found, so extend buffer */
283
51.0k
    offset = (*buflen)-1;
284
51.0k
    *buflen += 1000;
285
51.0k
    newbuf = realloc(*buf, (size_t)*buflen);
286
51.0k
    if(!newbuf){
287
0
      return NULL;
288
0
    }
289
51.0k
    *buf = newbuf;
290
51.0k
  }while(1);
291
983k
}
292
293
294
#define INVOKE_LOG_FN(format, ...) \
295
0
  do{ \
296
0
    if(log_fn){ \
297
0
      int tmp_err_no = errno; \
298
0
      char msg[2*PATH_MAX]; \
299
0
      snprintf(msg, sizeof(msg), (format), __VA_ARGS__); \
300
0
      msg[sizeof(msg)-1] = '\0'; \
301
0
      (*log_fn)(msg); \
302
0
      errno = tmp_err_no; \
303
0
    } \
304
0
  }while (0)
305
306
int mosquitto_write_file(const char* target_path, bool restrict_read, int (*write_fn)(FILE* fptr, void* user_data), void* user_data, void (*log_fn)(const char* msg))
307
1
{
308
1
  int rc = 0;
309
1
  FILE *fptr = NULL;
310
1
  char tmp_file_path[PATH_MAX];
311
312
1
  if(!target_path || !write_fn){
313
0
    return MOSQ_ERR_INVAL;
314
0
  }
315
316
1
  rc = snprintf(tmp_file_path, PATH_MAX, "%s.new", target_path);
317
1
  if(rc < 0 || rc >= PATH_MAX){
318
0
    return MOSQ_ERR_INVAL;
319
0
  }
320
321
1
#ifndef WIN32
322
  /**
323
  *
324
  * If a system lost power during the rename operation at the
325
  * end of this file the filesystem could potentially be left
326
  * with a directory that looks like this after powerup:
327
  *
328
  * 24094 -rw-r--r--    2 root     root          4099 May 30 16:27 mosquitto.db
329
  * 24094 -rw-r--r--    2 root     root          4099 May 30 16:27 mosquitto.db.new
330
  *
331
  * The 24094 shows that mosquitto.db.new is hard-linked to the
332
  * same file as mosquitto.db.  If fopen(outfile, "wb") is naively
333
  * called then mosquitto.db will be truncated and the database
334
  * potentially corrupted.
335
  *
336
  * Any existing mosquitto.db.new file must be removed prior to
337
  * opening to guarantee that it is not hard-linked to
338
  * mosquitto.db.
339
  *
340
  */
341
1
  if(unlink(tmp_file_path) && errno != ENOENT){
342
0
    INVOKE_LOG_FN("unable to remove stale tmp file %s, error %s", tmp_file_path, strerror(errno));
343
0
    return MOSQ_ERR_INVAL;
344
0
  }
345
1
#endif
346
347
1
  fptr = mosquitto_fopen(tmp_file_path, "wb", restrict_read);
348
1
  if(fptr == NULL){
349
0
    INVOKE_LOG_FN("unable to open %s for writing, error %s", tmp_file_path, strerror(errno));
350
0
    return MOSQ_ERR_INVAL;
351
0
  }
352
353
1
  if((rc = (*write_fn)(fptr, user_data)) != MOSQ_ERR_SUCCESS){
354
0
    goto error;
355
0
  }
356
357
1
  rc = MOSQ_ERR_ERRNO;
358
1
#ifndef WIN32
359
  /**
360
  *
361
  * Closing a file does not guarantee that the contents are
362
  * written to disk.  Need to flush to send data from app to OS
363
  * buffers, then fsync to deliver data from OS buffers to disk
364
  * (as well as disk hardware permits).
365
  *
366
  * man close (http://linux.die.net/man/2/close, 2016-06-20):
367
  *
368
  *   "successful close does not guarantee that the data has
369
  *   been successfully saved to disk, as the kernel defers
370
  *   writes.  It is not common for a filesystem to flush
371
  *   the  buffers  when  the stream is closed.  If you need
372
  *   to be sure that the data is physically stored, use
373
  *   fsync(2).  (It will depend on the disk hardware at this
374
  *   point."
375
  *
376
  * This guarantees that the new state file will not overwrite
377
  * the old state file before its contents are valid.
378
  *
379
  */
380
1
  if(fflush(fptr) != 0 && errno != EINTR){
381
0
    INVOKE_LOG_FN("unable to flush %s, error %s", tmp_file_path, strerror(errno));
382
0
    goto error;
383
0
  }
384
385
1
  if(fsync(fileno(fptr)) != 0 && errno != EINTR){
386
0
    INVOKE_LOG_FN("unable to sync %s to disk, error %s", tmp_file_path, strerror(errno));
387
0
    goto error;
388
0
  }
389
1
#endif
390
391
1
  if(fclose(fptr) != 0){
392
0
    INVOKE_LOG_FN("unable to close %s on disk, error %s", tmp_file_path, strerror(errno));
393
0
    fptr = NULL;
394
0
    goto error;
395
0
  }
396
1
  fptr = NULL;
397
398
#ifdef WIN32
399
  if(remove(target_path) != 0 && errno != ENOENT){
400
    INVOKE_LOG_FN("unable to remove %s on disk, error %s", target_path, strerror(errno));
401
    goto error;
402
  }
403
#endif
404
405
1
  if(rename(tmp_file_path, target_path) != 0){
406
0
    INVOKE_LOG_FN("unable to replace %s by tmp file  %s, error %s", target_path, tmp_file_path, strerror(errno));
407
0
    goto error;
408
0
  }
409
1
  return MOSQ_ERR_SUCCESS;
410
411
0
error:
412
0
  if(fptr){
413
0
    fclose(fptr);
414
0
    unlink(tmp_file_path);
415
0
  }
416
0
  return MOSQ_ERR_ERRNO;
417
1
}
418
419
420
int mosquitto_read_file(const char *file, char **buf, size_t *buflen)
421
0
{
422
0
  FILE *fptr;
423
0
  long l;
424
0
  size_t buflen_i;
425
426
0
  *buf = NULL;
427
0
  fptr = fopen(file, "rt");
428
0
  if(fptr == NULL){
429
0
    return MOSQ_ERR_ERRNO;
430
0
  }
431
432
0
  fseek(fptr, 0, SEEK_END);
433
0
  l = ftell(fptr);
434
0
  fseek(fptr, 0, SEEK_SET);
435
0
  if(l < 0){
436
0
    fclose(fptr);
437
0
    return MOSQ_ERR_ERRNO;
438
0
  }else if(l == 0){
439
0
    *buf = NULL;
440
0
    if(buflen){
441
0
      *buflen = 0;
442
0
    }
443
0
    fclose(fptr);
444
0
    return MOSQ_ERR_SUCCESS;
445
0
  }
446
0
  buflen_i = (size_t)l;
447
448
0
  *buf = mosquitto_calloc(buflen_i+1, sizeof(char));
449
0
  if((*buf) == NULL){
450
0
    fclose(fptr);
451
0
    return MOSQ_ERR_NOMEM;
452
0
  }
453
0
  if(fread(*buf, 1, buflen_i, fptr) != buflen_i){
454
0
    mosquitto_FREE(*buf);
455
0
    fclose(fptr);
456
0
    return MOSQ_ERR_INVAL;
457
0
  }
458
0
  fclose(fptr);
459
0
  if(buflen){
460
0
    *buflen = buflen_i;
461
0
  }
462
463
0
  return MOSQ_ERR_SUCCESS;
464
0
}