1 /*
2  * Copyright © 2020 Valve Corporation
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a
5  * copy of this software and associated documentation files (the "Software"),
6  * to deal in the Software without restriction, including without limitation
7  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8  * and/or sell copies of the Software, and to permit persons to whom the
9  * Software is furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice (including the next
12  * paragraph) shall be included in all copies or substantial portions of the
13  * Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
18  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21  * IN THE SOFTWARE.
22  */
23 
24 /* This is a basic c implementation of a fossilize db like format intended for
25  * use with the Mesa shader cache.
26  *
27  * The format is compatible enough to allow the fossilize db tools to be used
28  * to do things like merge db collections.
29  */
30 
31 #include "fossilize_db.h"
32 
33 #ifdef FOZ_DB_UTIL
34 
35 #include <assert.h>
36 #include <stddef.h>
37 #include <stdlib.h>
38 #include <string.h>
39 #include <sys/file.h>
40 #include <sys/types.h>
41 #include <unistd.h>
42 
43 #include "crc32.h"
44 #include "hash_table.h"
45 #include "mesa-sha1.h"
46 #include "ralloc.h"
47 
48 #define FOZ_REF_MAGIC_SIZE 16
49 
50 static const uint8_t stream_reference_magic_and_version[FOZ_REF_MAGIC_SIZE] = {
51    0x81, 'F', 'O', 'S',
52    'S', 'I', 'L', 'I',
53    'Z', 'E', 'D', 'B',
54    0, 0, 0, FOSSILIZE_FORMAT_VERSION, /* 4 bytes to use for versioning. */
55 };
56 
57 /* Mesa uses 160bit hashes to identify cache entries, a hash of this size
58  * makes collisions virtually impossible for our use case. However the foz db
59  * format uses a 64bit hash table to lookup file offsets for reading cache
60  * entries so we must shorten our hash.
61  */
62 static uint64_t
truncate_hash_to_64bits(const uint8_t * cache_key)63 truncate_hash_to_64bits(const uint8_t *cache_key)
64 {
65    uint64_t hash = 0;
66    unsigned shift = 7;
67    for (unsigned i = 0; i < 8; i++) {
68       hash |= ((uint64_t)cache_key[i]) << shift * 8;
69       shift--;
70    }
71    return hash;
72 }
73 
74 static bool
check_files_opened_successfully(FILE * file,FILE * db_idx)75 check_files_opened_successfully(FILE *file, FILE *db_idx)
76 {
77    if (!file) {
78       if (db_idx)
79          fclose(db_idx);
80       return false;
81    }
82 
83    if (!db_idx) {
84       if (file)
85          fclose(file);
86       return false;
87    }
88 
89    return true;
90 }
91 
92 static bool
create_foz_db_filenames(char * cache_path,char * name,char ** filename,char ** idx_filename)93 create_foz_db_filenames(char *cache_path, char *name, char **filename,
94                         char **idx_filename)
95 {
96    if (asprintf(filename, "%s/%s.foz", cache_path, name) == -1)
97       return false;
98 
99    if (asprintf(idx_filename, "%s/%s_idx.foz", cache_path, name) == -1) {
100       free(*filename);
101       return false;
102    }
103 
104    return true;
105 }
106 
107 
108 /* This looks at stuff that was added to the index since the last time we looked at it. This is safe
109  * to do without locking the file as we assume the file is append only */
110 static void
update_foz_index(struct foz_db * foz_db,FILE * db_idx,unsigned file_idx)111 update_foz_index(struct foz_db *foz_db, FILE *db_idx, unsigned file_idx)
112 {
113    uint64_t offset = ftell(db_idx);
114    fseek(db_idx, 0, SEEK_END);
115    uint64_t len = ftell(db_idx);
116    uint64_t parsed_offset = offset;
117 
118    if (offset == len)
119       return;
120 
121    fseek(db_idx, offset, SEEK_SET);
122    while (offset < len) {
123       char bytes_to_read[FOSSILIZE_BLOB_HASH_LENGTH + sizeof(struct foz_payload_header)];
124       struct foz_payload_header *header;
125 
126       /* Corrupt entry. Our process might have been killed before we
127        * could write all data.
128        */
129       if (offset + sizeof(bytes_to_read) > len)
130          break;
131 
132       /* NAME + HEADER in one read */
133       if (fread(bytes_to_read, 1, sizeof(bytes_to_read), db_idx) !=
134           sizeof(bytes_to_read))
135          break;
136 
137       offset += sizeof(bytes_to_read);
138       header = (struct foz_payload_header*)&bytes_to_read[FOSSILIZE_BLOB_HASH_LENGTH];
139 
140       /* Corrupt entry. Our process might have been killed before we
141        * could write all data.
142        */
143       if (offset + header->payload_size > len ||
144           header->payload_size != sizeof(uint64_t))
145          break;
146 
147       char hash_str[FOSSILIZE_BLOB_HASH_LENGTH + 1] = {0};
148       memcpy(hash_str, bytes_to_read, FOSSILIZE_BLOB_HASH_LENGTH);
149 
150       /* read cache item offset from index file */
151       uint64_t cache_offset;
152       if (fread(&cache_offset, 1, sizeof(cache_offset), db_idx) !=
153           sizeof(cache_offset))
154          break;
155 
156       offset += header->payload_size;
157       parsed_offset = offset;
158 
159       struct foz_db_entry *entry = ralloc(foz_db->mem_ctx,
160                                           struct foz_db_entry);
161       entry->header = *header;
162       entry->file_idx = file_idx;
163       _mesa_sha1_hex_to_sha1(entry->key, hash_str);
164 
165       /* Truncate the entry's hash string to a 64bit hash for use with a
166        * 64bit hash table for looking up file offsets.
167        */
168       hash_str[16] = '\0';
169       uint64_t key = strtoull(hash_str, NULL, 16);
170 
171       entry->offset = cache_offset;
172 
173       _mesa_hash_table_u64_insert(foz_db->index_db, key, entry);
174    }
175 
176 
177    fseek(db_idx, parsed_offset, SEEK_SET);
178 }
179 
180 /* exclusive flock with timeout. timeout is in nanoseconds */
lock_file_with_timeout(FILE * f,int64_t timeout)181 static int lock_file_with_timeout(FILE *f, int64_t timeout)
182 {
183    int err;
184    int fd = fileno(f);
185    int64_t iterations = MAX2(DIV_ROUND_UP(timeout, 1000000), 1);
186 
187    /* Since there is no blocking flock with timeout and we don't want to totally spin on getting the
188     * lock, use a nonblocking method and retry every millisecond. */
189    for (int64_t iter = 0; iter < iterations; ++iter) {
190       err = flock(fd, LOCK_EX | LOCK_NB);
191       if (err == 0 || errno != EAGAIN)
192          break;
193       usleep(1000);
194    }
195    return err;
196 }
197 
198 static bool
load_foz_dbs(struct foz_db * foz_db,FILE * db_idx,uint8_t file_idx,bool read_only)199 load_foz_dbs(struct foz_db *foz_db, FILE *db_idx, uint8_t file_idx,
200              bool read_only)
201 {
202    /* Scan through the archive and get the list of cache entries. */
203    fseek(db_idx, 0, SEEK_END);
204    size_t len = ftell(db_idx);
205    rewind(db_idx);
206 
207    /* Try not to take the lock if len >= the size of the header, but if it is smaller we take the
208     * lock to potentially initialize the files. */
209    if (len < sizeof(stream_reference_magic_and_version)) {
210       /* Wait for 100 ms in case of contention, after that we prioritize getting the app started. */
211       int err = lock_file_with_timeout(foz_db->file[file_idx], 100000000);
212       if (err == -1)
213          goto fail;
214 
215       /* Compute length again so we know nobody else did it in the meantime */
216       fseek(db_idx, 0, SEEK_END);
217       len = ftell(db_idx);
218       rewind(db_idx);
219    }
220 
221    if (len != 0) {
222       uint8_t magic[FOZ_REF_MAGIC_SIZE];
223       if (fread(magic, 1, FOZ_REF_MAGIC_SIZE, db_idx) != FOZ_REF_MAGIC_SIZE)
224          goto fail;
225 
226       if (memcmp(magic, stream_reference_magic_and_version,
227                  FOZ_REF_MAGIC_SIZE - 1))
228          goto fail;
229 
230       int version = magic[FOZ_REF_MAGIC_SIZE - 1];
231       if (version > FOSSILIZE_FORMAT_VERSION ||
232           version < FOSSILIZE_FORMAT_MIN_COMPAT_VERSION)
233          goto fail;
234 
235    } else {
236       /* Appending to a fresh file. Make sure we have the magic. */
237       if (fwrite(stream_reference_magic_and_version, 1,
238                  sizeof(stream_reference_magic_and_version), foz_db->file[file_idx]) !=
239           sizeof(stream_reference_magic_and_version))
240          goto fail;
241 
242       if (fwrite(stream_reference_magic_and_version, 1,
243                  sizeof(stream_reference_magic_and_version), db_idx) !=
244           sizeof(stream_reference_magic_and_version))
245          goto fail;
246 
247       fflush(foz_db->file[file_idx]);
248       fflush(db_idx);
249    }
250 
251    flock(fileno(foz_db->file[file_idx]), LOCK_UN);
252 
253    update_foz_index(foz_db, db_idx, file_idx);
254 
255    foz_db->alive = true;
256    return true;
257 
258 fail:
259    flock(fileno(foz_db->file[file_idx]), LOCK_UN);
260    foz_destroy(foz_db);
261    return false;
262 }
263 
264 /* Here we open mesa cache foz dbs files. If the files exist we load the index
265  * db into a hash table. The index db contains the offsets needed to later
266  * read cache entries from the foz db containing the actual cache entries.
267  */
268 bool
foz_prepare(struct foz_db * foz_db,char * cache_path)269 foz_prepare(struct foz_db *foz_db, char *cache_path)
270 {
271    char *filename = NULL;
272    char *idx_filename = NULL;
273    if (!create_foz_db_filenames(cache_path, "foz_cache", &filename, &idx_filename))
274       return false;
275 
276    /* Open the default foz dbs for read/write. If the files didn't already exist
277     * create them.
278     */
279    foz_db->file[0] = fopen(filename, "a+b");
280    foz_db->db_idx = fopen(idx_filename, "a+b");
281 
282    free(filename);
283    free(idx_filename);
284 
285    if (!check_files_opened_successfully(foz_db->file[0], foz_db->db_idx))
286       return false;
287 
288    simple_mtx_init(&foz_db->mtx, mtx_plain);
289    simple_mtx_init(&foz_db->flock_mtx, mtx_plain);
290    foz_db->mem_ctx = ralloc_context(NULL);
291    foz_db->index_db = _mesa_hash_table_u64_create(NULL);
292 
293    if (!load_foz_dbs(foz_db, foz_db->db_idx, 0, false))
294       return false;
295 
296    uint8_t file_idx = 1;
297    char *foz_dbs = getenv("MESA_DISK_CACHE_READ_ONLY_FOZ_DBS");
298    if (!foz_dbs)
299       return true;
300 
301    for (unsigned n; n = strcspn(foz_dbs, ","), *foz_dbs;
302         foz_dbs += MAX2(1, n)) {
303       char *foz_db_filename = strndup(foz_dbs, n);
304 
305       filename = NULL;
306       idx_filename = NULL;
307       if (!create_foz_db_filenames(cache_path, foz_db_filename, &filename,
308                                    &idx_filename)) {
309          free(foz_db_filename);
310          continue; /* Ignore invalid user provided filename and continue */
311       }
312       free(foz_db_filename);
313 
314       /* Open files as read only */
315       foz_db->file[file_idx] = fopen(filename, "rb");
316       FILE *db_idx = fopen(idx_filename, "rb");
317 
318       free(filename);
319       free(idx_filename);
320 
321       if (!check_files_opened_successfully(foz_db->file[file_idx], db_idx)) {
322          /* Prevent foz_destroy from destroying it a second time. */
323          foz_db->file[file_idx] = NULL;
324 
325          continue; /* Ignore invalid user provided filename and continue */
326       }
327 
328       if (!load_foz_dbs(foz_db, db_idx, file_idx, true)) {
329          fclose(db_idx);
330          return false;
331       }
332 
333       fclose(db_idx);
334       file_idx++;
335 
336       if (file_idx >= FOZ_MAX_DBS)
337          break;
338    }
339 
340    return true;
341 }
342 
343 void
foz_destroy(struct foz_db * foz_db)344 foz_destroy(struct foz_db *foz_db)
345 {
346    if (foz_db->db_idx)
347       fclose(foz_db->db_idx);
348    for (unsigned i = 0; i < FOZ_MAX_DBS; i++) {
349       if (foz_db->file[i])
350          fclose(foz_db->file[i]);
351    }
352 
353    if (foz_db->mem_ctx) {
354       _mesa_hash_table_u64_destroy(foz_db->index_db);
355       ralloc_free(foz_db->mem_ctx);
356       simple_mtx_destroy(&foz_db->flock_mtx);
357       simple_mtx_destroy(&foz_db->mtx);
358    }
359 }
360 
361 /* Here we lookup a cache entry in the index hash table. If an entry is found
362  * we use the retrieved offset to read the cache entry from disk.
363  */
364 void *
foz_read_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,size_t * size)365 foz_read_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
366                size_t *size)
367 {
368    uint64_t hash = truncate_hash_to_64bits(cache_key_160bit);
369 
370    void *data = NULL;
371 
372    if (!foz_db->alive)
373       return NULL;
374 
375    simple_mtx_lock(&foz_db->mtx);
376 
377    struct foz_db_entry *entry =
378       _mesa_hash_table_u64_search(foz_db->index_db, hash);
379    if (!entry) {
380       update_foz_index(foz_db, foz_db->db_idx, 0);
381       entry = _mesa_hash_table_u64_search(foz_db->index_db, hash);
382    }
383    if (!entry) {
384       simple_mtx_unlock(&foz_db->mtx);
385       return NULL;
386    }
387 
388    uint8_t file_idx = entry->file_idx;
389    if (fseek(foz_db->file[file_idx], entry->offset, SEEK_SET) < 0)
390       goto fail;
391 
392    uint32_t header_size = sizeof(struct foz_payload_header);
393    if (fread(&entry->header, 1, header_size, foz_db->file[file_idx]) !=
394        header_size)
395       goto fail;
396 
397    /* Check for collision using full 160bit hash for increased assurance
398     * against potential collisions.
399     */
400    for (int i = 0; i < 20; i++) {
401       if (cache_key_160bit[i] != entry->key[i])
402          goto fail;
403    }
404 
405    uint32_t data_sz = entry->header.payload_size;
406    data = malloc(data_sz);
407    if (fread(data, 1, data_sz, foz_db->file[file_idx]) != data_sz)
408       goto fail;
409 
410    /* verify checksum */
411    if (entry->header.crc != 0) {
412       if (util_hash_crc32(data, data_sz) != entry->header.crc)
413          goto fail;
414    }
415 
416    simple_mtx_unlock(&foz_db->mtx);
417 
418    if (size)
419       *size = data_sz;
420 
421    return data;
422 
423 fail:
424    free(data);
425 
426    /* reading db entry failed. reset the file offset */
427    simple_mtx_unlock(&foz_db->mtx);
428 
429    return NULL;
430 }
431 
432 /* Here we write the cache entry to disk and store its offset in the index db.
433  */
434 bool
foz_write_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,const void * blob,size_t blob_size)435 foz_write_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
436                 const void *blob, size_t blob_size)
437 {
438    uint64_t hash = truncate_hash_to_64bits(cache_key_160bit);
439 
440    if (!foz_db->alive)
441       return false;
442 
443    /* The flock is per-fd, not per thread, we do it outside of the main mutex to avoid having to
444     * wait in the mutex potentially blocking reads. We use the secondary flock_mtx to stop race
445     * conditions between the write threads sharing the same file descriptor. */
446    simple_mtx_lock(&foz_db->flock_mtx);
447 
448    /* Wait for 1 second. This is done outside of the main mutex as I believe there is more potential
449     * for file contention than mtx contention of significant length. */
450    int err = lock_file_with_timeout(foz_db->file[0], 1000000000);
451    if (err == -1)
452       goto fail_file;
453 
454    simple_mtx_lock(&foz_db->mtx);
455 
456    update_foz_index(foz_db, foz_db->db_idx, 0);
457 
458    struct foz_db_entry *entry =
459       _mesa_hash_table_u64_search(foz_db->index_db, hash);
460    if (entry) {
461       simple_mtx_unlock(&foz_db->mtx);
462       flock(fileno(foz_db->file[0]), LOCK_UN);
463       simple_mtx_unlock(&foz_db->flock_mtx);
464       return NULL;
465    }
466 
467    /* Prepare db entry header and blob ready for writing */
468    struct foz_payload_header header;
469    header.uncompressed_size = blob_size;
470    header.format = FOSSILIZE_COMPRESSION_NONE;
471    header.payload_size = blob_size;
472    header.crc = util_hash_crc32(blob, blob_size);
473 
474    fseek(foz_db->file[0], 0, SEEK_END);
475 
476    /* Write hash header to db */
477    char hash_str[FOSSILIZE_BLOB_HASH_LENGTH + 1]; /* 40 digits + null */
478    _mesa_sha1_format(hash_str, cache_key_160bit);
479    if (fwrite(hash_str, 1, FOSSILIZE_BLOB_HASH_LENGTH, foz_db->file[0]) !=
480        FOSSILIZE_BLOB_HASH_LENGTH)
481       goto fail;
482 
483    off_t offset = ftell(foz_db->file[0]);
484 
485    /* Write db entry header */
486    if (fwrite(&header, 1, sizeof(header), foz_db->file[0]) != sizeof(header))
487       goto fail;
488 
489    /* Now write the db entry blob */
490    if (fwrite(blob, 1, blob_size, foz_db->file[0]) != blob_size)
491       goto fail;
492 
493    /* Flush everything to file to reduce chance of cache corruption */
494    fflush(foz_db->file[0]);
495 
496    /* Write hash header to index db */
497    if (fwrite(hash_str, 1, FOSSILIZE_BLOB_HASH_LENGTH, foz_db->db_idx) !=
498        FOSSILIZE_BLOB_HASH_LENGTH)
499       goto fail;
500 
501    header.uncompressed_size = sizeof(uint64_t);
502    header.format = FOSSILIZE_COMPRESSION_NONE;
503    header.payload_size = sizeof(uint64_t);
504    header.crc = 0;
505 
506    if (fwrite(&header, 1, sizeof(header), foz_db->db_idx) !=
507        sizeof(header))
508       goto fail;
509 
510    if (fwrite(&offset, 1, sizeof(uint64_t), foz_db->db_idx) !=
511        sizeof(uint64_t))
512       goto fail;
513 
514    /* Flush everything to file to reduce chance of cache corruption */
515    fflush(foz_db->db_idx);
516 
517    entry = ralloc(foz_db->mem_ctx, struct foz_db_entry);
518    entry->header = header;
519    entry->offset = offset;
520    entry->file_idx = 0;
521    _mesa_sha1_hex_to_sha1(entry->key, hash_str);
522    _mesa_hash_table_u64_insert(foz_db->index_db, hash, entry);
523 
524    simple_mtx_unlock(&foz_db->mtx);
525    flock(fileno(foz_db->file[0]), LOCK_UN);
526    simple_mtx_unlock(&foz_db->flock_mtx);
527 
528    return true;
529 
530 fail:
531    simple_mtx_unlock(&foz_db->mtx);
532 fail_file:
533    flock(fileno(foz_db->file[0]), LOCK_UN);
534    simple_mtx_unlock(&foz_db->flock_mtx);
535    return false;
536 }
537 #else
538 
539 bool
foz_prepare(struct foz_db * foz_db,char * filename)540 foz_prepare(struct foz_db *foz_db, char *filename)
541 {
542    fprintf(stderr, "Warning: Mesa single file cache selected but Mesa wasn't "
543            "built with single cache file support. Shader cache will be disabled"
544            "!\n");
545    return false;
546 }
547 
548 void
foz_destroy(struct foz_db * foz_db)549 foz_destroy(struct foz_db *foz_db)
550 {
551 }
552 
553 void *
foz_read_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,size_t * size)554 foz_read_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
555                size_t *size)
556 {
557    return false;
558 }
559 
560 bool
foz_write_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,const void * blob,size_t size)561 foz_write_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
562                 const void *blob, size_t size)
563 {
564    return false;
565 }
566 
567 #endif
568