Loading...
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <darwintest.h>
#include <darwintest_utils.h>

static char tmpfile_template[] = "/tmp/libc_test_fflushXXXXX";
#define BUFSZ 128
static char wrbuf[BUFSZ] = "";
static const size_t filesz = BUFSZ * 120;

static void
cleanup_tmp_file(void)
{
	(void)unlink(tmpfile_template);
}

static const char *
assert_empty_tmp_file(void)
{
	T_SETUPBEGIN;

	int tmpfd = mkstemp(tmpfile_template);
	T_ASSERT_POSIX_SUCCESS(tmpfd, "created tmp file at %s", tmpfile_template);
	T_ATEND(cleanup_tmp_file);
	close(tmpfd);

	T_SETUPEND;

	return tmpfile_template;
}

static const char *
assert_full_tmp_file(void)
{
	T_SETUPBEGIN;

	int tmpfd = mkstemp(tmpfile_template);
	T_ASSERT_POSIX_SUCCESS(tmpfd, "created tmp file at %s", tmpfile_template);
	T_ATEND(cleanup_tmp_file);

	/*
	 * Write a pattern of bytes into the file -- the lowercase alphabet,
	 * separated by newlines.
	 */
	for (size_t i = 0; i < BUFSZ; i++) {
		wrbuf[i] = 'a' + (i % 27);
		if (i % 27 == 26) {
			wrbuf[i] = '\n';
		}
	}
	for (size_t i = 0; i < filesz; i++) {
		ssize_t byteswr = 0;
		do {
			byteswr = write(tmpfd, wrbuf, BUFSZ);
		} while (byteswr == -1 && errno == EAGAIN);

		T_QUIET; T_ASSERT_POSIX_SUCCESS(byteswr, "wrote %d bytes to tmp file",
				BUFSZ);
		T_QUIET; T_ASSERT_EQ(byteswr, (ssize_t)BUFSZ,
				"wrote correct amount of bytes to tmp file");
	}

	close(tmpfd);

	T_SETUPEND;

	return tmpfile_template;
}

/*
 * Ensure that fflush on an input stream conforms to the SUSv3 definition, which
 * requires synchronizing the FILE position with the underlying file descriptor.
 */
