Coverage Report

Created: 2023-09-19 06:58

/src/mosquitto/common/misc_mosq.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 "misc_mosq.h"
48
#include "logging_mosq.h"
49
50
51
FILE *mosquitto__fopen(const char *path, const char *mode, bool restrict_read)
52
8.53k
{
53
#ifdef WIN32
54
  char buf[4096];
55
  int rc;
56
  int flags = 0;
57
58
  rc = ExpandEnvironmentStringsA(path, buf, 4096);
59
  if(rc == 0 || rc > 4096){
60
    return NULL;
61
  }else{
62
    if(restrict_read){
63
      HANDLE hfile;
64
      SECURITY_ATTRIBUTES sec;
65
      EXPLICIT_ACCESS_A ea;
66
      PACL pacl = NULL;
67
      char username[UNLEN + 1];
68
      DWORD ulen = UNLEN;
69
      SECURITY_DESCRIPTOR sd;
70
      DWORD dwCreationDisposition;
71
      int fd;
72
      FILE *fptr;
73
74
      switch(mode[0]){
75
        case 'a':
76
          dwCreationDisposition = OPEN_ALWAYS;
77
          flags = _O_APPEND;
78
          break;
79
        case 'r':
80
          dwCreationDisposition = OPEN_EXISTING;
81
          flags = _O_RDONLY;
82
          break;
83
        case 'w':
84
          dwCreationDisposition = CREATE_ALWAYS;
85
          break;
86
        default:
87
          return NULL;
88
      }
89
90
      GetUserNameA(username, &ulen);
91
      if(!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)){
92
        return NULL;
93
      }
94
      BuildExplicitAccessWithNameA(&ea, username, GENERIC_ALL, SET_ACCESS, NO_INHERITANCE);
95
      if(SetEntriesInAclA(1, &ea, NULL, &pacl) != ERROR_SUCCESS){
96
        return NULL;
97
      }
98
      if(!SetSecurityDescriptorDacl(&sd, TRUE, pacl, FALSE)){
99
        LocalFree(pacl);
100
        return NULL;
101
      }
102
103
      memset(&sec, 0, sizeof(sec));
104
      sec.nLength = sizeof(SECURITY_ATTRIBUTES);
105
      sec.bInheritHandle = FALSE;
106
      sec.lpSecurityDescriptor = &sd;
107
108
      hfile = CreateFileA(buf, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ,
109
        &sec,
110
        dwCreationDisposition,
111
        FILE_ATTRIBUTE_NORMAL,
112
        NULL);
113
114
      LocalFree(pacl);
115
116
      fd = _open_osfhandle((intptr_t)hfile, flags);
117
      if(fd < 0){
118
        return NULL;
119
      }
120
121
      fptr = _fdopen(fd, mode);
122
      if(!fptr){
123
        _close(fd);
124
        return NULL;
125
      }
126
      if(mode[0] == 'a'){
127
        fseek(fptr, 0, SEEK_END);
128
      }
129
      return fptr;
130
131
    }else {
132
      return fopen(buf, mode);
133
    }
134
  }
135
#else
136
8.53k
  FILE *fptr;
137
8.53k
  struct stat statbuf;
138
139
8.53k
  if(restrict_read){
140
221
    mode_t old_mask;
141
142
221
    old_mask = umask(0077);
143
144
221
    int open_flags = O_NOFOLLOW;
145
884
    for(size_t i = 0; i<strlen(mode); i++){
146
663
      if(mode[i] == 'r'){
147
221
        open_flags |= O_RDONLY;
148
442
      }else if(mode[i] == 'w'){
149
0
        open_flags |= O_WRONLY;
150
0
        open_flags |= (O_TRUNC | O_CREAT | O_EXCL);
151
152
442
      }else if(mode[i] == 'a'){
153
0
        open_flags |= O_WRONLY;
154
0
        open_flags |= (O_APPEND | O_CREAT);
155
442
      }else if(mode[i] == 't'){
156
221
      }else if(mode[i] == 'b'){
157
221
      }else if(mode[i] == '+'){
158
221
        open_flags |= O_RDWR;
159
221
      }
160
663
    }
161
221
    int fd = open(path, open_flags, 0600);
162
221
    if(fd < 0) return NULL;
163
221
    fptr = fdopen(fd, mode);
164
165
221
    umask(old_mask);
166
8.31k
  }else{
167
8.31k
    fptr = fopen(path, mode);
168
8.31k
  }
169
8.53k
  if(!fptr) return NULL;
170
171
8.53k
  if(fstat(fileno(fptr), &statbuf) < 0){
172
0
    fclose(fptr);
173
0
    return NULL;
174
0
  }
