Files
RetroArch/tasks/task_decompress.c
T
LibretroAdmin b9be38fd55 strl: copy string literals without scanning them at runtime
strlcpy() begins with strlen() of its source. It lives in another
translation unit, so with no LTO that scan happens at runtime even when
the source is a literal whose length the compiler already measured, and
the call itself cannot be folded away either.

strlcpy_lit() passes the length over as sizeof(). When the destination
size is also a constant -- sizeof(buf), the usual case -- nothing is
left but the stores memcpy() compiles to:

  strlcpy      "core_options"  sizeof(buf)   7.01 ns
  strlcpy_lit  "core_options"  sizeof(buf)   0.42 ns
  strlcpy      41-char literal sizeof(buf)   7.74 ns
  strlcpy_lit  41-char literal sizeof(buf)   0.71 ns
  strlcpy      "core_options"  runtime len   7.11 ns
  strlcpy_lit  "core_options"  runtime len   0.70 ns

The destination bound is kept. When the literal does not fit it defers
to strlcpy(), so the copy truncates as before rather than overrunning;
a form that took only the literal would have turned every one of these
sites into an overflow waiting for a small buffer. The literal is
concatenated with "" so a non-literal source is a compile error rather
than a sizeof() that silently measures a pointer.

818 sites across 104 files were converted by parsing arguments rather
than by pattern-matching text: only a three-argument call whose second
argument is a bare string literal is rewritten, so a macro that happens
to expand to a literal is left alone. Line endings are preserved, so
the CRLF files carry only their own changed lines.

test/compat/test_strl.c compares the two against each other over every
destination size from 0 past the literal's length, for literals that
are empty, single-character, path-shaped, adjacently concatenated and
escape-bearing, checking the return, the terminator and the whole
buffer so an overrun past the terminator is caught. ASan+UBSan+LSan and
TSan both 4/4. The full menu_displaylist_build_list walk over every
displaylist type is byte-identical to before the change
(md5 a2a081bcac09cdc8d72d67efd14ac40e).
2026-08-30 02:10:20 +02:00

532 lines
17 KiB
C