T_DECL(fflush_input, "fflush on a read-only FILE resets fd offset")
{
	const char *tmpfile = assert_full_tmp_file();

	T_SETUPBEGIN;

	FILE *tmpf = fopen(tmpfile, "r");
	T_QUIET; T_WITH_ERRNO;
	T_ASSERT_NOTNULL(tmpf, "opened tmp file for reading");

	/*
	 * Move some way into the file.
	 */
	char buf[100] = "";
	size_t nread = fread(buf, sizeof(buf), 1, tmpf);
	T_ASSERT_EQ(nread, (size_t)1, "read correct number of items from FILE");
	char last_read_char = buf[sizeof(buf) - 1];

	off_t curoff = lseek(fileno(tmpf), 0, SEEK_CUR);
	T_ASSERT_GT(curoff, (off_t)0, "file offset should be non-zero");

	T_SETUPEND;

	/*
	 * fflush(3) to reset the fd back to the FILE offset.
	 */
	int ret = fflush(tmpf);
	T_ASSERT_POSIX_SUCCESS(ret, "fflush on read-only FILE");

	off_t flushoff = lseek(fileno(tmpf), 0, SEEK_CUR);
	T_ASSERT_EQ(flushoff, (off_t)sizeof(buf),
			"offset of file should be bytes read on FILE after fflush");

	/*
	 * Make sure the FILE is reading the right thing -- the next character
	 * should be one letter after the last byte read, from the last call to
	 * fread(3).
	 */
	char c = '\0';
	nread = fread(&c, sizeof(c), 1, tmpf);
	T_QUIET;
	T_ASSERT_EQ(nread, (size_t)1, "read correct number of items from FILE");

	/*
	 * The pattern in the file is the alphabet -- and this doesn't land on
	 * a newline.
	 */
	T_QUIET;
	T_ASSERT_NE((flushoff) % 27, (off_t)0,
			"previous offset shouldn't land on newline");
	T_QUIET;
	T_ASSERT_NE((flushoff + 1) % 27, (off_t)0,
			"current offset shouldn't land on newline");

	T_ASSERT_EQ(c, last_read_char + 1, "read correct byte after fflush");

	ret = fflush(tmpf);
	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "fflush on read-only FILE");

	flushoff = lseek(fileno(tmpf), 0, SEEK_CUR);
	T_ASSERT_EQ(flushoff, (off_t)(sizeof(buf) + sizeof(c)),
			"offset of file should be incremented after subsequent read");

	/*
	 * Use ungetc(3) to induce the optimized ungetc behavior in the FILE.
	 */
	int ugret = ungetc(c, tmpf);
	T_QUIET; T_ASSERT_NE(ugret, EOF, "ungetc after fflush");
	T_QUIET; T_ASSERT_EQ((char)ugret, c, "ungetc un-got the correct char");

	ret = fflush(tmpf);
	T_ASSERT_POSIX_SUCCESS(ret, "fflush after ungetc");
	flushoff = lseek(fileno(tmpf), 0, SEEK_CUR);
	T_ASSERT_EQ(flushoff, (off_t)sizeof(buf),
			"offset of file should be correct after ungetc and fflush");

	nread = fread(&c, sizeof(c), 1, tmpf);
	T_QUIET;
	T_ASSERT_EQ(nread, (size_t)1, "read correct number of items from FILE");
	T_ASSERT_EQ(c, last_read_char + 1,
			"read correct byte after ungetc and fflush");
}

/*
 * Try to trick fclose into not reporting an ENOSPC error from the underlying
 * descriptor in update mode.  Previous versions of Libc only flushed the FILE
 * if it was write-only.
 */

#if TARGET_OS_OSX
/*
 * Only macOS contains a version of hdiutil that can create disk images.
 */

#define DMGFILE "/tmp/test_fclose_enospc.dmg"
#define VOLNAME "test_fclose_enospc"
static const char *small_file = "/Volumes/" VOLNAME "/test.txt";

static void
cleanup_dmg(void)
{
	char *hdiutil_detach_argv[] = {
		"/usr/bin/hdiutil", "detach", "/Volumes/" VOLNAME, NULL,
	};
	pid_t hdiutil_detach = -1;
	int ret = dt_launch_tool(&hdiutil_detach, hdiutil_detach_argv, false, NULL,
			NULL);
	if (ret != -1) {
		int status = 0;
		(void)waitpid(hdiutil_detach, &status, 0);
	}
	(void)unlink(DMGFILE);
}

