From 80130ceb0805c3ad7cdd6d51791d570689030bc1 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Wed, 12 Aug 2026 11:13:30 -0700 Subject: [PATCH] fix(encoder): don't return the list iterator when no encoder matches update_encoder_list_cinfo() declares enc_ctx uninitialised and uses it as the list_for_each_entry() iterator. For a DVB stream carrying a language, the search loop continues past every encoder whose dvb_lang does not match, so it can finish without returning. When the encoder list is non-empty none of the creation branches below run either, and the function falls through to enc_ctx->prev = NULL; with enc_ctx still holding the value the loop terminated on: the list head reinterpreted as an encoder_ctx. That pointer is written to, and later general_loop.c does dvb_enc->timing = dvb_dec->timing on it, which segfaults. It reproduces on any recording that carries both a teletext stream and a DVB subtitle stream, in every output format, with no extra flags: ccextractor --out=srt sample.mpg -> SIGSEGV ccextractor --out=webvtt sample.ts -> SIGSEGV Restricting to one PID (--datapid) avoids it, which is why the regression suite never caught it: the only affected sample it runs passes --datapid. Clear the iterator after the loop so "not found" is distinguishable, and give such a stream its own per-language encoder, which is what the multi-DVB path already does. The existing dvb_pid_count >= 2 test stays as an OR so a recording whose only caption stream is a single DVB PID keeps its plain output filename -- that case reaches the standard path with an empty encoder list. A NULL guard before the tail keeps the fall-through honest if some future path adds one. Both streams are now extracted into separate, well-formed files (out.srt plus out_eng.srt) rather than one file with two interleaved numberings. --- src/lib_ccx/lib_ccx.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/lib_ccx/lib_ccx.c b/src/lib_ccx/lib_ccx.c index 1b174c3d1..9d0d54b26 100644 --- a/src/lib_ccx/lib_ccx.c +++ b/src/lib_ccx/lib_ccx.c @@ -469,6 +469,11 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca return enc_ctx; } + /* Nothing matched. list_for_each_entry() leaves the iterator pointing at the list + head reinterpreted as an encoder_ctx, which is not a usable encoder, so clear it + to let the code below tell "not found" apart from "found". */ + enc_ctx = NULL; + const char *extension = get_file_extension(ccx_options.enc_cfg.write_format); if (!extension && ccx_options.enc_cfg.write_format != CCX_OF_CURL) return NULL; @@ -487,7 +492,12 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca dvb_pid_count++; } - if (dvb_pid_count >= 2) + /* Give the stream its own per-language encoder when there is more than one DVB + PID, or when some other stream (teletext, 608/708) already owns an encoder and + this DVB PID is an extra one alongside it. A recording whose only caption + stream is a single DVB PID still reaches the standard path below, because the + encoder list is empty on that first call, which keeps its filename unchanged. */ + if (dvb_pid_count >= 2 || !list_empty(&ctx->enc_ctx_head)) { struct encoder_cfg local_cfg = ccx_options.enc_cfg; local_cfg.program_number = pn; @@ -574,6 +584,11 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca list_add_tail(&(enc_ctx->list), &(ctx->enc_ctx_head)); } + /* No encoder was found or created. Report it rather than dereferencing the iterator + left behind by the search loop; callers already skip a NULL encoder. */ + if (!enc_ctx) + return NULL; + // DVB related enc_ctx->prev = NULL; if (cinfo && cinfo->codec == CCX_CODEC_DVB && cinfo->lang[0])