/* RetroArch - A frontend for libretro.
* Copyright (C) 2011-2017 - Daniel De Matteis
* Copyright (C) 2016-2019 - Brad Parker
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <file/file_path.h>
#include <file/archive_file.h>
#include <retro_miscellaneous.h>
#include <compat/strl.h>
#include "tasks_internal.h"
#include "../file_path_special.h"
#include "../msg_hash.h"
#define CALLBACK_ERROR_SIZE 4200
static int file_decompressed_target_file(const char *name,
const char *valid_exts, const uint8_t *cdata,
unsigned cmode, uint32_t csize, uint32_t size,
uint32_t crc32, struct archive_extract_userdata *userdata)
{
return 0;
}
/* Rejects archive member names that could escape the intended
* extraction directory ("Zip Slip"). Returns true if the name is
* safe to concatenate onto a target-directory prefix. */
static bool archive_name_is_safe(const char *name)
{
const char *p;
const char *seg_start;
if (!name || !*name)
return false;
/* Reject absolute paths in either Unix or Windows form. */
if (name[0] == '/' || name[0] == '\\')
return false;
/* Reject drive-letter prefix e.g. "C:..." */
if (name[1] == ':')
return false;
/* Reject any ".." path segment. Treat both '/' and '\\' as
* separators so Windows-authored archives can't traverse when
* extracted on a POSIX host. */
p = seg_start = name;
for (;;)
{
char c = *p;
if (c == '/' || c == '\\' || c == '\0')
{
if ((size_t)(p - seg_start) == 2
&& seg_start[0] == '.' && seg_start[1] == '.')
return false;
if (c == '\0')
break;
seg_start = p + 1;
}
p++;
}
return true;
}
static int file_decompressed_subdir(const char *name,
const char *valid_exts,
const uint8_t *cdata,
unsigned cmode, uint32_t csize, uint32_t size,
uint32_t crc32, struct archive_extract_userdata *userdata)
{
char path_dir[DIR_MAX_LENGTH];
char path[PATH_MAX_LENGTH];
size_t _len;
/* Reject empty names (strlen-1 OOB) and traversal attempts. */
if (!archive_name_is_safe(name))
return 1;
_len = strlen(name);
/* Look at last character. Ignore directories, go to next file. */
if (name[_len - 1] == '/' || name[_len - 1] == '\\')
return 1;
/* Loop multiple subdirs */
if (strchr(userdata->dec->subdir, '|'))
{
const char *subdir = userdata->dec->subdir;
bool found = false;
while (*subdir)
{
const char *end = strchr(subdir, '|');
size_t len = end ? (size_t)(end - subdir) : strlen(subdir);
if (strncmp(name, subdir, len) == 0
&& (name[len] == '/' || name[len] == '\\' || name[len] == '\0'))
{
found = true;
break;
}
if (end)
subdir = end + 1;
else
break;
}
if (!found)
return 1;
}
else if (strstr(name, userdata->dec->subdir) != name)
return 1;
/* Remove desired subdir from the file path
* if not extracting multiple subdirs */
if (!strchr(userdata->dec->subdir, '|'))
name += strlen(userdata->dec->subdir) + 1;
fill_pathname_join_special(path,
userdata->dec->target_dir, name, sizeof(path));
fill_pathname_basedir(path_dir, path, sizeof(path_dir));
/* Make directory */
if (path_mkdir(path_dir))
{
/* Start the decode rather than driving it to completion here.
* Whatever is left is parked in the transfer and finished by
* file_archive_parse_file_iterate() on later ticks, one slice
* per tick, so a large member no longer holds the frame. */
if (file_archive_perform_mode_start(path, valid_exts,
cdata, cmode, csize, size, crc32, userdata) != -1)
return 1;
}
userdata->dec->callback_error = (char*)malloc(CALLBACK_ERROR_SIZE);
/* NULL-check: the strlcpy below NULL-derefs on OOM. The
* downstream task_set_error calls (lines 246, 274, 304) accept
* NULL, so skipping the error-string population leaves the task
* with a NULL error and the user sees no 'Failed to deflate'
* message - which is strictly better than segfaulting. Any
* subsequent 'if (dec->callback_error)' downstream is already
* NULL-safe. */
if (userdata->dec->callback_error)
{
_len = strlcpy_lit(userdata->dec->callback_error,
"Failed to deflate ",
CALLBACK_ERROR_SIZE);
_len += strlcpy(
userdata->dec->callback_error + _len,
path,
CALLBACK_ERROR_SIZE - _len);
userdata->dec->callback_error[ _len] = '.';
userdata->dec->callback_error[++_len] = '\n';
userdata->dec->callback_error[++_len] = '\0';
}
return 0;
}
static int file_decompressed(const char *name, const char *valid_exts,
const uint8_t *cdata, unsigned cmode, uint32_t csize, uint32_t size,
uint32_t crc32, struct archive_extract_userdata *userdata)
{
char path[PATH_MAX_LENGTH];
decompress_state_t *dec = userdata->dec;
size_t _len;
/* Reject empty names (strlen-1 OOB) and traversal attempts. */
if (!archive_name_is_safe(name))
return 1;
_len = strlen(name);
/* Look at last character. Ignore directories, go to next file. */
if (name[_len - 1] == '/' || name[_len - 1] == '\\')
return 1;
/* Make directory */
fill_pathname_join_special(path, dec->target_dir, name, sizeof(path));
path_basedir_wrapper(path);
if (path_mkdir(path))
{
fill_pathname_join_special(path, dec->target_dir, name, sizeof(path));
/* Started, not completed; see the twin in file_archived. */
if (file_archive_perform_mode_start(path, valid_exts,
cdata, cmode, csize, size, crc32, userdata) != -1)
return 1;
}
dec->callback_error = (char*)malloc(CALLBACK_ERROR_SIZE);
/* NULL-check: see twin in file_archived for reasoning. */
if (dec->callback_error)
{
_len = strlcpy_lit(dec->callback_error, "Failed to deflate ",
CALLBACK_ERROR_SIZE);
_len += strlcpy(dec->callback_error + _len,
path, CALLBACK_ERROR_SIZE - _len);
dec->callback_error[ _len] = '.';
dec->callback_error[++_len] = '\n';
dec->callback_error[++_len] = '\0';
}
return 0;
}
static void task_decompress_handler_finished(retro_task_t *task,
decompress_state_t *dec)
{
uint8_t flg;
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
flg = task_get_flags(task);
if (!task_get_error(task) && ((flg & RETRO_TASK_FLG_CANCELLED) > 0))
task_set_error(task, strdup("Task canceled"));
if (!task_get_error(task))
{
decompress_task_data_t *data =
(decompress_task_data_t*)calloc(1, sizeof(*data));
/* The completion callback owns 'data' and frees both it and
* data->source_file, so it is handed a copy of the path
* rather than the state's own buffer: dec->source_file must
* stay live until the task is fully retired (see the note
* below). On allocation failure a task error is set instead,
* so downstream consumers see a clean error state (a NULL
* task_data, which they all check) rather than partial
* success. */
if (data)
data->source_file = strdup(dec->source_file);
if (data && data->source_file)
task_set_data(task, data);
else
{
free(data);
task_set_error(task, strdup("Out of memory"));
}
}
/* Deliberately NOT freeing 'dec' (task->state) here.
*
* This runs on the worker thread, inside the task handler, with
* no queue lock held: the task is still sitting in the running
* queue at this point, and after the handler returns it stays
* reachable through the finished and retiring lists until the
* main thread fully retires it - many frames later.
* task_decompress_finder() dereferences task->state->source_file
* on every candidate whose handler matches, so freeing the state
* here left it dangling for that entire window, and any
* task_push_decompress() racing a finishing extraction - e.g.
* cb_generic_download pushing the next archive from within
* task_queue gather while the previous one retires - called
* strcmp() on freed memory: a hard SIGSEGV. (Same bug and same
* fix as task_http.c's finder; see the note at the end of
* task_http_transfer_handler().)
*
* Teardown now happens in task_decompress_cleanup(), which the
* task queue calls during retirement, after the task has been
* pruned from every list and is no longer findable. */
}
static void task_decompress_cleanup(retro_task_t *task)
{
decompress_state_t *dec = (decompress_state_t*)task->state;
if (!dec)
return;
/* Cleared first so that a finder which somehow still reaches
* this task sees no state rather than a stale pointer. */
task->state = NULL;
free(dec->source_file);
free(dec->target_dir);
free(dec->target_file);
free(dec->subdir);
free(dec->valid_ext);
free(dec->callback_error);
free(dec->userdata);
free(dec);
}
static void task_decompress_handler(retro_task_t *task)
{
int ret;
uint8_t flg;
bool retdec = false;
decompress_state_t *dec = (decompress_state_t*)task->state;
dec->userdata->dec = dec;
strlcpy(dec->userdata->archive_path,
dec->source_file, sizeof(dec->userdata->archive_path));
ret = file_archive_parse_file_iterate(
&dec->archive,
&retdec, dec->source_file,
dec->valid_ext, file_decompressed, dec->userdata);
task_set_progress(task,
file_archive_parse_file_progress(&dec->archive));
flg = task_get_flags(task);
if (((flg & RETRO_TASK_FLG_CANCELLED) > 0) || ret != 0)
{
task_set_error(task, dec->callback_error);
/* Ownership of the buffer moved to task->error (freed at
* retirement); cleared so task_decompress_cleanup() does
* not free it a second time. */
dec->callback_error = NULL;
file_archive_parse_file_iterate_stop(&dec->archive);
task_decompress_handler_finished(task, dec);
}
}
static void task_decompress_handler_target_file(retro_task_t *task)
{
int ret;
uint8_t flg;
bool retdec;
decompress_state_t *dec = (decompress_state_t*)task->state;
strlcpy(dec->userdata->archive_path,
dec->source_file, sizeof(dec->userdata->archive_path));
ret = file_archive_parse_file_iterate(&dec->archive,
&retdec, dec->source_file,
dec->valid_ext, file_decompressed_target_file, dec->userdata);
task_set_progress(task,
file_archive_parse_file_progress(&dec->archive));
flg = task_get_flags(task);
if (((flg & RETRO_TASK_FLG_CANCELLED) > 0) || ret != 0)
{
task_set_error(task, dec->callback_error);
/* Ownership of the buffer moved to task->error (freed at
* retirement); cleared so task_decompress_cleanup() does
* not free it a second time. */
dec->callback_error = NULL;
file_archive_parse_file_iterate_stop(&dec->archive);
task_decompress_handler_finished(task, dec);
}
}
static void task_decompress_handler_subdir(retro_task_t *task)
{
int ret;
uint8_t flg;
bool retdec;
decompress_state_t *dec = (decompress_state_t*)task->state;
dec->userdata->dec = dec;
strlcpy(dec->userdata->archive_path,
dec->source_file,
sizeof(dec->userdata->archive_path));
ret = file_archive_parse_file_iterate(
&dec->archive, &retdec, dec->source_file,
dec->valid_ext, file_decompressed_subdir, dec->userdata);
task_set_progress(task,
file_archive_parse_file_progress(&dec->archive));
flg = task_get_flags(task);
if (((flg & RETRO_TASK_FLG_CANCELLED) > 0) || ret != 0)
{
task_set_error(task, dec->callback_error);
/* Ownership of the buffer moved to task->error (freed at
* retirement); cleared so task_decompress_cleanup() does
* not free it a second time. */
dec->callback_error = NULL;
file_archive_parse_file_iterate_stop(&dec->archive);
task_decompress_handler_finished(task, dec);
}
}
static bool task_decompress_finder(
retro_task_t *task, void *user_data)
{
decompress_state_t *dec = (decompress_state_t*)task->state;
/* Every handler variant reads its source archive and keeps a
* decompress_state_t in task->state, so all of them count as
* 'this archive is being extracted' for dedup and for callers
* deciding whether the file is safe to overwrite. */
if ( task->handler != task_decompress_handler
&& task->handler != task_decompress_handler_subdir
&& task->handler != task_decompress_handler_target_file)
return false;
/* The state stays attached until the queue retires the task
* (task_decompress_cleanup), so a finished-but-not-yet-retired
* extraction still matches here. That is deliberate: find()
* must stay truthful for the whole window or a duplicate
* extraction could be pushed against the same destination. The
* NULL checks are belt-and-braces for a task whose cleanup has
* already run. */
if (!dec || !dec->source_file)
return false;
return !strcmp(dec->source_file, (const char*)user_data);
}
bool task_check_decompress(const char *source_file)
{
task_finder_data_t find_data;
/* Prepare find parameters */
find_data.func = task_decompress_finder;
find_data.userdata = (void *)source_file;
/* Return whether decompressing is in progress or not */
return task_queue_find(&find_data);
}
void *task_push_decompress(
const char *source_file,
const char *target_dir,
const char *target_file,
const char *subdir,
const char *valid_ext,
retro_task_callback_t cb,
void *user_data,
void *frontend_userdata,
bool mute)
{
size_t _len;
char tmp[PATH_MAX_LENGTH];
const char *ext = NULL;
decompress_state_t *s = NULL;
retro_task_t *t = NULL;
tmp[0] = '\0';
if (!target_dir || !*target_dir || !source_file || !*source_file)
return NULL;
ext = path_get_extension(source_file);
/* ZIP or APK only */
if (
!path_is_valid(source_file) ||
(
strcasecmp(ext, "zip") != 0
&& strcasecmp(ext, "apk") != 0
#ifdef HAVE_7ZIP
&& strcasecmp(ext, "7z") != 0
#endif
#if defined(HAVE_ZSTD) || defined(HAVE_RZSTD)
&& strcasecmp(ext, "zst") != 0
#endif
)
)
return NULL;
if (!valid_ext || !valid_ext[0])
valid_ext = NULL;
if (task_check_decompress(source_file))
return NULL;
if (!(s = (decompress_state_t*)calloc(1, sizeof(*s))))
return NULL;
if (!(t = task_init()))
{
free(s);
return NULL;
}
s->source_file = strdup(source_file);
s->target_dir = strdup(target_dir);
if (!s->source_file || !s->target_dir)
goto error;
s->valid_ext = valid_ext ? strdup(valid_ext) : NULL;
s->archive.type = ARCHIVE_TRANSFER_INIT;
s->userdata = (struct archive_extract_userdata*)
calloc(1, sizeof(*s->userdata));
if (!s->userdata)
goto error;
t->frontend_userdata= frontend_userdata;
t->state = s;
t->handler = task_decompress_handler;
/* Releases t->state; must run at retirement, not at FINISHED
* time - see the note in task_decompress_handler_finished(). */
t->cleanup = task_decompress_cleanup;
if (subdir && *subdir)
{
s->subdir = strdup(subdir);
if (!s->subdir)
goto error;
t->handler = task_decompress_handler_subdir;
}
else if (target_file && *target_file)
{
s->target_file = strdup(target_file);
if (!s->target_file)
goto error;
t->handler = task_decompress_handler_target_file;
}
t->callback = cb;
t->user_data = user_data;
_len = strlcpy(tmp,
msg_hash_to_str(MSG_EXTRACTING), sizeof(tmp));
tmp[ _len] = ':';
tmp[++_len] = ' ';
tmp[++_len] = '\0';
strlcpy(tmp + _len,
path_basename(source_file),
sizeof(tmp) - _len);
t->title = strdup(tmp);
if (!t->title)
goto error;
t->flags |= RETRO_TASK_FLG_ALTERNATIVE_LOOK;
if (mute)
t->flags |= RETRO_TASK_FLG_MUTE;
else
t->flags &= ~RETRO_TASK_FLG_MUTE;
task_queue_push(t);
return t;
error:
free(s->source_file);
free(s->target_dir);
free(s->valid_ext);
free(s->subdir);
free(s->target_file);
free(s->userdata);
free(s);
free(t);
return NULL;
}