T_DECL(fclose_enospc, "ensure ENOSPC is preserved on fclose")
{
	T_SETUPBEGIN;

	/*
	 * Ensure a disk is available that will fill up and start returning ENOSPC.
	 *
	 * system(3) would be easier...
	 */
	char *hdiutil_argv[] = {
		"/usr/bin/hdiutil", "create", "-size", "5m", "-type", "UDIF",
		"-volname", VOLNAME, "-nospotlight", "-fs", "HFS+", DMGFILE, "-attach",
		NULL,
	};
	pid_t hdiutil_create = -1;
	int ret = dt_launch_tool(&hdiutil_create, hdiutil_argv, false, NULL, NULL);
	T_ASSERT_POSIX_SUCCESS(ret, "created and attached 5MB DMG");
	int status = 0;
	pid_t waited = waitpid(hdiutil_create, &status, 0);
	T_QUIET; T_ASSERT_EQ(waited, hdiutil_create,
			"should have waited for the process that was launched");
	T_QUIET;
	T_ASSERT_TRUE(WIFEXITED(status), "hdiutil should have exited");
	T_QUIET;
	T_ASSERT_EQ(WEXITSTATUS(status), 0,
			"hdiutil should have exited successfully");

	T_ATEND(cleanup_dmg);

	/*
	 * Open for updating, as previously only write-only files would be flushed
	 * on fclose.
	 */
	FILE *fp = fopen(small_file, "a+");
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fp, "opened file at %s for append-updating", small_file);

	char *buf = malloc(BUFSIZ);
	T_QUIET; T_WITH_ERRNO;
	T_ASSERT_NOTNULL(buf, "should allocate BUFSIZ bytes");

	for (int i = 0; i < BUFSIZ; i++) {
		buf[i] = (char)(i % 256);
	}

	/*
	 * Fill up the disk -- induce ENOSPC.
	 */
	size_t wrsize = BUFSIZ;
	for (int i = 0; i < 2; i++) {
		for (;;) {
			errno = 0;
			if (write(fileno(fp), buf, wrsize) < 0) {
				if (errno == ENOSPC) {
					break;
				}
				T_WITH_ERRNO; T_ASSERT_FAIL("write(2) failed");
			}
		}
		wrsize = 1;
	}
	T_PASS("filled up the file until ENOSPC");
	free(buf);

	/*
	 * Make sure the FILE is at the end, so any writes it does hit ENOSPC.
	 */
	ret = fseek(fp, 0, SEEK_END);
	T_ASSERT_POSIX_SUCCESS(ret, "fseek to the end of a complete file");

	/*
	 * Try to push a character into the file; since this is buffered, it should
	 * succeed.
	 */
	ret = fputc('a', fp);
	T_ASSERT_POSIX_SUCCESS(ret,
			"fputc to put an additional character in the FILE");

	T_SETUPEND;

	/*
	 * fclose should catch the ENOSPC error when it flushes the file, before it
	 * closes the underlying descriptor.
	 */
	errno = 0;
	ret = fclose(fp);
	if (ret != EOF) {
		T_ASSERT_FAIL("fclose should fail when the FILE is full");
	}
	if (errno != ENOSPC) {
		T_WITH_ERRNO; T_ASSERT_FAIL("fclose should fail with ENOSPC");
	}

	T_PASS("fclose returned ENOSPC");
}

#endif // TARGET_OS_OSX

/*
 * Ensure no errors are returned when flushing a read-only, unseekable input
 * stream.
 */
T_DECL(fflush_unseekable_input,
		"ensure sanity when an unseekable input stream is flushed")
{
	T_SETUPBEGIN;

	/*
	 * Use a pipe for the unseekable streams.
	 */
	int pipes[2];
	int ret = pipe(pipes);
	T_ASSERT_POSIX_SUCCESS(ret, "create a pipe");
	FILE *in = fdopen(pipes[0], "r");
	T_QUIET; T_WITH_ERRNO; T_ASSERT_NOTNULL(in,
			"open input stream to read end of pipe");
	FILE *out = fdopen(pipes[1], "w");
	T_QUIET; T_WITH_ERRNO; T_ASSERT_NOTNULL(out,
			"open output stream to write end of pipe");

	/*
	 * Fill the pipe with some text (but not too much that the write would
	 * block!).
	 */
	fprintf(out, "this is a test and has some more text");
	ret = fflush(out);
	T_ASSERT_POSIX_SUCCESS(ret, "flushed the output stream");

	/*
	 * Protect stdio from delving too deep into the pipe.
	 */
	char inbuf[8] = {};
	setbuffer(in, inbuf, sizeof(inbuf));

	/*
	 * Just read a teensy bit to get the FILE offset different from the
	 * descriptor "offset."
	 */
	char rdbuf[2] = {};
	size_t nitems = fread(rdbuf, sizeof(rdbuf), 1, in);
	T_QUIET; T_ASSERT_GT(nitems, (size_t)0,
			"read from the read end of the pipe");

	T_SETUPEND;

	ret = fflush(in);
	T_ASSERT_POSIX_SUCCESS(ret,
			"should successfully flush unseekable input stream after reading");
}