175
176
8.53k
  if(restrict_read){
177
221
    if(statbuf.st_mode & S_IRWXO){
178
1
#ifdef WITH_BROKER
179
1
      log__printf(NULL, MOSQ_LOG_WARNING,
180
#else
181
      fprintf(stderr,
182
#endif
183
1
          "Warning: File %s has world readable permissions. Future versions will refuse to load this file.",
184
1
          path);
185
#if 0
186
      return NULL;
187
#endif
188
1
    }
189
221
    if(statbuf.st_uid != getuid()){
190
0
      char buf[4096];
191
0
      struct passwd pw, *result;
192
193
0
      getpwuid_r(getuid(), &pw, buf, sizeof(buf), &result);
194
0
      if(result){
195
0
#ifdef WITH_BROKER
196
0
        log__printf(NULL, MOSQ_LOG_WARNING,
197
#else
198
        fprintf(stderr,
199
#endif
200
0
            "Warning: File %s owner is not %s. Future versions will refuse to load this file.",
201
0
            path, result->pw_name);
202
0
      }
203
#if 0
204
      // Future version
205
      return NULL;
206
#endif
207
0
    }
208
221
    if(statbuf.st_gid != getgid()){
209
0
      char buf[4096];
210
0
      struct group grp, *result;
211
212
0
      getgrgid_r(getgid(), &grp, buf, sizeof(buf), &result);
213
0
      if(result){
214
0
#ifdef WITH_BROKER
215
0
        log__printf(NULL, MOSQ_LOG_WARNING,
216
#else
217
        fprintf(stderr,
218
#endif
219
0
            "Warning: File %s group is not %s. Future versions will refuse to load this file.",
220
0
            path, result->gr_name);
221
0
      }
222
#if 0
223
      // Future version
224
      return NULL
225
#endif
226
0
    }
227
221
  }
228
229
230
8.53k
  if(!S_ISREG(statbuf.st_mode) && !S_ISLNK(statbuf.st_mode)){
231
0
#ifdef WITH_BROKER
232
0
    log__printf(NULL, MOSQ_LOG_ERR, "Error: %s is not a file.", path);
233
0
#endif
234
0
    fclose(fptr);
235
0
    return NULL;
236
0
  }
237
8.53k
  return fptr;
238
8.53k
#endif
239
8.53k
}
240
241
242
char *misc__trimblanks(char *str)
243
39.3k
{
244
39.3k
  char *endptr;
245
246
39.3k
  if(str == NULL) return NULL;
247
248
39.3k
  while(isspace(str[0])){
249
232
    str++;
250
232
  }
251
39.3k
  endptr = &str[strlen(str)-1];
252
59.0k
  while(endptr > str && isspace(endptr[0])){
253
19.7k
    endptr[0] = '\0';
254
19.7k
    endptr--;
255
19.7k
  }
256
39.3k
  return str;
257
39.3k
}
258
259
260
char *fgets_extending(char **buf, int *buflen, FILE *stream)
261
28.1k
{
262
28.1k
  char *rc;
263
28.1k
  char endchar;
264
28.1k
  int offset = 0;
265
28.1k
  char *newbuf;
266
28.1k
  size_t len;
267
268
28.1k
  if(stream == NULL || buf == NULL || buflen == NULL || *buflen < 1){
269
0
    return NULL;
270
0
  }
271
272
36.6k
  do{
273
36.6k
    rc = fgets(&((*buf)[offset]), (*buflen)-offset, stream);
274
36.6k
    if(feof(stream) || rc == NULL){
275
8.62k
      return rc;
276
8.62k
    }
277
278
28.0k
    len = strlen(*buf);
279
28.0k
    if(len == 0){
280
17
      return rc;
281
17
    }
282
28.0k
    endchar = (*buf)[len-1];
283
28.0k
    if(endchar == '\n'){
284
19.5k
      return rc;
285
19.5k
    }
286
    /* No EOL char found, so extend buffer */
287
8.50k
    offset = (*buflen)-1;
288
8.50k
    *buflen += 1000;
289
8.50k
    newbuf = realloc(*buf, (size_t)*buflen);
290
8.50k
    if(!newbuf){
291
0
      return NULL;
292
0
    }
293
8.50k
    *buf = newbuf;
294
8.50k
  }while(1);
295
28.1k
}
296
297
298
#define INVOKE_LOG_FN(format, ...) \
299
0
  do{ \
300
0
    if(log_fn){ \
301
0
      int tmp_err_no = errno; \
302
0
      char msg[2*PATH_MAX]; \
303
0
      snprintf(msg, sizeof(msg), (format), __VA_ARGS__); \
304
0
      msg[sizeof(msg)-1] = '\0'; \
305
0
      (*log_fn)(msg); \
306
0
      errno = tmp_err_no; \
307
0
    } \
308
0
  }while (0)
309
310
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))
311
0
{
312
0
  int rc = 0;
313
0
  FILE *fptr = NULL;
314
0
  char tmp_file_path[PATH_MAX];
315
316
0
  if(!target_path || !write_fn){
317
0
    return MOSQ_ERR_INVAL;
318
0
  }
319
320
0
  rc = snprintf(tmp_file_path, PATH_MAX, "%s.new", target_path);
321
0
  if(rc < 0 || rc >= PATH_MAX){
322
0
    return MOSQ_ERR_INVAL;
323
0
  }
324
325
0
#ifndef WIN32
326
  /**
327
  *
328
  * If a system lost power during the rename operation at the
329
  * end of this file the filesystem could potentially be left
330
  * with a directory that looks like this after powerup:
331
  *
332
  * 24094 -rw-r--r--    2 root     root          4099 May 30 16:27 mosquitto.db
333
  * 24094 -rw-r--r--    2 root     root          4099 May 30 16:27 mosquitto.db.new
334
  *
335
  * The 24094 shows that mosquitto.db.new is hard-linked to the
336
  * same file as mosquitto.db.  If fopen(outfile, "wb") is naively
337
  * called then mosquitto.db will be truncated and the database
338
  * potentially corrupted.
339
  *
340
  * Any existing mosquitto.db.new file must be removed prior to
341
  * opening to guarantee that it is not hard-linked to
342
  * mosquitto.db.
343
  *
344
  */
345
0
  if(unlink(tmp_file_path) && errno != ENOENT){
346
0
    INVOKE_LOG_FN("unable to remove stale tmp file %s, error %s", tmp_file_path, strerror(errno));
347
0
    return MOSQ_ERR_INVAL;
348
0
  }
349
0
#endif
350
351
0
  fptr = mosquitto__fopen(tmp_file_path, "wb", restrict_read);
352
0
  if(fptr == NULL){
353
0
    INVOKE_LOG_FN("unable to open %s for writing, error %s", tmp_file_path, strerror(errno));
354
0
    return MOSQ_ERR_INVAL;
355
0
  }
356
357
0
  if((rc = (*write_fn)(fptr, user_data)) != MOSQ_ERR_SUCCESS){
358
0
    goto error;
359
0
  }
360
361
0
  rc = MOSQ_ERR_ERRNO;
362
0
#ifndef WIN32
363
  /**
364
  *
365
  * Closing a file does not guarantee that the contents are
366
  * written to disk.  Need to flush to send data from app to OS
367
  * buffers, then fsync to deliver data from OS buffers to disk
368
  * (as well as disk hardware permits).
369
  *
370
  * man close (http://linux.die.net/man/2/close, 2016-06-20):
371
  *
372
  *   "successful close does not guarantee that the data has
373
  *   been successfully saved to disk, as the kernel defers
374
  *   writes.  It is not common for a filesystem to flush
375
  *   the  buffers  when  the stream is closed.  If you need
376
  *   to be sure that the data is physically stored, use
377
  *   fsync(2).  (It will depend on the disk hardware at this
378
  *   point."
379
  *
380
  * This guarantees that the new state file will not overwrite
381
  * the old state file before its contents are valid.
382
  *
383
  */
384
0
  if(fflush(fptr) != 0 && errno != EINTR){
385
0
    INVOKE_LOG_FN("unable to flush %s, error %s", tmp_file_path, strerror(errno));
386
0
    goto error;
387
0
  }
388
389
0
  if(fsync(fileno(fptr)) != 0 && errno != EINTR){
390
0
    INVOKE_LOG_FN("unable to sync %s to disk, error %s", tmp_file_path, strerror(errno));
391
0
    goto error;
392
0
  }
393
0
#endif
394
395
0
  if(fclose(fptr) != 0){
396
0
    INVOKE_LOG_FN("unable to close %s on disk, error %s", tmp_file_path, strerror(errno));
397
0
    fptr = NULL;
398
0
    goto error;
399
0
  }
400
0
  fptr = NULL;
401
402
#ifdef WIN32
403
  if(remove(target_path) != 0 && errno != ENOENT){
404
    INVOKE_LOG_FN("unable to remove %s on disk, error %s", target_path, strerror(errno));
405
    goto error;
406
  }
407
#endif
408
409
0
  if(rename(tmp_file_path, target_path) != 0){
410
0
    INVOKE_LOG_FN("unable to replace %s by tmp file  %s, error %s", target_path, tmp_file_path, strerror(errno));
411
0
    goto error;
412
0
  }
413
0
  return MOSQ_ERR_SUCCESS;
414
415
0
error:
416
0
  if(fptr){
417
0
    fclose(fptr);
418
0
    unlink(tmp_file_path);
419
0
  }
420
0
  return MOSQ_ERR_ERRNO;
421
0
}