/*
 * Ensure that reading to the end of a file and then calling ftell() still
 * causes EOF.
 */
T_DECL(ftell_feof,
		"ensure ftell does not reset feof when actually at end of file") {
	T_SETUPBEGIN;
	FILE *fp = fopen("/System/Library/CoreServices/SystemVersion.plist", "rb");
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fp, "opened SystemVersion.plist");
	struct stat sb;
	T_ASSERT_POSIX_SUCCESS(fstat(fileno(fp), &sb), "fstat SystemVersion.plist");
	void *buf = malloc(sb.st_size * 2);
	T_ASSERT_NOTNULL(buf, "allocating buffer for size of SystemVersion.plist");
	T_SETUPEND;

	T_ASSERT_POSIX_SUCCESS(fseek(fp, 0, SEEK_SET), "seek to beginning");
	// fread can return short *or* zero, according to manpage
	fread(buf, sb.st_size * 2, 1, fp);
	T_ASSERT_EQ(ftell(fp), sb.st_size, "tfell() == file size");
	T_ASSERT_TRUE(feof(fp), "feof() reports end-of-file");
	free(buf);
}

T_DECL(putc_flush, "ensure putc flushes to file on close") {
	const char *fname = assert_empty_tmp_file();
	FILE *fp = fopen(fname, "w");
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
	T_WITH_ERRNO;
	T_ASSERT_EQ(fwrite("testing", 1, 7, fp), 7, "write temp contents");
	(void)fclose(fp);

	fp = fopen(fname, "r+");
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");

	T_ASSERT_POSIX_SUCCESS(fseek(fp, -1, SEEK_END), "seek to end - 1");
	T_ASSERT_EQ(fgetc(fp), 'g', "fgetc should read 'g'");
	T_ASSERT_EQ(fgetc(fp), EOF, "fgetc should read EOF");
	T_ASSERT_EQ(ftell(fp), 7, "tfell should report position 7");

	int ret = fputc('!', fp);
	T_ASSERT_POSIX_SUCCESS(ret,
			"fputc to put an additional character in the FILE");
	T_ASSERT_EQ(ftell(fp), 8, "tfell should report position 8");

	T_QUIET;
	T_ASSERT_POSIX_SUCCESS(fclose(fp), "close temp file");

	fp = fopen(fname, "r");
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");

	char buf[9];
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fgets(buf, sizeof(buf), fp), "read file data");
	T_ASSERT_EQ_STR(buf, "testing!", "read all the new data");

	(void)fclose(fp);
}

T_DECL(putc_writedrop, "ensure writes are flushed with a pending read buffer") {
	const char *fname = assert_empty_tmp_file();
	FILE *fp = fopen(fname, "w");
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
	T_WITH_ERRNO;
	T_ASSERT_EQ(fwrite("testing", 1, 7, fp), 7, "write temp contents");
	(void)fclose(fp);

	fp = fopen(fname, "r+");
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");

	T_ASSERT_POSIX_SUCCESS(fseek(fp, -1, SEEK_END), "seek to end - 1");

	int ret = fputc('!', fp);
	T_ASSERT_POSIX_SUCCESS(ret,
			"fputc to put an additional character in the FILE");
	// flush the write buffer by reading a byte from the stream to put the
	// FILE* into read mode
	T_ASSERT_EQ(fgetc(fp), EOF, "fgetc should read EOF");

	T_QUIET;
	T_ASSERT_POSIX_SUCCESS(fclose(fp), "close temp file");

	fp = fopen(fname, "r");
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");

	char buf[9];
	T_WITH_ERRNO;
	T_ASSERT_NOTNULL(fgets(buf, sizeof(buf), fp), "read file data");
	T_ASSERT_EQ_STR(buf, "testin!", "read all the new data");

	(void)fclose(fp);
}